diff --git a/.changeset/coffee-is-good.md b/.changeset/coffee-is-good.md new file mode 100644 index 0000000000..e7ed39dc12 --- /dev/null +++ b/.changeset/coffee-is-good.md @@ -0,0 +1,5 @@ +--- +"@finos/legend-studio": minor +--- + +Add (WIP) global testable runner v0. \ No newline at end of file diff --git a/.changeset/poor-shirts-matter.md b/.changeset/poor-shirts-matter.md new file mode 100644 index 0000000000..c5537e992d --- /dev/null +++ b/.changeset/poor-shirts-matter.md @@ -0,0 +1,7 @@ +--- +"@finos/legend-graph": minor +--- + +Deprecate running service Legacy Tests. +Add `RelationalData` and `EqualToTDS` to support testing on relational queries. +Support running testable tests. \ No newline at end of file diff --git a/.changeset/rick-shirts-matter.md b/.changeset/rick-shirts-matter.md new file mode 100644 index 0000000000..95d7132627 --- /dev/null +++ b/.changeset/rick-shirts-matter.md @@ -0,0 +1,4 @@ +--- +"@finos/legend-manual-tests": patch +"@finos/legend-art": patch +--- diff --git a/packages/legend-art/src/components/Icon.tsx b/packages/legend-art/src/components/Icon.tsx index beb59414f3..4743f3e901 100644 --- a/packages/legend-art/src/components/Icon.tsx +++ b/packages/legend-art/src/components/Icon.tsx @@ -129,6 +129,7 @@ export { MdEdit as EditIcon, // to be reviewed MdSubject as SubjectIcon, MdViewHeadline as ViewHeadlineIcon, + MdWarning as WarningIcon, } from 'react-icons/md'; export { VscError as ErrorIcon, diff --git a/packages/legend-graph/src/MetaModelConst.ts b/packages/legend-graph/src/MetaModelConst.ts index 5d3abd5b1e..8074213048 100644 --- a/packages/legend-graph/src/MetaModelConst.ts +++ b/packages/legend-graph/src/MetaModelConst.ts @@ -327,6 +327,7 @@ export enum CORE_HASH_STRUCTURE { EQUAL_TO_JSON_ASSERT_FAIL = 'EQUAL_TO_JSON_ASSERT_FAIL', EQUAL_TO = 'EQUAL_TO', EQUAL_TO_JSON = 'EQUAL_TO_JSON', + EQUAL_TO_TDS = 'EQUAL_TO_TDS', TEST_RESULT = 'TEST_RESULT', TEST_ERROR = 'TEST_ERROR', TEST_FAILED = 'TEST_FAILED', @@ -337,6 +338,11 @@ export enum CORE_HASH_STRUCTURE { MODEL_STORE_DATA = 'MODEL_STORE_DATA', DATA_ELEMENT_REFERENCE = 'DATA_ELEMENT_REFERENCE', DATA_ELEMENT = 'DATA_ELEMENT', + RELATIONAL_DATA = 'RELATIONAL_DATA', + RELATIONAL_TDS = 'RELATIONAL_TDS', + RELATIONAL_DATA_TABLE = 'RELATIONAL_DATA_TABLE', + RELATIONAL_DATA_TABLE_ROW = 'RELATIONAL_DATA_TABLE_ROW', + RELATIONAL_DATA_TABLE_COLUMN = 'RELATIONAL_DATA_TABLE_COLUMN', } export enum MILESTONING_STEREOTYPE { diff --git a/packages/legend-graph/src/graph/BasicModel.ts b/packages/legend-graph/src/graph/BasicModel.ts index c2af4c02f2..547c737256 100644 --- a/packages/legend-graph/src/graph/BasicModel.ts +++ b/packages/legend-graph/src/graph/BasicModel.ts @@ -65,6 +65,7 @@ import { getOrCreatePackage, } from '../helpers/DomainHelper'; import { DataElement } from '../models/metamodels/pure/packageableElements/data/DataElement'; +import type { Testable } from '../models/metamodels/pure/test/Testable'; const FORBIDDEN_EXTENSION_ELEMENT_CLASS = new Set([ PackageableElement, @@ -207,6 +208,14 @@ export abstract class BasicModel { return Array.from(this.generationSpecificationsIndex.values()); } + get ownTestables(): Testable[] { + return [ + ...this.ownServices, + // TODO: add mappings once supported in the backend + // ...this.ownMappings, + ]; + } + getExtensionElements( extensionElementClass: Clazz, ): T[] { diff --git a/packages/legend-graph/src/graph/PureGraphPlugin.ts b/packages/legend-graph/src/graph/PureGraphPlugin.ts index 98b2ea58f5..521ddbf354 100644 --- a/packages/legend-graph/src/graph/PureGraphPlugin.ts +++ b/packages/legend-graph/src/graph/PureGraphPlugin.ts @@ -18,6 +18,7 @@ import { AbstractPlugin, type Clazz } from '@finos/legend-shared'; import type { PackageableElement } from '../models/metamodels/pure/packageableElements/PackageableElement'; import type { PureModel } from './PureModel'; import type { GraphPluginManager } from '../GraphPluginManager'; +import type { TestableExtension } from './TestableExtension'; export type DeadReferencesCleaner = (graph: PureModel) => void; @@ -48,4 +49,6 @@ export abstract class PureGraphPlugin extends AbstractPlugin { * Get the list of procedures to be done to cleanup dead references in the graph. */ getExtraDeadReferencesCleaners?(): DeadReferencesCleaner[]; + + getExtraTestables?(): TestableExtension[]; } diff --git a/packages/legend-graph/src/graph/PureModel.ts b/packages/legend-graph/src/graph/PureModel.ts index d0a0920803..71920df390 100644 --- a/packages/legend-graph/src/graph/PureModel.ts +++ b/packages/legend-graph/src/graph/PureModel.ts @@ -38,7 +38,6 @@ import type { Mapping } from '../models/metamodels/pure/packageableElements/mapp import type { Profile } from '../models/metamodels/pure/packageableElements/domain/Profile'; import type { Stereotype } from '../models/metamodels/pure/packageableElements/domain/Stereotype'; import type { Tag } from '../models/metamodels/pure/packageableElements/domain/Tag'; -import type { PackageableElement } from '../models/metamodels/pure/packageableElements/PackageableElement'; import type { Store } from '../models/metamodels/pure/packageableElements/store/Store'; import { DependencyManager } from '../graph/DependencyManager'; import { ConcreteFunctionDefinition } from '../models/metamodels/pure/packageableElements/domain/ConcreteFunctionDefinition'; @@ -58,6 +57,9 @@ import { import type { PureGraphPlugin } from './PureGraphPlugin'; import { createPath } from '../MetaModelUtils'; import type { DataElement } from '../models/metamodels/pure/packageableElements/data/DataElement'; +import type { Testable } from '../models/metamodels/pure/test/Testable'; +import type { PackageableElement } from '../models/metamodels/pure/packageableElements/PackageableElement'; +import type { TestableExtension } from './TestableExtension'; /** * CoreModel holds meta models which are constant and basic building block of the graph. Since throughout the lifetime @@ -220,6 +222,19 @@ export class PureModel extends BasicModel { ]; } + get testableExtensions(): TestableExtension[] { + return this.graphPlugins.flatMap( + (plugin) => plugin.getExtraTestables?.() ?? [], + ); + } + + get allOwnTestables(): Testable[] { + const extraTestables = this.testableExtensions.flatMap( + (plugin) => plugin.getExtraTestables?.(this) ?? [], + ); + return [...this.ownTestables].concat(...extraTestables); + } + getProfileStereotype = ( path: string, value: string, diff --git a/packages/legend-graph/src/graph/TestableExtension.ts b/packages/legend-graph/src/graph/TestableExtension.ts new file mode 100644 index 0000000000..fdb4765f9b --- /dev/null +++ b/packages/legend-graph/src/graph/TestableExtension.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Testable } from '../models/metamodels/pure/test/Testable'; +import type { PureModel } from './PureModel'; + +export type TestableExtension = { + getExtraTestables?: (graph: PureModel) => Testable[]; + getIdFromTestable?: ( + testable: Testable, + graph: PureModel, + ) => string | undefined; + getTestableFromId?: (id: string, graph: PureModel) => Testable | undefined; +}; diff --git a/packages/legend-graph/src/graphManager/AbstractPureGraphManager.ts b/packages/legend-graph/src/graphManager/AbstractPureGraphManager.ts index 27737be091..74c4f1d9d6 100644 --- a/packages/legend-graph/src/graphManager/AbstractPureGraphManager.ts +++ b/packages/legend-graph/src/graphManager/AbstractPureGraphManager.ts @@ -26,7 +26,6 @@ import type { } from './action/generation/ImportConfigurationDescription'; import type { FileGenerationSpecification } from '../models/metamodels/pure/packageableElements/fileGeneration/FileGenerationSpecification'; import type { GenerationOutput } from './action/generation/GenerationOutput'; -import type { ServiceTestResult } from './action/service/ServiceTestResult'; import type { PackageableElement } from '../models/metamodels/pure/packageableElements/PackageableElement'; import type { PureModel, CoreModel, SystemModel } from '../graph/PureModel'; import type { Mapping } from '../models/metamodels/pure/packageableElements/mapping/Mapping'; @@ -62,6 +61,9 @@ import type { ExternalFormatDescription } from './action/externalFormat/External import type { ConfigurationProperty } from '../models/metamodels/pure/packageableElements/fileGeneration/ConfigurationProperty'; import type { GraphBuilderReport } from './GraphBuilderReport'; import type { ModelGenerationConfiguration } from '../models/ModelGenerationConfiguration'; +import type { DEPRECATED__ServiceTestResult } from './action/service/DEPRECATED__ServiceTestResult'; +import type { RunTestsTestableInput } from '../models/metamodels/pure/test/result/RunTestsTestableInput'; +import type { TestResult } from '../models/metamodels/pure/test/result/TestResult'; export interface TEMPORARY__EngineSetupConfig { env: string; @@ -209,6 +211,12 @@ export abstract class AbstractPureGraphManager { options?: { keepSourceInformation?: boolean }, ): Promise; + // ------------------------------------------- Test ------------------------------------------- + abstract runTests( + graph: PureModel, + testableInputs: RunTestsTestableInput[], + ): Promise; + // ------------------------------------------- ValueSpecification ------------------------------------------- abstract buildValueSpecification( @@ -333,15 +341,19 @@ export abstract class AbstractPureGraphManager { executionMode: ServiceExecutionMode, version: string | undefined, ): Promise; - abstract runServiceTests( - service: Service, - graph: PureModel, - ): Promise; abstract activateService( serviceUrl: string, serviceId: string, ): Promise; + /** + * @deprecated + */ + abstract runLegacyServiceTests( + service: Service, + graph: PureModel, + ): Promise; + // ------------------------------------------- Query ------------------------------------------- abstract searchQueries( diff --git a/packages/legend-graph/src/graphManager/action/changeDetection/DSLService_ObserverHelper.ts b/packages/legend-graph/src/graphManager/action/changeDetection/DSLService_ObserverHelper.ts index 8a4abdbb9a..fa0b0e0fad 100644 --- a/packages/legend-graph/src/graphManager/action/changeDetection/DSLService_ObserverHelper.ts +++ b/packages/legend-graph/src/graphManager/action/changeDetection/DSLService_ObserverHelper.ts @@ -52,7 +52,6 @@ import type { TestData } from '../../../models/metamodels/pure/packageableElemen import { observe_AtomicTest, observe_TestAssertion, - observe_TestSuite, } from './Test_ObserverHelper'; export const observe_ConnectionTestData = skipObservedWithContext( @@ -296,10 +295,7 @@ export const observe_Service = skipObservedWithContext( if (metamodel.test) { observe_ServiceTest_Legacy(metamodel.test); } - metamodel.tests.forEach((testSuite) => - observe_TestSuite(testSuite, context), - ); - + metamodel.tests.forEach((m) => observe_ServiceTestSuite(m, context)); return metamodel; }, ); diff --git a/packages/legend-graph/src/graphManager/action/changeDetection/Data_ObserverHelper.ts b/packages/legend-graph/src/graphManager/action/changeDetection/Data_ObserverHelper.ts index 0b68270d2c..80a6e6332f 100644 --- a/packages/legend-graph/src/graphManager/action/changeDetection/Data_ObserverHelper.ts +++ b/packages/legend-graph/src/graphManager/action/changeDetection/Data_ObserverHelper.ts @@ -21,6 +21,12 @@ import { DataElementReference, ModelStoreData, } from '../../../models/metamodels/pure/data/EmbeddedData'; +import { + type RelationalDataTable, + type RelationalDataTableColumn, + type RelationalDataTableRow, + RelationalData, +} from '../../../models/metamodels/pure/data/RelationalData'; import type { DataElement } from '../../../models/metamodels/pure/packageableElements/data/DataElement'; import type { EmbeddedData_PureGraphManagerPlugin_Extension } from '../../EmbeddedData_PureGraphManagerPlugin_Extension'; import { @@ -68,6 +74,48 @@ export const observe_ModelStoreData = skipObserved( }, ); +export const observe_RelationalDataTableColumn = skipObserved( + (metamodel: RelationalDataTableColumn): RelationalDataTableColumn => { + makeObservable(metamodel, { + hashCode: computed, + }); + return metamodel; + }, +); + +export const observe_RelationalDataTableRow = skipObserved( + (metamodel: RelationalDataTableRow): RelationalDataTableRow => { + makeObservable(metamodel, { + hashCode: computed, + }); + return metamodel; + }, +); + +const observe_RelationalDataTable = skipObserved( + (metamodel: RelationalDataTable): RelationalDataTable => { + makeObservable(metamodel, { + columns: observable, + rows: observable, + hashCode: computed, + }); + metamodel.columns.forEach(observe_RelationalDataTableColumn); + metamodel.rows.forEach(observe_RelationalDataTableRow); + return metamodel; + }, +); + +const observe_RelationalData = skipObserved( + (metamodel: RelationalData): RelationalData => { + makeObservable(metamodel, { + tables: observable, + hashCode: computed, + }); + metamodel.tables.forEach(observe_RelationalDataTable); + return metamodel; + }, +); + export function observe_EmbeddedData( metamodel: EmbeddedData, context: ObserverContext, @@ -78,6 +126,8 @@ export function observe_EmbeddedData( return observe_ExternalFormatData(metamodel); } else if (metamodel instanceof ModelStoreData) { return observe_ModelStoreData(metamodel); + } else if (metamodel instanceof RelationalData) { + return observe_RelationalData(metamodel); } const extraEmbeddedDataObservers = context.plugins.flatMap( (plugin) => diff --git a/packages/legend-graph/src/graphManager/action/changeDetection/Test_ObserverHelper.ts b/packages/legend-graph/src/graphManager/action/changeDetection/Test_ObserverHelper.ts index bb30e4dfa0..a614cc0cb8 100644 --- a/packages/legend-graph/src/graphManager/action/changeDetection/Test_ObserverHelper.ts +++ b/packages/legend-graph/src/graphManager/action/changeDetection/Test_ObserverHelper.ts @@ -19,98 +19,60 @@ import { ServiceTest } from '../../../DSLService_Exports'; import { ServiceTestSuite } from '../../../models/metamodels/pure/packageableElements/service/ServiceTestSuite'; import { EqualTo } from '../../../models/metamodels/pure/test/assertion/EqualTo'; import { EqualToJson } from '../../../models/metamodels/pure/test/assertion/EqualToJson'; -import { AssertFail } from '../../../models/metamodels/pure/test/assertion/status/AssertFail'; -import type { AssertionStatus } from '../../../models/metamodels/pure/test/assertion/status/AssertionStatus'; -import { AssertPass } from '../../../models/metamodels/pure/test/assertion/status/AssertPass'; -import { EqualToJsonAssertFail } from '../../../models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail'; +import { + EqualToTDS, + type RelationalTDS, +} from '../../../models/metamodels/pure/test/assertion/EqualToTDS'; import type { TestAssertion } from '../../../models/metamodels/pure/test/assertion/TestAssertion'; -import type { AtomicTestId } from '../../../models/metamodels/pure/test/result/AtomicTestId'; -import type { TestError } from '../../../models/metamodels/pure/test/result/TestError'; -import type { TestFailed } from '../../../models/metamodels/pure/test/result/TestFailed'; -import type { TestPassed } from '../../../models/metamodels/pure/test/result/TestPassed'; -import type { TestResult } from '../../../models/metamodels/pure/test/result/TestResult'; import type { AtomicTest, TestSuite, } from '../../../models/metamodels/pure/test/Test'; import { type ObserverContext, skipObserved } from './CoreObserverHelper'; -import { observe_ExternalFormatData } from './Data_ObserverHelper'; +import { + observe_ExternalFormatData, + observe_RelationalDataTableColumn, + observe_RelationalDataTableRow, +} from './Data_ObserverHelper'; import { observe_ServiceTest, observe_ServiceTestSuite, } from './DSLService_ObserverHelper'; -export const observe_AtomicTestId = skipObserved( - (metamodel: AtomicTestId): AtomicTestId => { - makeObservable(metamodel, { - testSuiteId: observable, - atomicTestId: observable, - hashCode: computed, - }); - - return metamodel; - }, -); - -export const observe_AssertFail = skipObserved( - (metamodel: AssertFail): AssertFail => { - makeObservable(metamodel, { - id: observable, - message: observable, - hashCode: computed, - }); - - return metamodel; - }, -); - -export const observe_AssertPass = skipObserved( - (metamodel: AssertPass): AssertPass => { - makeObservable(metamodel, { - id: observable, - hashCode: computed, - }); +const observe_EqualTo = skipObserved((metamodel: EqualTo): EqualTo => { + makeObservable(metamodel, { + id: observable, + expected: observable, + hashCode: computed, + }); - return metamodel; - }, -); + return metamodel; +}); -export const observe_EqualToJsonAssertFail = skipObserved( - (metamodel: EqualToJsonAssertFail): EqualToJsonAssertFail => { +const observe_RelationalTDS = skipObserved( + (metamodel: RelationalTDS): RelationalTDS => { makeObservable(metamodel, { - id: observable, - message: observable, - actual: observable, - expected: observable, + rows: observable, + columns: observable, hashCode: computed, }); - + metamodel.columns.forEach(observe_RelationalDataTableColumn); + metamodel.rows.forEach(observe_RelationalDataTableRow); return metamodel; }, ); -function observe_AssertionStatus(metamodel: AssertionStatus): AssertionStatus { - if (metamodel instanceof EqualToJsonAssertFail) { - return observe_EqualToJsonAssertFail(metamodel); - } else if (metamodel instanceof AssertFail) { - return observe_AssertFail(metamodel); - } else if (metamodel instanceof AssertPass) { - return observe_AssertPass(metamodel); - } - return metamodel; -} - -export const observe_EqualTo = skipObserved((metamodel: EqualTo): EqualTo => { +const observe_EqualToTDS = skipObserved((metamodel: EqualToTDS): EqualToTDS => { makeObservable(metamodel, { id: observable, expected: observable, hashCode: computed, }); - + observe_RelationalTDS(metamodel.expected); return metamodel; }); -export const observe_EqualToJson = skipObserved( +const observe_EqualToJson = skipObserved( (metamodel: EqualToJson): EqualToJson => { makeObservable(metamodel, { id: observable, @@ -124,65 +86,6 @@ export const observe_EqualToJson = skipObserved( }, ); -export const observe_TestError = skipObserved( - (metamodel: TestError): TestError => { - makeObservable(metamodel, { - testable: observable, - atomicTestId: observable, - error: observable, - hashCode: computed, - }); - - observe_AtomicTestId(metamodel.atomicTestId); - - return metamodel; - }, -); - -export const observe_TestFailed = skipObserved( - (metamodel: TestFailed): TestFailed => { - makeObservable(metamodel, { - testable: observable, - atomicTestId: observable, - assertStatuses: observable, - hashCode: computed, - }); - - metamodel.assertStatuses.forEach(observe_AssertionStatus); - observe_AtomicTestId(metamodel.atomicTestId); - - return metamodel; - }, -); - -export const observe_TestPassed = skipObserved( - (metamodel: TestPassed): TestPassed => { - makeObservable(metamodel, { - testable: observable, - atomicTestId: observable, - hashCode: computed, - }); - - observe_AtomicTestId(metamodel.atomicTestId); - - return metamodel; - }, -); - -export const observe_TestResult = skipObserved( - (metamodel: TestResult): TestResult => { - makeObservable(metamodel, { - testable: observable, - atomicTestId: observable, - hashCode: computed, - }); - - observe_AtomicTestId(metamodel.atomicTestId); - - return metamodel; - }, -); - export function observe_AtomicTest(metamodel: AtomicTest): AtomicTest { if (metamodel instanceof ServiceTest) { return observe_ServiceTest(metamodel); @@ -195,6 +98,8 @@ export function observe_TestAssertion(metamodel: TestAssertion): TestAssertion { return observe_EqualTo(metamodel); } else if (metamodel instanceof EqualToJson) { return observe_EqualToJson(metamodel); + } else if (metamodel instanceof EqualToTDS) { + return observe_EqualToTDS(metamodel); } return metamodel; } diff --git a/packages/legend-graph/src/graphManager/action/service/ServiceTestResult.ts b/packages/legend-graph/src/graphManager/action/service/DEPRECATED__ServiceTestResult.ts similarity index 80% rename from packages/legend-graph/src/graphManager/action/service/ServiceTestResult.ts rename to packages/legend-graph/src/graphManager/action/service/DEPRECATED__ServiceTestResult.ts index a690caa9d5..e8336a9382 100644 --- a/packages/legend-graph/src/graphManager/action/service/ServiceTestResult.ts +++ b/packages/legend-graph/src/graphManager/action/service/DEPRECATED__ServiceTestResult.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -export class ServiceTestResult { +/** + * TODO: Remove once migration from `ServiceTest_Legacy` to `ServiceTest` is complete + * @deprecated + */ +export class DEPRECATED__ServiceTestResult { name!: string; result!: boolean; } diff --git a/packages/legend-graph/src/index.ts b/packages/legend-graph/src/index.ts index 8d20315655..d50ad6521c 100644 --- a/packages/legend-graph/src/index.ts +++ b/packages/legend-graph/src/index.ts @@ -218,7 +218,7 @@ export * from './graphManager/action/generation/GenerationConfigurationDescripti export { GenerationOutput } from './graphManager/action/generation/GenerationOutput'; export { ServiceExecutionMode } from './graphManager/action/service/ServiceExecutionMode'; export { ServiceRegistrationResult } from './graphManager/action/service/ServiceRegistrationResult'; -export { ServiceTestResult } from './graphManager/action/service/ServiceTestResult'; +export { DEPRECATED__ServiceTestResult } from './graphManager/action/service/DEPRECATED__ServiceTestResult'; export { SourceInformation } from './graphManager/action/SourceInformation'; export { getGraphManager } from './models/protocols/pure/Pure'; @@ -281,6 +281,19 @@ export * from './models/protocols/pure/v1/transformation/pureGraph/to/V1_DSLExte export * from './models/ModelGenerationConfiguration'; export * from './models/protocols/pure/MappingGeneration_PureProtocolProcessorPlugin_Extension'; +// --------------------------------------------- TESTING -------------------------------------------------- + +export * from './models/metamodels/pure/test/Testable'; +export * from './models/metamodels/pure/test/result/RunTestsTestableInput'; +export * from './models/metamodels/pure/test/result/TestResult'; +export * from './models/metamodels/pure/test/assertion/status/AssertionStatus'; +export * from './models/metamodels/pure/test/assertion/status/AssertFail'; +export * from './models/metamodels/pure/test/assertion/status/AssertPass'; +export * from './models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail'; +export * from './models/metamodels/pure/test/assertion/TestAssertion'; +export * from './models/metamodels/pure/test/Test'; +export * from './models/metamodels/pure/test/result/AtomicTestId'; + // --------------------------------------------- OBSERVER -------------------------------------------------- export * from './graphManager/action/changeDetection/PackageableElementObserver'; diff --git a/packages/legend-graph/src/models/metamodels/pure/data/EmbeddedData.ts b/packages/legend-graph/src/models/metamodels/pure/data/EmbeddedData.ts index 47dbc32c80..e20b25861d 100644 --- a/packages/legend-graph/src/models/metamodels/pure/data/EmbeddedData.ts +++ b/packages/legend-graph/src/models/metamodels/pure/data/EmbeddedData.ts @@ -20,12 +20,14 @@ import { hashObjectWithoutSourceInformation } from '../../../../MetaModelUtils'; import type { DataElement } from '../packageableElements/data/DataElement'; import type { Class } from '../packageableElements/domain/Class'; import type { PackageableElementReference } from '../packageableElements/PackageableElementReference'; +import type { RelationalData } from './RelationalData'; export interface EmbeddedDataVisitor { visit_EmbeddedData(embeddedData: EmbeddedData): T; visit_ExternalFormatData(externalFormatData: ExternalFormatData): T; visit_ModelStoreData(modelStoreData: ModelStoreData): T; visit_DataElementReference(dataElementReference: DataElementReference): T; + visit_RelationalData(relationalData: RelationalData): T; } export abstract class EmbeddedData implements Hashable { diff --git a/packages/legend-graph/src/models/metamodels/pure/data/RelationalData.ts b/packages/legend-graph/src/models/metamodels/pure/data/RelationalData.ts new file mode 100644 index 0000000000..7915e9a8bc --- /dev/null +++ b/packages/legend-graph/src/models/metamodels/pure/data/RelationalData.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type Hashable, hashArray } from '@finos/legend-shared'; +import { CORE_HASH_STRUCTURE } from '../../../../MetaModelConst'; +import { type EmbeddedDataVisitor, EmbeddedData } from './EmbeddedData'; + +export class RelationalDataTableColumn implements Hashable { + value!: string; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE_COLUMN, + this.value, + ]); + } +} +export class RelationalDataTableRow implements Hashable { + values!: string; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE_ROW, + this.values, + ]); + } +} + +export class RelationalDataTable implements Hashable { + schemaName: string | undefined; + tableName!: string; + columns: RelationalDataTableColumn[] = []; + rows: RelationalDataTableRow[] = []; + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE, + this.schemaName ?? '', + this.tableName, + hashArray(this.columns), + hashArray(this.rows), + ]); + } +} +export class RelationalData extends EmbeddedData implements Hashable { + tables: RelationalDataTable[] = []; + accept_EmbeddedDataVisitor(visitor: EmbeddedDataVisitor): T { + return visitor.visit_RelationalData(this); + } + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE, + hashArray(this.tables), + ]); + } +} diff --git a/packages/legend-graph/src/models/metamodels/pure/packageableElements/service/Service.ts b/packages/legend-graph/src/models/metamodels/pure/packageableElements/service/Service.ts index 0b1d16ddd4..8b4de3607a 100644 --- a/packages/legend-graph/src/models/metamodels/pure/packageableElements/service/Service.ts +++ b/packages/legend-graph/src/models/metamodels/pure/packageableElements/service/Service.ts @@ -25,10 +25,11 @@ import type { StereotypeReference } from '../domain/StereotypeReference'; import type { TaggedValue } from '../domain/TaggedValue'; import type { DEPRECATED__ServiceTest } from './DEPRECATED__ServiceTest'; import type { ServiceTestSuite } from './ServiceTestSuite'; +import type { Testable } from '../../test/Testable'; export const DEFAULT_SERVICE_PATTERN = '/'; -export class Service extends PackageableElement implements Hashable { +export class Service extends PackageableElement implements Hashable, Testable { stereotypes: StereotypeReference[] = []; taggedValues: TaggedValue[] = []; pattern = '/'; diff --git a/packages/legend-graph/src/models/metamodels/pure/test/Test.ts b/packages/legend-graph/src/models/metamodels/pure/test/Test.ts index c27a537676..139cd0d488 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/Test.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/Test.ts @@ -17,10 +17,6 @@ import type { Hashable } from '@finos/legend-shared'; import type { TestAssertion } from './assertion/TestAssertion'; -export interface Testable { - tests: Test[]; -} - export abstract class Test implements Hashable { id!: string; @@ -28,6 +24,7 @@ export abstract class Test implements Hashable { } export abstract class AtomicTest extends Test implements Hashable { + parentSuite: TestSuite | undefined; assertions: TestAssertion[] = []; abstract override get hashCode(): string; diff --git a/packages/legend-graph/src/models/metamodels/pure/test/Testable.ts b/packages/legend-graph/src/models/metamodels/pure/test/Testable.ts new file mode 100644 index 0000000000..ba7fe2978b --- /dev/null +++ b/packages/legend-graph/src/models/metamodels/pure/test/Testable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isNonNullable } from '@finos/legend-shared'; +import type { PureModel } from '../../../../graph/PureModel'; +import { PackageableElement } from '../packageableElements/PackageableElement'; +import type { Test } from './Test'; + +export interface Testable { + tests: Test[]; +} +export const getNullableTestable = ( + id: string, + graph: PureModel, +): Testable | undefined => + graph.allOwnTestables.find( + (e) => e instanceof PackageableElement && e.path === id, + ) ?? + graph.testableExtensions + .map((g) => g.getTestableFromId?.(id, graph)) + .filter(isNonNullable)[0]; + +export const getNullableIdFromTestable = ( + testable: Testable, + graph: PureModel, +): string | undefined => { + if (testable instanceof PackageableElement) { + return testable.path; + } + return graph.testableExtensions + .map((g) => g.getIdFromTestable?.(testable, graph)) + .filter(isNonNullable)[0]; +}; diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualTo.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualTo.ts index e565e712c9..fd75f8a9b2 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualTo.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualTo.ts @@ -17,7 +17,7 @@ import { hashArray, type Hashable } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; import { hashObjectWithoutSourceInformation } from '../../../../../MetaModelUtils'; -import { TestAssertion } from './TestAssertion'; +import { TestAssertion, type TestAssertionVisitor } from './TestAssertion'; export class EqualTo extends TestAssertion implements Hashable { expected!: object; @@ -29,4 +29,8 @@ export class EqualTo extends TestAssertion implements Hashable { hashObjectWithoutSourceInformation(this.expected), ]); } + + accept_TestAssertionVisitor(visitor: TestAssertionVisitor): T { + return visitor.visit_EqualTo(this); + } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToJson.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToJson.ts index d76868fa11..dc999c6d4c 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToJson.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToJson.ts @@ -17,7 +17,7 @@ import { hashArray, type Hashable } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; import type { ExternalFormatData } from '../../data/EmbeddedData'; -import { TestAssertion } from './TestAssertion'; +import { TestAssertion, type TestAssertionVisitor } from './TestAssertion'; export class EqualToJson extends TestAssertion implements Hashable { expected!: ExternalFormatData; @@ -29,4 +29,8 @@ export class EqualToJson extends TestAssertion implements Hashable { this.expected, ]); } + + accept_TestAssertionVisitor(visitor: TestAssertionVisitor): T { + return visitor.visit_EqualToJSON(this); + } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/result/TestFailed.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToTDS.ts similarity index 50% rename from packages/legend-graph/src/models/metamodels/pure/test/result/TestFailed.ts rename to packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToTDS.ts index 4ef9f0d97a..d1dfc87f06 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/result/TestFailed.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/EqualToTDS.ts @@ -16,18 +16,36 @@ import { hashArray, type Hashable } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; -import type { AssertionStatus } from '../assertion/status/AssertionStatus'; -import { TestResult } from './TestResult'; +import type { + RelationalDataTableColumn, + RelationalDataTableRow, +} from '../../data/RelationalData'; +import { type TestAssertionVisitor, TestAssertion } from './TestAssertion'; -export class TestFailed extends TestResult implements Hashable { - assertStatuses: AssertionStatus[] = []; +export class RelationalTDS implements Hashable { + columns: RelationalDataTableColumn[] = []; + rows: RelationalDataTableRow[] = []; + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_TDS, + hashArray(this.columns), + hashArray(this.rows), + ]); + } +} - override get hashCode(): string { +export class EqualToTDS extends TestAssertion { + expected!: RelationalTDS; + + get hashCode(): string { return hashArray([ - CORE_HASH_STRUCTURE.TEST_FAILED, - this.testable, - this.atomicTestId, - hashArray(this.assertStatuses), + CORE_HASH_STRUCTURE.EQUAL_TO_TDS, + this.id, + this.expected, ]); } + + accept_TestAssertionVisitor(visitor: TestAssertionVisitor): T { + return visitor.visit_EqualToTDS(this); + } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/TestAssertion.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/TestAssertion.ts index 1bd8377b84..dced1fda7f 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/TestAssertion.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/TestAssertion.ts @@ -15,17 +15,26 @@ */ import type { Hashable } from '@finos/legend-shared'; - -export interface TestAssertionVisitor { - visit_TestAssertion(testAssertion: TestAssertion): T; -} +import type { AtomicTest } from '../Test'; +import type { EqualTo } from './EqualTo'; +import type { EqualToJson } from './EqualToJson'; +import type { EqualToTDS } from './EqualToTDS'; export abstract class TestAssertion implements Hashable { id!: string; + parentTest: AtomicTest | undefined; + abstract get hashCode(): string; - accept_TestAssertionVisitor(visitor: TestAssertionVisitor): T { - return visitor.visit_TestAssertion(this); - } + abstract accept_TestAssertionVisitor(visitor: TestAssertionVisitor): T; +} +export interface TestAssertionVisitor { + visit_TestAssertion(testAssertion: TestAssertion): T; + + visit_EqualTo(equal: EqualTo): T; + + visit_EqualToJSON(equal: EqualToJson): T; + + visit_EqualToTDS(equal: EqualToTDS): T; } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertFail.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertFail.ts index c8a4a621b8..cff8be6ca3 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertFail.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertFail.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; +import type { TestAssertion } from '../TestAssertion'; import { AssertionStatus } from './AssertionStatus'; -export class AssertFail extends AssertionStatus implements Hashable { - message!: string; +export class AssertFail extends AssertionStatus { + message: string | undefined; - get hashCode(): string { - return hashArray([CORE_HASH_STRUCTURE.ASSERT_FAIL, this.id, this.message]); + constructor(assertion: TestAssertion, message: string | undefined) { + super(assertion); + this.message = message; } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertPass.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertPass.ts index dc38f14a89..832eb3722d 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertPass.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertPass.ts @@ -14,12 +14,6 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; import { AssertionStatus } from './AssertionStatus'; -export class AssertPass extends AssertionStatus implements Hashable { - get hashCode(): string { - return hashArray([CORE_HASH_STRUCTURE.ASSERT_PASS, this.id]); - } -} +export class AssertPass extends AssertionStatus {} diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertionStatus.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertionStatus.ts index 99f1f734b9..12588b2304 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertionStatus.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/AssertionStatus.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import type { Hashable } from '@finos/legend-shared'; +import type { TestAssertion } from '../TestAssertion'; -export abstract class AssertionStatus implements Hashable { - id!: string; - - abstract get hashCode(): string; +export abstract class AssertionStatus { + assertion: TestAssertion; + constructor(assertion: TestAssertion) { + this.assertion = assertion; + } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail.ts b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail.ts index 3667138d00..03ace435ed 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/assertion/status/EqualToJsonAssertFail.ts @@ -14,21 +14,9 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; import { AssertFail } from './AssertFail'; -export class EqualToJsonAssertFail extends AssertFail implements Hashable { +export class EqualToJsonAssertFail extends AssertFail { expected!: string; actual!: string; - - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.EQUAL_TO_JSON_ASSERT_FAIL, - this.id, - this.message, - this.expected, - this.actual, - ]); - } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/result/AtomicTestId.ts b/packages/legend-graph/src/models/metamodels/pure/test/result/AtomicTestId.ts index 6522949643..3f6582efa6 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/result/AtomicTestId.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/result/AtomicTestId.ts @@ -13,19 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { AtomicTest, TestSuite } from '../Test'; -import { type Hashable, hashArray } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; +export class AtomicTestId { + parentSuite: TestSuite | undefined; + atomicTest: AtomicTest; -export class AtomicTestId implements Hashable { - testSuiteId!: string; - atomicTestId!: string; - - get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.ATOMIC_TEST_ID, - this.testSuiteId, - this.atomicTestId, - ]); + constructor(testSuite: TestSuite | undefined, atomicTestId: AtomicTest) { + this.parentSuite = testSuite; + this.atomicTest = atomicTestId; } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/result/TestPassed.ts b/packages/legend-graph/src/models/metamodels/pure/test/result/RunTestsTestableInput.ts similarity index 61% rename from packages/legend-graph/src/models/metamodels/pure/test/result/TestPassed.ts rename to packages/legend-graph/src/models/metamodels/pure/test/result/RunTestsTestableInput.ts index ef059c549c..8d5a887294 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/result/TestPassed.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/result/RunTestsTestableInput.ts @@ -14,16 +14,14 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; -import { TestResult } from './TestResult'; +import type { Testable } from '../Testable'; +import type { AtomicTestId } from './AtomicTestId'; -export class TestPassed extends TestResult implements Hashable { - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_PASSED, - this.testable, - this.atomicTestId, - ]); +export class RunTestsTestableInput { + testable: Testable; + unitTestIds: AtomicTestId[] = []; + + constructor(testable: Testable) { + this.testable = testable; } } diff --git a/packages/legend-graph/src/models/metamodels/pure/test/result/TestError.ts b/packages/legend-graph/src/models/metamodels/pure/test/result/TestError.ts deleted file mode 100644 index 50b782b3a3..0000000000 --- a/packages/legend-graph/src/models/metamodels/pure/test/result/TestError.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2020-present, Goldman Sachs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; -import { TestResult } from './TestResult'; - -export class TestError extends TestResult implements Hashable { - error!: string; - - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_ERROR, - this.testable, - this.atomicTestId, - this.error, - ]); - } -} diff --git a/packages/legend-graph/src/models/metamodels/pure/test/result/TestResult.ts b/packages/legend-graph/src/models/metamodels/pure/test/result/TestResult.ts index 2ad5b8b146..d3bcd90d96 100644 --- a/packages/legend-graph/src/models/metamodels/pure/test/result/TestResult.ts +++ b/packages/legend-graph/src/models/metamodels/pure/test/result/TestResult.ts @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../MetaModelConst'; +import type { AssertionStatus } from '../assertion/status/AssertionStatus'; +import type { Testable } from '../Testable'; import type { AtomicTestId } from './AtomicTestId'; -export class TestResult implements Hashable { - testable!: string; // should be `testable` +export class TestResult { + testable!: Testable; atomicTestId!: AtomicTestId; +} +export class TestError extends TestResult { + error!: string; +} + +export class TestPassed extends TestResult {} - get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_RESULT, - this.testable, - this.atomicTestId, - ]); - } +export class TestFailed extends TestResult { + assertStatuses: AssertionStatus[] = []; } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/V1_PureGraphManager.ts b/packages/legend-graph/src/models/protocols/pure/v1/V1_PureGraphManager.ts index 1a82060c91..61fe22c0e9 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/V1_PureGraphManager.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/V1_PureGraphManager.ts @@ -38,6 +38,7 @@ import { StopWatch, assertNonEmptyString, filterByType, + isNonNullable, } from '@finos/legend-shared'; import type { TEMPORARY__AbstractEngineConfig } from '../../../../graphManager/action/TEMPORARY__AbstractEngineConfig'; import { @@ -66,13 +67,12 @@ import type { DependencyManager } from '../../../../graph/DependencyManager'; import type { Class } from '../../../metamodels/pure/packageableElements/domain/Class'; import { RawLambda } from '../../../metamodels/pure/rawValueSpecification/RawLambda'; import type { RawValueSpecification } from '../../../metamodels/pure/rawValueSpecification/RawValueSpecification'; -import type { Service } from '../../../metamodels/pure/packageableElements/service/Service'; import type { FileGenerationSpecification } from '../../../metamodels/pure/packageableElements/fileGeneration/FileGenerationSpecification'; import type { GenerationConfigurationDescription, GenerationMode, } from '../../../../graphManager/action/generation/GenerationConfigurationDescription'; -import type { ServiceTestResult } from '../../../../graphManager/action/service/ServiceTestResult'; +import type { DEPRECATED__ServiceTestResult } from '../../../../graphManager/action/service/DEPRECATED__ServiceTestResult'; import type { ServiceRegistrationResult } from '../../../../graphManager/action/service/ServiceRegistrationResult'; import type { ExecutionResult } from '../../../../graphManager/action/execution/ExecutionResult'; import type { GenerationOutput } from '../../../../graphManager/action/generation/GenerationOutput'; @@ -201,7 +201,7 @@ import type { } from '../../../../graphManager/action/query/Query'; import { V1_buildQuery, - V1_buildServiceTestResult, + V1_buildLegacyServiceTestResult, V1_buildServiceRegistrationResult, V1_transformQuery, V1_buildGenerationOutput, @@ -239,6 +239,16 @@ import type { ModelGenerationConfiguration } from '../../../ModelGenerationConfi import type { MappingGeneration_PureProtocolProcessorPlugin_Extension } from '../MappingGeneration_PureProtocolProcessorPlugin_Extension'; import type { Package } from '../../../metamodels/pure/packageableElements/domain/Package'; import { V1_DataElement } from './model/packageableElements/data/V1_DataElement'; +import { + V1_RunTestsInput, + V1_RunTestsTestableInput, +} from './engine/test/V1_RunTestsInput'; +import { V1_AtomicTestId } from './model/test/V1_AtomicTestId'; +import type { RunTestsTestableInput } from '../../../metamodels/pure/test/result/RunTestsTestableInput'; +import { V1_buildTestsResult } from './engine/test/V1_RunTestsResult'; +import type { TestResult } from '../../../metamodels/pure/test/result/TestResult'; +import type { Service } from '../../../../DSLService_Exports'; +import { getNullableIdFromTestable } from '../../../metamodels/pure/test/Testable'; const V1_FUNCTION_SUFFIX_MULTIPLICITY_INFINITE = 'MANY'; @@ -1612,6 +1622,35 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { return this.pureModelContextDataToEntities(generatedModel); } + // ------------------------------------------- Test ------------------------------------------- + async runTests( + graph: PureModel, + testableInputs: RunTestsTestableInput[], + ): Promise { + const runTestsInput = new V1_RunTestsInput(); + runTestsInput.model = this.getFullGraphModelData(graph); + runTestsInput.testables = testableInputs + .map((input) => { + const testable = getNullableIdFromTestable(input.testable, graph); + if (!testable) { + return undefined; + } + const runTestableInput = new V1_RunTestsTestableInput(); + runTestableInput.testable = testable; + runTestableInput.unitTestIds = input.unitTestIds.map((unit) => { + const unitAtomicTest = new V1_AtomicTestId(); + unitAtomicTest.testSuiteId = unit.parentSuite?.id; + unitAtomicTest.atomicTestId = unit.atomicTest.id; + return unitAtomicTest; + }); + return runTestableInput; + }) + .filter(isNonNullable); + const runTestsResult = await this.engine.runTests(runTestsInput); + const result = V1_buildTestsResult(runTestsResult, graph); + return result; + } + // ------------------------------------------- ValueSpecification ------------------------------------------- buildValueSpecification( @@ -1951,10 +1990,10 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { // --------------------------------------------- Service --------------------------------------------- - async runServiceTests( + async runLegacyServiceTests( service: Service, graph: PureModel, - ): Promise { + ): Promise { const protocolGraph = this.getFullGraphModelData(graph); const targetService = guaranteeNonNullable( protocolGraph.elements @@ -1966,8 +2005,8 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { (element) => !(element instanceof V1_Service), ); protocolGraph.elements.push(targetService); - return (await this.engine.runServiceTests(protocolGraph)).map( - V1_buildServiceTestResult, + return (await this.engine.runLegacyServiceTests(protocolGraph)).map( + V1_buildLegacyServiceTestResult, ); } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_Engine.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_Engine.ts index b798c644fc..2252ba019a 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_Engine.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_Engine.ts @@ -51,7 +51,7 @@ import { V1_serializePureModelContext, V1_serializePureModelContextData, } from '../transformation/pureProtocol/V1_PureProtocolSerialization'; -import { V1_ServiceTestResult } from '../engine/service/V1_ServiceTestResult'; +import { V1_DEPRECATED__ServiceTestResult } from './service/V1_DEPRECATED__ServiceTestResult'; import { V1_serializeRawValueSpecification } from '../transformation/pureProtocol/serializationHelpers/V1_RawValueSpecificationSerializationHelper'; import { V1_transformRawLambda } from '../transformation/pureGraph/from/V1_RawValueSpecificationTransformer'; import { V1_GenerateFileInput } from '../engine/generation/V1_FileGenerationInput'; @@ -99,6 +99,8 @@ import type { ExternalFormatDescription } from '../../../../../graphManager/acti import { V1_ExternalFormatDescription } from './externalFormat/V1_ExternalFormatDescription'; import { V1_ExternalFormatModelGenerationInput } from './externalFormat/V1_ExternalFormatModelGeneration'; import { GRAPH_MANAGER_EVENT } from '../../../../../graphManager/GraphManagerEvent'; +import { V1_RunTestsInput } from './test/V1_RunTestsInput'; +import { V1_RunTestsResult } from './test/V1_RunTestsResult'; class V1_EngineConfig extends TEMPORARY__AbstractEngineConfig { private engine: V1_Engine; @@ -467,6 +469,15 @@ export class V1_Engine { ); } + // --------------------------------------------- Test --------------------------------------------- + + async runTests(input: V1_RunTestsInput): Promise { + const result = (await this.engineServerClient.runTests( + V1_RunTestsInput.serialization.toJson(input), + )) as unknown as PlainObject; + return V1_RunTestsResult.serialization.fromJson(result); + } + // ------------------------------------------- File Generation ------------------------------------------- async getAvailableGenerationConfigurationDescriptions(): Promise< @@ -585,14 +596,14 @@ export class V1_Engine { return (await this.engineServerClient.getServerServiceInfo()) as unknown as V1_ServiceConfigurationInfo; } - async runServiceTests( + async runLegacyServiceTests( model: V1_PureModelContextData, - ): Promise { + ): Promise { return ( await this.engineServerClient.runServiceTests( V1_serializePureModelContextData(model), ) - ).map((v) => V1_ServiceTestResult.serialization.fromJson(v)); + ).map((v) => V1_DEPRECATED__ServiceTestResult.serialization.fromJson(v)); } async registerService( diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineHelper.ts index 15f49f7aed..a72ca51041 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineHelper.ts @@ -28,8 +28,8 @@ import { import { type V1_LightQuery, V1_Query } from './query/V1_Query'; import type { PureModel } from '../../../../../graph/PureModel'; import { PackageableElementExplicitReference } from '../../../../metamodels/pure/packageableElements/PackageableElementReference'; -import { ServiceTestResult } from '../../../../../graphManager/action/service/ServiceTestResult'; -import type { V1_ServiceTestResult } from './service/V1_ServiceTestResult'; +import { DEPRECATED__ServiceTestResult } from '../../../../../graphManager/action/service/DEPRECATED__ServiceTestResult'; +import type { V1_DEPRECATED__ServiceTestResult } from './service/V1_DEPRECATED__ServiceTestResult'; import type { V1_ServiceRegistrationResult } from './service/V1_ServiceRegistrationResult'; import { ServiceRegistrationResult } from '../../../../../graphManager/action/service/ServiceRegistrationResult'; import { @@ -247,10 +247,10 @@ export const V1_transformQuerySearchSpecification = ( return protocol; }; -export const V1_buildServiceTestResult = ( - protocol: V1_ServiceTestResult, -): ServiceTestResult => { - const metamodel = new ServiceTestResult(); +export const V1_buildLegacyServiceTestResult = ( + protocol: V1_DEPRECATED__ServiceTestResult, +): DEPRECATED__ServiceTestResult => { + const metamodel = new DEPRECATED__ServiceTestResult(); metamodel.name = guaranteeNonNullable( protocol.name, `Service test result 'name' field is missing`, diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineServerClient.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineServerClient.ts index 9351bdb09c..bbf0762876 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineServerClient.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/V1_EngineServerClient.ts @@ -25,7 +25,7 @@ import { import type { ImportMode } from '../../../../../graphManager/action/generation/ImportConfigurationDescription'; import type { V1_PureModelContextData } from '../model/context/V1_PureModelContextData'; import type { V1_LambdaReturnTypeResult } from './compilation/V1_LambdaReturnTypeResult'; -import type { V1_ServiceTestResult } from './service/V1_ServiceTestResult'; +import type { V1_DEPRECATED__ServiceTestResult } from './service/V1_DEPRECATED__ServiceTestResult'; import type { V1_ServiceRegistrationResult } from './service/V1_ServiceRegistrationResult'; import type { V1_ServiceConfigurationInfo } from './service/V1_ServiceConfiguration'; import type { V1_CompileResult } from './compilation/V1_CompileResult'; @@ -50,6 +50,8 @@ import type { V1_QuerySearchSpecification } from './query/V1_QuerySearchSpecific import type { EXECUTION_SERIALIZATION_FORMAT } from '../../../../../graphManager/action/execution/ExecutionResult'; import type { V1_ExternalFormatDescription } from './externalFormat/V1_ExternalFormatDescription'; import type { V1_ExternalFormatModelGenerationInput } from './externalFormat/V1_ExternalFormatModelGeneration'; +import type { V1_RunTestsInput } from './test/V1_RunTestsInput'; +import type { V1_TestResult } from '../model/test/result/V1_TestResult'; enum CORE_ENGINE_TRACER_SPAN { GRAMMAR_TO_JSON = 'transform Pure code to protocol', @@ -71,6 +73,8 @@ enum CORE_ENGINE_TRACER_SPAN { RUN_SERVICE_TESTS = 'run service tests', GENERATE_TEST_DATA_WITH_DEFAULT_SEED = 'generate test data with default seed', + RUN_TESTS = 'run testable tests', + CREATE_QUERY = 'create query', UPDATE_QUERY = 'update query', DELETE_QUERY = 'delete query', @@ -187,6 +191,20 @@ export class V1_EngineServerClient extends AbstractServerClient { { enableCompression: true }, ); + // ------------------------------------------- Test --------------------------------------- + runTests = ( + input: PlainObject, + ): Promise> => + this.postWithTracing( + this.getTraceData(CORE_ENGINE_TRACER_SPAN.RUN_TESTS), + `${this._pure()}/testable/runTests`, + input, + {}, + undefined, + undefined, + { enableCompression: true }, + ); + // ------------------------------------------- External Format --------------------------------------- _externalFormats = (): string => `${this._pure()}/external/format`; @@ -415,7 +433,7 @@ export class V1_EngineServerClient extends AbstractServerClient { ); runServiceTests = ( model: PlainObject, - ): Promise[]> => + ): Promise[]> => this.postWithTracing( this.getTraceData(CORE_ENGINE_TRACER_SPAN.RUN_SERVICE_TESTS), `${this._service()}/doTest`, diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_ServiceTestResult.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_DEPRECATED__ServiceTestResult.ts similarity index 89% rename from packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_ServiceTestResult.ts rename to packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_DEPRECATED__ServiceTestResult.ts index 6831bdca48..07ebf9b411 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_ServiceTestResult.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/service/V1_DEPRECATED__ServiceTestResult.ts @@ -17,12 +17,12 @@ import { createModelSchema, primitive } from 'serializr'; import { SerializationFactory } from '@finos/legend-shared'; -export class V1_ServiceTestResult { +export class V1_DEPRECATED__ServiceTestResult { name!: string; result!: boolean; static readonly serialization = new SerializationFactory( - createModelSchema(V1_ServiceTestResult, { + createModelSchema(V1_DEPRECATED__ServiceTestResult, { name: primitive(), result: primitive(), }), diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsInput.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsInput.ts new file mode 100644 index 0000000000..c1f765316a --- /dev/null +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsInput.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SerializationFactory, usingModelSchema } from '@finos/legend-shared'; +import { createModelSchema, object, primitive, list } from 'serializr'; +import { V1_PureModelContextData } from '../../model/context/V1_PureModelContextData'; +import type { V1_AtomicTestId } from '../../model/test/V1_AtomicTestId'; +import { V1_atomicTestIdModelSchema } from '../../transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper'; + +export class V1_RunTestsTestableInput { + testable!: string; + unitTestIds: V1_AtomicTestId[] = []; + + static readonly serialization = new SerializationFactory( + createModelSchema(V1_RunTestsTestableInput, { + testable: primitive(), + unitTestIds: list(usingModelSchema(V1_atomicTestIdModelSchema)), + }), + ); +} + +export class V1_RunTestsInput { + model!: V1_PureModelContextData; + testables: V1_RunTestsTestableInput[] = []; + + static readonly serialization = new SerializationFactory( + createModelSchema(V1_RunTestsInput, { + model: object(V1_PureModelContextData), + testables: usingModelSchema( + V1_RunTestsTestableInput.serialization.schema, + ), + }), + ); +} diff --git a/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsResult.ts b/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsResult.ts new file mode 100644 index 0000000000..281e26e1f4 --- /dev/null +++ b/packages/legend-graph/src/models/protocols/pure/v1/engine/test/V1_RunTestsResult.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isNonNullable, SerializationFactory } from '@finos/legend-shared'; +import { createModelSchema, custom, list, SKIP } from 'serializr'; +import type { PureModel } from '../../../../../../graph/PureModel'; +import type { TestResult } from '../../../../../metamodels/pure/test/result/TestResult'; +import type { V1_TestResult } from '../../model/test/result/V1_TestResult'; +import { V1_buildTestResult } from '../../transformation/pureGraph/to/helpers/V1_TestResultBuilderHelper'; +import { V1_deserializeTestResult } from '../../transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper'; + +export class V1_RunTestsResult { + results: V1_TestResult[] = []; + static readonly serialization = new SerializationFactory( + createModelSchema(V1_RunTestsResult, { + results: list(custom((value) => SKIP, V1_deserializeTestResult)), + }), + ); +} + +export const V1_buildTestsResult = ( + results: V1_RunTestsResult, + graph: PureModel, +): TestResult[] => + results.results + .map((r) => V1_buildTestResult(r, graph)) + .filter(isNonNullable); diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_EmbeddedData.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_EmbeddedData.ts index 903bdf6439..3e56e030cb 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_EmbeddedData.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_EmbeddedData.ts @@ -17,12 +17,14 @@ import { type Hashable, hashArray } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; import { hashObjectWithoutSourceInformation } from '../../../../../../MetaModelUtils'; +import type { V1_RelationalData } from './V1_RelationalData'; export interface V1_EmbeddedDataVisitor { visit_EmbeddedData(embeddedData: V1_EmbeddedData): T; visit_ExternalFormatData(externalFormatData: V1_ExternalFormatData): T; visit_ModelStoreData(modelStoreData: V1_ModelStoreData): T; visit_DataElementReference(dataElementReference: V1_DataElementReference): T; + visit_RelationalData(relationalData: V1_RelationalData): T; } export abstract class V1_EmbeddedData implements Hashable { diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_RelationalData.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_RelationalData.ts new file mode 100644 index 0000000000..ebfea2af60 --- /dev/null +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/data/V1_RelationalData.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type Hashable, hashArray } from '@finos/legend-shared'; +import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; +import { + V1_EmbeddedData, + type V1_EmbeddedDataVisitor, +} from './V1_EmbeddedData'; + +export class V1_RelationalDataTableColumn implements Hashable { + value!: string; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE_COLUMN, + this.value, + ]); + } +} + +export class V1_RelationalDataTableRow implements Hashable { + values!: string; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE_ROW, + this.values, + ]); + } +} + +export class V1_RelationalDataTable implements Hashable { + schemaName: string | undefined; + tableName!: string; + columns: V1_RelationalDataTableColumn[] = []; + rows: V1_RelationalDataTableRow[] = []; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE, + this.schemaName ?? '', + this.tableName, + hashArray(this.columns), + hashArray(this.rows), + ]); + } +} + +export class V1_RelationalData extends V1_EmbeddedData implements Hashable { + tables: V1_RelationalDataTable[] = []; + accept_EmbeddedDataVisitor(visitor: V1_EmbeddedDataVisitor): T { + return visitor.visit_RelationalData(this); + } + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_DATA_TABLE, + hashArray(this.tables), + ]); + } +} diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/V1_AtomicTestId.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/V1_AtomicTestId.ts index 2df4b9e514..7282dc8f37 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/V1_AtomicTestId.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/V1_AtomicTestId.ts @@ -14,18 +14,7 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../MetaModelConst'; - -export class V1_AtomicTestId implements Hashable { - testSuiteId!: string; +export class V1_AtomicTestId { + testSuiteId: string | undefined; atomicTestId!: string; - - get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.ATOMIC_TEST_ID, - this.testSuiteId, - this.atomicTestId, - ]); - } } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualTo.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualTo.ts index ef44c0bfd0..1be4c044bb 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualTo.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualTo.ts @@ -17,7 +17,10 @@ import { hashArray, type Hashable } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; import { hashObjectWithoutSourceInformation } from '../../../../../../../MetaModelUtils'; -import { V1_TestAssertion } from './V1_TestAssertion'; +import { + V1_TestAssertion, + type V1_TestAssertionVisitor, +} from './V1_TestAssertion'; export class V1_EqualTo extends V1_TestAssertion implements Hashable { expected!: object; @@ -29,4 +32,7 @@ export class V1_EqualTo extends V1_TestAssertion implements Hashable { hashObjectWithoutSourceInformation(this.expected), ]); } + accept_TestAssertionVisitor(visitor: V1_TestAssertionVisitor): T { + return visitor.visit_EqualTo(this); + } } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToJson.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToJson.ts index 51a35a7dfd..976aeb6de7 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToJson.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToJson.ts @@ -17,7 +17,10 @@ import { hashArray, type Hashable } from '@finos/legend-shared'; import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; import type { V1_ExternalFormatData } from '../../data/V1_EmbeddedData'; -import { V1_TestAssertion } from './V1_TestAssertion'; +import { + V1_TestAssertion, + type V1_TestAssertionVisitor, +} from './V1_TestAssertion'; export class V1_EqualToJson extends V1_TestAssertion implements Hashable { expected!: V1_ExternalFormatData; @@ -29,4 +32,7 @@ export class V1_EqualToJson extends V1_TestAssertion implements Hashable { this.expected, ]); } + accept_TestAssertionVisitor(visitor: V1_TestAssertionVisitor): T { + return visitor.visit_EqualToJSON(this); + } } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToTDS.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToTDS.ts new file mode 100644 index 0000000000..577f5794f9 --- /dev/null +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_EqualToTDS.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hashArray, type Hashable } from '@finos/legend-shared'; +import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; +import type { + V1_RelationalDataTableColumn, + V1_RelationalDataTableRow, +} from '../../data/V1_RelationalData'; +import { + type V1_TestAssertionVisitor, + V1_TestAssertion, +} from './V1_TestAssertion'; + +export class V1_RelationalTDS implements Hashable { + columns: V1_RelationalDataTableColumn[] = []; + rows: V1_RelationalDataTableRow[] = []; + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATIONAL_TDS, + hashArray(this.columns), + hashArray(this.rows), + ]); + } +} + +export class V1_EqualToTDS extends V1_TestAssertion { + expected!: V1_RelationalTDS; + + get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.EQUAL_TO_TDS, + this.id, + this.expected, + ]); + } + + accept_TestAssertionVisitor(visitor: V1_TestAssertionVisitor): T { + return visitor.visit_EqualToTDS(this); + } +} diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_TestAssertion.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_TestAssertion.ts index 32934ba90c..203ea402a5 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_TestAssertion.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/assertion/V1_TestAssertion.ts @@ -15,9 +15,15 @@ */ import type { Hashable } from '@finos/legend-shared'; +import type { V1_EqualTo } from './V1_EqualTo'; +import type { V1_EqualToJson } from './V1_EqualToJson'; +import type { V1_EqualToTDS } from './V1_EqualToTDS'; export interface V1_TestAssertionVisitor { visit_TestAssertion(testAssertion: V1_TestAssertion): T; + visit_EqualToTDS(testAssertion: V1_EqualToTDS): T; + visit_EqualTo(testAssertion: V1_EqualTo): T; + visit_EqualToJSON(testAssertion: V1_EqualToJson): T; } export abstract class V1_TestAssertion implements Hashable { @@ -25,7 +31,7 @@ export abstract class V1_TestAssertion implements Hashable { abstract get hashCode(): string; - accept_TestAssertionVisitor(visitor: V1_TestAssertionVisitor): T { - return visitor.visit_TestAssertion(this); - } + abstract accept_TestAssertionVisitor( + visitor: V1_TestAssertionVisitor, + ): T; } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestError.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestError.ts deleted file mode 100644 index faf75c5c85..0000000000 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestError.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2020-present, Goldman Sachs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; -import { V1_TestResult } from './V1_TestResult'; - -export class V1_TestError extends V1_TestResult implements Hashable { - error!: string; - - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_ERROR, - this.testable, - this.atomicTestId, - this.error, - ]); - } -} diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestFailed.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestFailed.ts deleted file mode 100644 index 005f03b1d7..0000000000 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestFailed.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2020-present, Goldman Sachs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; -import type { V1_AssertionStatus } from '../assertion/status/V1_AssertionStatus'; -import { V1_TestResult } from './V1_TestResult'; - -export class V1_TestFailed extends V1_TestResult implements Hashable { - assertStatuses: V1_AssertionStatus[] = []; - - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_FAILED, - this.testable, - this.atomicTestId, - hashArray(this.assertStatuses), - ]); - } -} diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestResult.ts b/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestResult.ts index dcde5a7b7a..83331f2d33 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestResult.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestResult.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; +import type { V1_AssertionStatus } from '../assertion/status/V1_AssertionStatus'; import type { V1_AtomicTestId } from '../V1_AtomicTestId'; -export class V1_TestResult implements Hashable { +export abstract class V1_TestResult { testable!: string; atomicTestId!: V1_AtomicTestId; +} + +export class V1_TestError extends V1_TestResult { + error!: string; +} - get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.ASSERT_FAIL, - this.testable, - this.atomicTestId, - ]); - } +export class V1_TestPassed extends V1_TestResult {} +export class V1_TestFailed extends V1_TestResult { + assertStatuses: V1_AssertionStatus[] = []; } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_DataElementTransformer.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_DataElementTransformer.ts index cdf0785257..fcdcd26f93 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_DataElementTransformer.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_DataElementTransformer.ts @@ -21,6 +21,10 @@ import { ExternalFormatData, ModelStoreData, } from '../../../../../../metamodels/pure/data/EmbeddedData'; +import { + type RelationalDataTable, + RelationalData, +} from '../../../../../../metamodels/pure/data/RelationalData'; import type { DataElement } from '../../../../../../metamodels/pure/packageableElements/data/DataElement'; import type { DSLData_PureProtocolProcessorPlugin_Extension } from '../../../../DSLData_PureProtocolProcessorPlugin_Extension'; import { @@ -29,6 +33,12 @@ import { V1_ExternalFormatData, V1_ModelStoreData, } from '../../../model/data/V1_EmbeddedData'; +import { + V1_RelationalData, + V1_RelationalDataTable, + V1_RelationalDataTableColumn, + V1_RelationalDataTableRow, +} from '../../../model/data/V1_RelationalData'; import { V1_DataElement } from '../../../model/packageableElements/data/V1_DataElement'; import { V1_initPackageableElement } from './V1_CoreTransformerHelper'; import { @@ -39,7 +49,7 @@ import type { V1_GraphTransformerContext } from './V1_GraphTransformerContext'; // ----------------------------------------------- DATA ---------------------------------------- -export const V1_transformModelStoreData = ( +const V1_transformModelStoreData = ( element: ModelStoreData, ): V1_ModelStoreData => { const modelStoreDataElement = new V1_ModelStoreData(); @@ -60,7 +70,7 @@ export const V1_transformExternalFormatData = ( return externalFormatDataElement; }; -export const V1_transformDataElementReference = ( +const V1_transformDataElementReference = ( element: DataElementReference, ): V1_DataElementReference => { const dataElementReference = new V1_DataElementReference(); @@ -68,6 +78,32 @@ export const V1_transformDataElementReference = ( element.dataElement.valueForSerialization ?? ''; return dataElementReference; }; +const V1_transformRelationalDataTable = ( + element: RelationalDataTable, +): V1_RelationalDataTable => { + const table = new V1_RelationalDataTable(); + table.tableName = element.tableName; + table.schemaName = element.schemaName; + table.rows = element.rows.map((e) => { + const row = new V1_RelationalDataTableRow(); + row.values = e.values; + return row; + }); + table.columns = element.columns.map((e) => { + const col = new V1_RelationalDataTableColumn(); + col.value = e.value; + return col; + }); + return table; +}; + +const V1_transformRelationalData = ( + element: RelationalData, +): V1_RelationalData => { + const data = new V1_RelationalData(); + data.tables = element.tables.map(V1_transformRelationalDataTable); + return data; +}; export const V1_transformEmbeddedData = ( metamodel: EmbeddedData, @@ -79,6 +115,8 @@ export const V1_transformEmbeddedData = ( return V1_transformExternalFormatData(metamodel); } else if (metamodel instanceof DataElementReference) { return V1_transformDataElementReference(metamodel); + } else if (metamodel instanceof RelationalData) { + return V1_transformRelationalData(metamodel); } const extraEmbeddedDataTransformers = context.plugins.flatMap( (plugin) => diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_TestTransformer.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_TestTransformer.ts index 51e62f83c5..01101772a2 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_TestTransformer.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/from/V1_TestTransformer.ts @@ -15,15 +15,6 @@ */ import { UnsupportedOperationError } from '@finos/legend-shared'; -import { AssertFail } from '../../../../../../metamodels/pure/test/assertion/status/AssertFail'; -import type { AssertionStatus } from '../../../../../../metamodels/pure/test/assertion/status/AssertionStatus'; -import { AssertPass } from '../../../../../../metamodels/pure/test/assertion/status/AssertPass'; -import { EqualToJsonAssertFail } from '../../../../../../metamodels/pure/test/assertion/status/EqualToJsonAssertFail'; -import { V1_AssertFail } from '../../../model/test/assertion/status/V1_AssertFail'; -import type { V1_AssertionStatus } from '../../../model/test/assertion/status/V1_AssertionStatus'; -import { V1_AssertPass } from '../../../model/test/assertion/status/V1_AssertPass'; -import { V1_EqualToJsonAssertFail } from '../../../model/test/assertion/status/V1_EqualToJsonAssertFail'; -import { V1_AtomicTestId } from '../../../model/test/V1_AtomicTestId'; import { V1_EqualTo } from '../../../model/test/assertion/V1_EqualTo'; import { V1_EqualToJson } from '../../../model/test/assertion/V1_EqualToJson'; import { EqualTo } from '../../../../../../metamodels/pure/test/assertion/EqualTo'; @@ -34,72 +25,21 @@ import { ServiceTest } from '../../../../../../metamodels/pure/packageableElemen import type { V1_TestAssertion } from '../../../model/test/assertion/V1_TestAssertion'; import type { TestAssertion } from '../../../../../../metamodels/pure/test/assertion/TestAssertion'; import type { V1_TestSuite } from '../../../model/test/V1_TestSuite'; -import { V1_TestResult } from '../../../model/test/result/V1_TestResult'; -import { V1_TestPassed } from '../../../model/test/result/V1_TestPassed'; -import { V1_TestFailed } from '../../../model/test/result/V1_TestFailed'; -import { V1_TestError } from '../../../model/test/result/V1_TestError'; -import type { TestResult } from '../../../../../../metamodels/pure/test/result/TestResult'; -import type { TestPassed } from '../../../../../../metamodels/pure/test/result/TestPassed'; -import type { TestFailed } from '../../../../../../metamodels/pure/test/result/TestFailed'; -import type { TestError } from '../../../../../../metamodels/pure/test/result/TestError'; import { V1_transformServiceTest, V1_transformServiceTestSuite, } from './V1_ServiceTransformer'; import { ServiceTestSuite } from '../../../../../../metamodels/pure/packageableElements/service/ServiceTestSuite'; import type { V1_GraphTransformerContext } from './V1_GraphTransformerContext'; -import type { AtomicTestId } from '../../../../../../metamodels/pure/test/result/AtomicTestId'; import type { AtomicTest, TestSuite, } from '../../../../../../metamodels/pure/test/Test'; - -const transformAtomicTestId = (element: AtomicTestId): V1_AtomicTestId => { - const atomicTestId = new V1_AtomicTestId(); - atomicTestId.atomicTestId = element.atomicTestId; - atomicTestId.testSuiteId = element.testSuiteId; - return atomicTestId; -}; - -const transformAssertFail = (element: AssertFail): V1_AssertFail => { - const assertFail = new V1_AssertFail(); - assertFail.id = element.id; - assertFail.message = element.message; - return assertFail; -}; - -const transformAssertPass = (element: AssertPass): V1_AssertPass => { - const assertPass = new V1_AssertPass(); - assertPass.id = element.id; - return assertPass; -}; - -const transformEqualToJsonAssertFail = ( - element: EqualToJsonAssertFail, -): V1_EqualToJsonAssertFail => { - const equalToJsonAssertFail = new V1_EqualToJsonAssertFail(); - equalToJsonAssertFail.id = element.id; - equalToJsonAssertFail.message = element.message; - equalToJsonAssertFail.actual = element.actual; - equalToJsonAssertFail.expected = element.expected; - return equalToJsonAssertFail; -}; - -const transformAssertionStatus = ( - value: AssertionStatus, -): V1_AssertionStatus => { - if (value instanceof EqualToJsonAssertFail) { - return transformEqualToJsonAssertFail(value); - } else if (value instanceof AssertFail) { - return transformAssertFail(value); - } else if (value instanceof AssertPass) { - return transformAssertPass(value); - } - throw new UnsupportedOperationError( - `Can't transform assertion status`, - value, - ); -}; +import { EqualToTDS } from '../../../../../../metamodels/pure/test/assertion/EqualToTDS'; +import { + V1_EqualToTDS, + V1_RelationalTDS, +} from '../../../model/test/assertion/V1_EqualToTDS'; const transformEqualTo = (element: EqualTo): V1_EqualTo => { const equalTo = new V1_EqualTo(); @@ -115,36 +55,13 @@ const transformEqualToJson = (element: EqualToJson): V1_EqualToJson => { return equalToJson; }; -export const V1_transformTestError = (element: TestError): V1_TestError => { - const testError = new V1_TestError(); - testError.testable = element.testable; - testError.error = element.error; - testError.atomicTestId = transformAtomicTestId(element.atomicTestId); - return testError; -}; - -export const V1_transformTestFailed = (element: TestFailed): V1_TestFailed => { - const testFailed = new V1_TestFailed(); - testFailed.testable = element.testable; - testFailed.atomicTestId = transformAtomicTestId(element.atomicTestId); - testFailed.assertStatuses = element.assertStatuses.map((assertStatus) => - transformAssertionStatus(assertStatus), - ); - return testFailed; -}; - -export const V1_transformTestPassed = (element: TestPassed): V1_TestPassed => { - const testPassed = new V1_TestPassed(); - testPassed.testable = element.testable; - testPassed.atomicTestId = transformAtomicTestId(element.atomicTestId); - return testPassed; -}; - -export const V1_transformTestResult = (element: TestResult): V1_TestResult => { - const testResult = new V1_TestResult(); - testResult.testable = element.testable; - testResult.atomicTestId = transformAtomicTestId(element.atomicTestId); - return testResult; +const transformEqualToTDS = (element: EqualToTDS): V1_EqualToTDS => { + const equalToTDS = new V1_EqualToTDS(); + equalToTDS.id = element.id; + equalToTDS.expected = new V1_RelationalTDS(); + equalToTDS.expected.columns = element.expected.columns; + equalToTDS.expected.rows = element.expected.rows; + return equalToTDS; }; export const V1_transformAtomicTest = (value: AtomicTest): V1_AtomicTest => { @@ -161,6 +78,8 @@ export const V1_transformTestAssertion = ( return transformEqualTo(value); } else if (value instanceof EqualToJson) { return transformEqualToJson(value); + } else if (value instanceof EqualToTDS) { + return transformEqualToTDS(value); } throw new UnsupportedOperationError(`Can't transform test assertion`, value); }; diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_DataElementBuilderHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_DataElementBuilderHelper.ts index 3445f9eb54..c18bec4958 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_DataElementBuilderHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_DataElementBuilderHelper.ts @@ -21,6 +21,12 @@ import { ExternalFormatData, ModelStoreData, } from '../../../../../../../metamodels/pure/data/EmbeddedData'; +import { + RelationalData, + RelationalDataTable, + RelationalDataTableColumn, + RelationalDataTableRow, +} from '../../../../../../../metamodels/pure/data/RelationalData'; import type { Class } from '../../../../../../../metamodels/pure/packageableElements/domain/Class'; import type { DSLData_PureProtocolProcessorPlugin_Extension } from '../../../../../DSLData_PureProtocolProcessorPlugin_Extension'; import type { @@ -30,6 +36,7 @@ import type { V1_ExternalFormatData, V1_ModelStoreData, } from '../../../../model/data/V1_EmbeddedData'; +import type { V1_RelationalData } from '../../../../model/data/V1_RelationalData'; import type { V1_GraphBuilderContext } from '../V1_GraphBuilderContext'; @@ -90,4 +97,25 @@ export class V1_ProtocolToMetaModelEmbeddedDataBuilder ); return metamodel; } + + visit_RelationalData(relationalData: V1_RelationalData): EmbeddedData { + const metamodel = new RelationalData(); + metamodel.tables = relationalData.tables.map((t) => { + const table = new RelationalDataTable(); + table.schemaName = t.schemaName; + table.columns = t.columns.map((c) => { + const col = new RelationalDataTableColumn(); + col.value = c.value; + return col; + }); + table.tableName = t.tableName; + table.rows = t.rows.map((r) => { + const row = new RelationalDataTableRow(); + row.values = r.values; + return row; + }); + return table; + }); + return metamodel; + } } diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_ServiceBuilderHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_ServiceBuilderHelper.ts index 9e6089d4fa..68231d57af 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_ServiceBuilderHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_ServiceBuilderHelper.ts @@ -81,6 +81,7 @@ import { V1_DEPRECATED__SingleExecutionTest, V1_DEPRECATED__MultiExecutionTest, } from '../../../../model/packageableElements/service/V1_DEPRECATED__ServiceTest'; +import type { TestSuite } from '../../../../../../../metamodels/pure/test/Test'; const buildConnectionTestData = ( element: V1_ConnectionTestData, @@ -115,15 +116,17 @@ const buildTestData = ( export const V1_buildServiceTest = ( element: V1_ServiceTest, + parentSuite: TestSuite | undefined, context: V1_GraphBuilderContext, ): ServiceTest => { const serviceTest = new ServiceTest(); serviceTest.id = element.id; + serviceTest.parentSuite = parentSuite; serviceTest.parameters = element.parameters.map((parameter) => buildParameterValue(parameter), ); serviceTest.assertions = element.assertions.map((assertion) => - V1_buildTestAssertion(assertion, context), + V1_buildTestAssertion(assertion, serviceTest, context), ); return serviceTest; }; @@ -136,7 +139,7 @@ export const V1_buildServiceTestSuite = ( serviceTestSuite.id = element.id; serviceTestSuite.testData = buildTestData(element.testData, context); serviceTestSuite.tests = element.tests.map((test) => - V1_buildAtomicTest(test, context), + V1_buildAtomicTest(test, serviceTestSuite, context), ); return serviceTestSuite; }; diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestBuilderHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestBuilderHelper.ts index a6c785831e..00776e27a4 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestBuilderHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestBuilderHelper.ts @@ -18,35 +18,22 @@ import { guaranteeType, UnsupportedOperationError } from '@finos/legend-shared'; import { ExternalFormatData } from '../../../../../../../metamodels/pure/data/EmbeddedData'; import { EqualTo } from '../../../../../../../metamodels/pure/test/assertion/EqualTo'; import { EqualToJson } from '../../../../../../../metamodels/pure/test/assertion/EqualToJson'; -import { AssertFail } from '../../../../../../../metamodels/pure/test/assertion/status/AssertFail'; -import type { AssertionStatus } from '../../../../../../../metamodels/pure/test/assertion/status/AssertionStatus'; -import { AssertPass } from '../../../../../../../metamodels/pure/test/assertion/status/AssertPass'; -import { EqualToJsonAssertFail } from '../../../../../../../metamodels/pure/test/assertion/status/EqualToJsonAssertFail'; +import { + EqualToTDS, + RelationalTDS, +} from '../../../../../../../metamodels/pure/test/assertion/EqualToTDS'; import type { TestAssertion } from '../../../../../../../metamodels/pure/test/assertion/TestAssertion'; -import { AtomicTestId } from '../../../../../../../metamodels/pure/test/result/AtomicTestId'; -import { TestError } from '../../../../../../../metamodels/pure/test/result/TestError'; -import { TestFailed } from '../../../../../../../metamodels/pure/test/result/TestFailed'; -import { TestPassed } from '../../../../../../../metamodels/pure/test/result/TestPassed'; -import { TestResult } from '../../../../../../../metamodels/pure/test/result/TestResult'; import type { AtomicTest, TestSuite, } from '../../../../../../../metamodels/pure/test/Test'; import { V1_ServiceTest } from '../../../../model/packageableElements/service/V1_ServiceTest'; import { V1_ServiceTestSuite } from '../../../../model/packageableElements/service/V1_ServiceTestSuite'; -import { V1_AssertFail } from '../../../../model/test/assertion/status/V1_AssertFail'; -import type { V1_AssertionStatus } from '../../../../model/test/assertion/status/V1_AssertionStatus'; -import { V1_AssertPass } from '../../../../model/test/assertion/status/V1_AssertPass'; -import { V1_EqualToJsonAssertFail } from '../../../../model/test/assertion/status/V1_EqualToJsonAssertFail'; import { V1_EqualTo } from '../../../../model/test/assertion/V1_EqualTo'; import { V1_EqualToJson } from '../../../../model/test/assertion/V1_EqualToJson'; +import { V1_EqualToTDS } from '../../../../model/test/assertion/V1_EqualToTDS'; import type { V1_TestAssertion } from '../../../../model/test/assertion/V1_TestAssertion'; -import type { V1_TestError } from '../../../../model/test/result/V1_TestError'; -import type { V1_TestFailed } from '../../../../model/test/result/V1_TestFailed'; -import type { V1_TestPassed } from '../../../../model/test/result/V1_TestPassed'; -import type { V1_TestResult } from '../../../../model/test/result/V1_TestResult'; import type { V1_AtomicTest } from '../../../../model/test/V1_AtomicTest'; -import type { V1_AtomicTestId } from '../../../../model/test/V1_AtomicTestId'; import type { V1_TestSuite } from '../../../../model/test/V1_TestSuite'; import type { V1_GraphBuilderContext } from '../V1_GraphBuilderContext'; import { V1_ProtocolToMetaModelEmbeddedDataBuilder } from './V1_DataElementBuilderHelper'; @@ -55,61 +42,25 @@ import { V1_buildServiceTestSuite, } from './V1_ServiceBuilderHelper'; -const buildAtomicTestId = (element: V1_AtomicTestId): AtomicTestId => { - const atomicTestId = new AtomicTestId(); - atomicTestId.atomicTestId = element.atomicTestId; - atomicTestId.testSuiteId = element.testSuiteId; - return atomicTestId; -}; - -const buildAssertFail = (element: V1_AssertFail): AssertFail => { - const assertFail = new AssertFail(); - assertFail.id = element.id; - assertFail.message = element.message; - return assertFail; -}; - -const buildAssertPass = (element: V1_AssertPass): AssertPass => { - const assertPass = new AssertPass(); - assertPass.id = element.id; - return assertPass; -}; - -const buildEqualToJsonAssertFail = ( - element: V1_EqualToJsonAssertFail, -): EqualToJsonAssertFail => { - const equalToJsonAssertFail = new EqualToJsonAssertFail(); - equalToJsonAssertFail.id = element.id; - equalToJsonAssertFail.message = element.message; - equalToJsonAssertFail.actual = element.actual; - equalToJsonAssertFail.expected = element.expected; - return equalToJsonAssertFail; -}; - -const buildAssertionStatus = (value: V1_AssertionStatus): AssertionStatus => { - if (value instanceof V1_EqualToJsonAssertFail) { - return buildEqualToJsonAssertFail(value); - } else if (value instanceof V1_AssertFail) { - return buildAssertFail(value); - } else if (value instanceof V1_AssertPass) { - return buildAssertPass(value); - } - throw new UnsupportedOperationError(`Can't build assertion status`, value); -}; - -const buildEqualTo = (element: V1_EqualTo): EqualTo => { +const buildEqualTo = ( + element: V1_EqualTo, + parentTest: AtomicTest | undefined, +): EqualTo => { const equalTo = new EqualTo(); equalTo.id = element.id; + equalTo.parentTest = parentTest; equalTo.expected = element.expected; return equalTo; }; const buildEqualToJson = ( element: V1_EqualToJson, + parentTest: AtomicTest | undefined, context: V1_GraphBuilderContext, ): EqualToJson => { const equalToJson = new EqualToJson(); equalToJson.id = element.id; + equalToJson.parentTest = parentTest; equalToJson.expected = guaranteeType( element.expected.accept_EmbeddedDataVisitor( new V1_ProtocolToMetaModelEmbeddedDataBuilder(context), @@ -119,56 +70,43 @@ const buildEqualToJson = ( return equalToJson; }; -export const V1_buildTestError = (element: V1_TestError): TestError => { - const testError = new TestError(); - testError.testable = element.testable; - testError.error = element.error; - testError.atomicTestId = buildAtomicTestId(element.atomicTestId); - return testError; -}; - -export const V1_buildTestFailed = (element: V1_TestFailed): TestFailed => { - const testFailed = new TestFailed(); - testFailed.testable = element.testable; - testFailed.atomicTestId = buildAtomicTestId(element.atomicTestId); - testFailed.assertStatuses = element.assertStatuses.map((assertStatus) => - buildAssertionStatus(assertStatus), - ); - return testFailed; -}; - -export const V1_buildTestPassed = (element: V1_TestPassed): TestPassed => { - const testPassed = new TestPassed(); - testPassed.testable = element.testable; - testPassed.atomicTestId = buildAtomicTestId(element.atomicTestId); - return testPassed; -}; - -export const V1_buildTestResult = (element: V1_TestResult): TestResult => { - const testResult = new TestResult(); - testResult.testable = element.testable; - testResult.atomicTestId = buildAtomicTestId(element.atomicTestId); - return testResult; +const buildEqualToTDS = ( + element: V1_EqualToTDS, + parentTest: AtomicTest | undefined, + context: V1_GraphBuilderContext, +): EqualToTDS => { + const equalToTDS = new EqualToTDS(); + equalToTDS.id = element.id; + equalToTDS.parentTest = parentTest; + const expected = new RelationalTDS(); + expected.columns = element.expected.columns; + expected.rows = element.expected.rows; + equalToTDS.expected = expected; + return equalToTDS; }; export const V1_buildAtomicTest = ( value: V1_AtomicTest, + parentSuite: TestSuite | undefined, context: V1_GraphBuilderContext, ): AtomicTest => { if (value instanceof V1_ServiceTest) { - return V1_buildServiceTest(value, context); + return V1_buildServiceTest(value, parentSuite, context); } throw new UnsupportedOperationError(`Can't build atomic test`, value); }; export const V1_buildTestAssertion = ( value: V1_TestAssertion, + parentTest: AtomicTest | undefined, context: V1_GraphBuilderContext, ): TestAssertion => { if (value instanceof V1_EqualTo) { - return buildEqualTo(value); + return buildEqualTo(value, parentTest); } else if (value instanceof V1_EqualToJson) { - return buildEqualToJson(value, context); + return buildEqualToJson(value, parentTest, context); + } else if (value instanceof V1_EqualToTDS) { + return buildEqualToTDS(value, parentTest, context); } throw new UnsupportedOperationError(`Can't build test assertion`, value); }; diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestResultBuilderHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestResultBuilderHelper.ts new file mode 100644 index 0000000000..3d7db80893 --- /dev/null +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureGraph/to/helpers/V1_TestResultBuilderHelper.ts @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + filterByType, + guaranteeNonNullable, + guaranteeType, + UnsupportedOperationError, +} from '@finos/legend-shared'; +import type { PureModel } from '../../../../../../../../graph/PureModel'; +import { AssertFail } from '../../../../../../../metamodels/pure/test/assertion/status/AssertFail'; +import type { AssertionStatus } from '../../../../../../../metamodels/pure/test/assertion/status/AssertionStatus'; +import { AssertPass } from '../../../../../../../metamodels/pure/test/assertion/status/AssertPass'; +import { EqualToJsonAssertFail } from '../../../../../../../metamodels/pure/test/assertion/status/EqualToJsonAssertFail'; +import { AtomicTestId } from '../../../../../../../metamodels/pure/test/result/AtomicTestId'; +import { + TestError, + TestFailed, + TestPassed, + type TestResult, +} from '../../../../../../../metamodels/pure/test/result/TestResult'; +import { + AtomicTest, + TestSuite, +} from '../../../../../../../metamodels/pure/test/Test'; +import { + type Testable, + getNullableTestable, +} from '../../../../../../../metamodels/pure/test/Testable'; +import { V1_AssertFail } from '../../../../model/test/assertion/status/V1_AssertFail'; +import type { V1_AssertionStatus } from '../../../../model/test/assertion/status/V1_AssertionStatus'; +import { V1_AssertPass } from '../../../../model/test/assertion/status/V1_AssertPass'; +import { V1_EqualToJsonAssertFail } from '../../../../model/test/assertion/status/V1_EqualToJsonAssertFail'; +import { + type V1_TestResult, + V1_TestFailed, + V1_TestPassed, + V1_TestError, +} from '../../../../model/test/result/V1_TestResult'; +import type { V1_AtomicTestId } from '../../../../model/test/V1_AtomicTestId'; + +const buildAtomicTestId = ( + element: V1_AtomicTestId, + testable: Testable, +): AtomicTestId => { + const testSuite = testable.tests + .filter(filterByType(TestSuite)) + .find((t) => t.id === element.testSuiteId); + let atomicTest: AtomicTest; + if (testSuite) { + atomicTest = guaranteeNonNullable( + testSuite.tests.find((aT) => aT.id === element.atomicTestId), + ); + } else { + atomicTest = guaranteeType( + testable.tests.find((e) => e.id === element.atomicTestId), + AtomicTest, + ); + } + return new AtomicTestId(testSuite, atomicTest); +}; + +const buildAssertFail = ( + element: V1_AssertFail, + atomicTest: AtomicTest, +): AssertFail => { + const assertion = guaranteeNonNullable( + atomicTest.assertions.find((a) => a.id === element.id), + ); + return new AssertFail(assertion, element.message); +}; +const buildAssertPass = ( + element: V1_AssertPass, + atomicTest: AtomicTest, +): AssertPass => { + const assertion = guaranteeNonNullable( + atomicTest.assertions.find((a) => a.id === element.id), + ); + return new AssertPass(assertion); +}; + +const buildEqualToJsonAssertFail = ( + element: V1_EqualToJsonAssertFail, + atomicTest: AtomicTest, +): EqualToJsonAssertFail => { + const assertion = guaranteeNonNullable( + atomicTest.assertions.find((a) => a.id === element.id), + ); + const equalToJsonAssertFail = new EqualToJsonAssertFail( + assertion, + element.message, + ); + equalToJsonAssertFail.actual = element.actual; + equalToJsonAssertFail.expected = element.expected; + return equalToJsonAssertFail; +}; + +const buildAssertionStatus = ( + value: V1_AssertionStatus, + atomicTest: AtomicTest, +): AssertionStatus => { + if (value instanceof V1_EqualToJsonAssertFail) { + return buildEqualToJsonAssertFail(value, atomicTest); + } else if (value instanceof V1_AssertFail) { + return buildAssertFail(value, atomicTest); + } else if (value instanceof V1_AssertPass) { + return buildAssertPass(value, atomicTest); + } + throw new UnsupportedOperationError(`Can't build assertion status`, value); +}; +export const V1_buildTestError = ( + element: V1_TestError, + testable: Testable, +): TestError => { + const testError = new TestError(); + testError.testable = testable; + testError.atomicTestId = buildAtomicTestId(element.atomicTestId, testable); + testError.error = element.error; + return testError; +}; +export const V1_buildTestFailed = ( + element: V1_TestFailed, + testable: Testable, +): TestFailed => { + const testFailed = new TestFailed(); + testFailed.atomicTestId = buildAtomicTestId(element.atomicTestId, testable); + testFailed.testable = testable; + testFailed.assertStatuses = element.assertStatuses.map((e) => + buildAssertionStatus(e, testFailed.atomicTestId.atomicTest), + ); + return testFailed; +}; + +export const V1_buildTestPassed = ( + element: V1_TestPassed, + testable: Testable, +): TestPassed => { + const testPassed = new TestPassed(); + testPassed.testable = testable; + testPassed.atomicTestId = buildAtomicTestId(element.atomicTestId, testable); + return testPassed; +}; + +export const V1_buildTestResult = ( + element: V1_TestResult, + graph: PureModel, +): TestResult | undefined => { + const testable = getNullableTestable(element.testable, graph); + if (!testable) { + return undefined; + } + if (element instanceof V1_TestPassed) { + return V1_buildTestPassed(element, testable); + } else if (element instanceof V1_TestFailed) { + return V1_buildTestFailed(element, testable); + } else if (element instanceof V1_TestError) { + return V1_buildTestError(element, testable); + } + throw new UnsupportedOperationError(`Can't build test result`, element); +}; diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_DataElementSerializationHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_DataElementSerializationHelper.ts index 1beee40a68..fd1cb8cadb 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_DataElementSerializationHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_DataElementSerializationHelper.ts @@ -22,6 +22,7 @@ import { usingConstantValueSchema, serializeMap, deserializeMap, + usingModelSchema, } from '@finos/legend-shared'; import { createModelSchema, @@ -30,6 +31,8 @@ import { type ModelSchema, primitive, serialize, + optional, + list, } from 'serializr'; import type { DSLData_PureProtocolProcessorPlugin_Extension } from '../../../../DSLData_PureProtocolProcessorPlugin_Extension'; import type { PureProtocolProcessorPlugin } from '../../../../PureProtocolProcessorPlugin'; @@ -39,6 +42,12 @@ import { V1_ModelStoreData, V1_DataElementReference, } from '../../../model/data/V1_EmbeddedData'; +import { + V1_RelationalData, + V1_RelationalDataTable, + V1_RelationalDataTableColumn, + V1_RelationalDataTableRow, +} from '../../../model/data/V1_RelationalData'; import { V1_DataElement } from '../../../model/packageableElements/data/V1_DataElement'; import { V1_stereotypePtrSchema, @@ -51,6 +60,7 @@ export enum V1_EmbeddedDataType { MODEL_STORE_DATA = 'modelStore', EXTERNAL_FORMAT_DATA = 'externalFormat', DATA_ELEMENT_REFERENCE = 'reference', + RELATIONAL_DATA = 'relationalData', } export const V1_modelStoreDataModelSchema = createModelSchema( @@ -81,6 +91,35 @@ export const V1_dataElementReferenceModelSchema = createModelSchema( }, ); +export const V1_relationalDataTableColumnSchema = createModelSchema( + V1_RelationalDataTableColumn, + { + value: primitive(), + }, +); + +export const V1_relationalDataTableRowModelSchema = createModelSchema( + V1_RelationalDataTableRow, + { + values: primitive(), + }, +); + +const V1_relationalDataTableModelSchema = createModelSchema( + V1_RelationalDataTable, + { + schemaName: optional(primitive()), + tableName: primitive(), + columns: list(usingModelSchema(V1_relationalDataTableColumnSchema)), + rows: list(usingModelSchema(V1_relationalDataTableRowModelSchema)), + }, +); + +const V1_relationalDataModelSchema = createModelSchema(V1_RelationalData, { + _type: usingConstantValueSchema(V1_EmbeddedDataType.RELATIONAL_DATA), + tables: list(usingModelSchema(V1_relationalDataTableModelSchema)), +}); + export const V1_serializeEmbeddedDataType = ( protocol: V1_EmbeddedData, plugins: PureProtocolProcessorPlugin[], @@ -91,6 +130,8 @@ export const V1_serializeEmbeddedDataType = ( return serialize(V1_modelStoreDataModelSchema, protocol); } else if (protocol instanceof V1_DataElementReference) { return serialize(V1_dataElementReferenceModelSchema, protocol); + } else if (protocol instanceof V1_RelationalData) { + return serialize(V1_relationalDataModelSchema, protocol); } const extraEmbeddedDataSerializers = plugins.flatMap( (plugin) => @@ -122,6 +163,8 @@ export const V1_deserializeEmbeddedDataType = ( return deserialize(V1_modelStoreDataModelSchema, json); case V1_EmbeddedDataType.DATA_ELEMENT_REFERENCE: return deserialize(V1_dataElementReferenceModelSchema, json); + case V1_EmbeddedDataType.RELATIONAL_DATA: + return deserialize(V1_relationalDataModelSchema, json); default: { const extraEmbeddedDataProtocolDeserializers = plugins.flatMap( (plugin) => diff --git a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper.ts b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper.ts index 65a357464e..1fe805ce0b 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper.ts +++ b/packages/legend-graph/src/models/protocols/pure/v1/transformation/pureProtocol/serializationHelpers/V1_TestSerializationHelper.ts @@ -37,15 +37,25 @@ import { V1_AssertPass } from '../../../model/test/assertion/status/V1_AssertPas import { V1_EqualToJsonAssertFail } from '../../../model/test/assertion/status/V1_EqualToJsonAssertFail'; import { V1_EqualTo } from '../../../model/test/assertion/V1_EqualTo'; import { V1_EqualToJson } from '../../../model/test/assertion/V1_EqualToJson'; +import { + V1_EqualToTDS, + V1_RelationalTDS, +} from '../../../model/test/assertion/V1_EqualToTDS'; import type { V1_TestAssertion } from '../../../model/test/assertion/V1_TestAssertion'; -import { V1_TestError } from '../../../model/test/result/V1_TestError'; -import { V1_TestFailed } from '../../../model/test/result/V1_TestFailed'; -import { V1_TestPassed } from '../../../model/test/result/V1_TestPassed'; -import { V1_TestResult } from '../../../model/test/result/V1_TestResult'; +import { + type V1_TestResult, + V1_TestError, + V1_TestFailed, + V1_TestPassed, +} from '../../../model/test/result/V1_TestResult'; import type { V1_AtomicTest } from '../../../model/test/V1_AtomicTest'; import { V1_AtomicTestId } from '../../../model/test/V1_AtomicTestId'; import type { V1_TestSuite } from '../../../model/test/V1_TestSuite'; -import { V1_externalFormatDataModelSchema } from './V1_DataElementSerializationHelper'; +import { + V1_externalFormatDataModelSchema, + V1_relationalDataTableColumnSchema, + V1_relationalDataTableRowModelSchema, +} from './V1_DataElementSerializationHelper'; import { V1_serviceTestModelSchema, V1_serviceTestSuiteModelSchema, @@ -64,6 +74,13 @@ export enum V1_AtomicTestType { enum V1_TestAssertionType { EQUAL_TO = 'equalTo', EQUAL_TO_JSON = 'equalToJson', + EQUAL_TO_TDS = 'equalToTDS', +} + +enum V1_TestResultType { + TEST_ERROR = 'testError', + TEST_PASSED = 'testPassed', + TEST_FAILED = 'testFailed', } export enum V1_TestSuiteType { @@ -72,7 +89,7 @@ export enum V1_TestSuiteType { export const V1_atomicTestIdModelSchema = createModelSchema(V1_AtomicTestId, { atomicTestId: primitive(), - key: primitive(), + testSuiteId: primitive(), }); export const V1_assertFailModelSchema = createModelSchema(V1_AssertFail, { @@ -144,6 +161,17 @@ export const V1_equalToJsonModelSchema = createModelSchema(V1_EqualToJson, { id: primitive(), }); +const V1_relationalTDSModelSchema = createModelSchema(V1_RelationalTDS, { + rows: list(usingModelSchema(V1_relationalDataTableRowModelSchema)), + columns: list(usingModelSchema(V1_relationalDataTableColumnSchema)), +}); + +const V1_equalToTDSModelSchema = createModelSchema(V1_EqualToTDS, { + _type: usingConstantValueSchema(V1_TestAssertionType.EQUAL_TO_TDS), + expected: usingModelSchema(V1_relationalTDSModelSchema), + id: primitive(), +}); + export const V1_testErrorModelSchema = createModelSchema(V1_TestError, { atomicTestId: usingModelSchema(V1_atomicTestIdModelSchema), error: primitive(), @@ -165,12 +193,22 @@ export const V1_testPassedModelSchema = createModelSchema(V1_TestPassed, { atomicTestId: usingModelSchema(V1_atomicTestIdModelSchema), testable: primitive(), }); - -export const V1_testResultModelSchema = createModelSchema(V1_TestResult, { - atomicTestId: usingModelSchema(V1_atomicTestIdModelSchema), - testable: primitive(), -}); - +export const V1_deserializeTestResult = ( + json: PlainObject, +): V1_TestResult => { + switch (json._type) { + case V1_TestResultType.TEST_ERROR: + return deserialize(V1_testErrorModelSchema, json); + case V1_TestResultType.TEST_FAILED: + return deserialize(V1_testFailedModelSchema, json); + case V1_TestResultType.TEST_PASSED: + return deserialize(V1_testPassedModelSchema, json); + default: + throw new UnsupportedOperationError( + `Can't deserialize atomic test of type '${json._type}'`, + ); + } +}; export const V1_serializeAtomicTest = ( protocol: V1_AtomicTest, ): PlainObject => { @@ -200,6 +238,8 @@ export const V1_serializeTestAssertion = ( return serialize(V1_equalToModelSchema, protocol); } else if (protocol instanceof V1_EqualToJson) { return serialize(V1_equalToJsonModelSchema, protocol); + } else if (protocol instanceof V1_EqualToTDS) { + return serialize(V1_equalToTDSModelSchema, protocol); } throw new UnsupportedOperationError( `Can't serialize test assertion`, @@ -215,6 +255,8 @@ export const V1_deserializeTestAssertion = ( return deserialize(V1_equalToModelSchema, json); case V1_TestAssertionType.EQUAL_TO_JSON: return deserialize(V1_equalToJsonModelSchema, json); + case V1_TestAssertionType.EQUAL_TO_TDS: + return deserialize(V1_equalToTDSModelSchema, json); default: throw new UnsupportedOperationError( `Can't deserialize test assertion of type '${json._type}'`, diff --git a/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/RoundtripGrammar.test.ts b/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/RoundtripGrammar.test.ts index 9136fffad9..787e4697e8 100644 --- a/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/RoundtripGrammar.test.ts +++ b/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/RoundtripGrammar.test.ts @@ -85,6 +85,9 @@ const EXCLUSIONS: { [key: string]: ROUNTRIP_TEST_PHASES[] | typeof SKIP } = { 'mapping-include-enum-mapping.pure': [ ROUNTRIP_TEST_PHASES.PROTOCOL_ROUNDTRIP, ], + + // TODO: Unskip once https://github.com/finos/legend-engine/pull/658 is resolved + 'relational-dataElement.pure': SKIP, }; type GrammarRoundtripOptions = { diff --git a/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/cases/relational-dataElement.pure b/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/cases/relational-dataElement.pure new file mode 100644 index 0000000000..1319ea62c9 --- /dev/null +++ b/packages/legend-manual-tests/src/__tests__/roundtrip-grammar/cases/relational-dataElement.pure @@ -0,0 +1,37 @@ +###Data +Data my::RelationalData +{ + Relational + #{ + MyTable: + { + schema: 'mySchema'; + columns: + [ + 'id', + 'firstName', + 'lastName', + 'age' + ]; + rows: + [ + '1,John,Doe', + '2,Nicole,Smith', + '3,Nick,Smith' + ]; + } + MyTable2: + { + columns: + [ + 'id', + 'name' + ]; + rows: + [ + '1,John', + '2,Jack' + ]; + } + }# +} diff --git a/packages/legend-studio/src/components/TestableStudio.tsx b/packages/legend-studio/src/components/TestableStudio.tsx new file mode 100644 index 0000000000..98736753ff --- /dev/null +++ b/packages/legend-studio/src/components/TestableStudio.tsx @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + getNullableIdFromTestable, + PackageableElement, + type Testable, +} from '@finos/legend-graph'; +import { isNonNullable, uuid } from '@finos/legend-shared'; +import type { EditorStore } from '../stores/EditorStore'; +import type { TestableMetadataGetter } from '../stores/LegendStudioPlugin'; +import { getElementTypeIcon } from './shared/ElementIconUtils'; + +export interface TestableMetadata { + testable: Testable; + id: string; + name: string; + icon: React.ReactNode; +} + +export const getTestableMetadata = ( + testable: Testable, + editorStore: EditorStore, + extraTestableMetadataGetters: TestableMetadataGetter[], +): TestableMetadata => { + if (testable instanceof PackageableElement) { + return { + testable: testable, + id: + getNullableIdFromTestable( + testable, + editorStore.graphManagerState.graph, + ) ?? uuid(), + name: testable.name, + icon: getElementTypeIcon( + editorStore, + editorStore.graphState.getPackageableElementType(testable), + ), + }; + } + const extraTestables = extraTestableMetadataGetters + .map((getter) => getter(testable, editorStore)) + .filter(isNonNullable); + return ( + extraTestables[0] ?? { + testable, + id: uuid(), + name: '(unknown)', + icon: null, + } + ); +}; diff --git a/packages/legend-studio/src/components/editor/ActivityBar.tsx b/packages/legend-studio/src/components/editor/ActivityBar.tsx index 318d17d85a..596a52e48f 100644 --- a/packages/legend-studio/src/components/editor/ActivityBar.tsx +++ b/packages/legend-studio/src/components/editor/ActivityBar.tsx @@ -34,6 +34,7 @@ import { CodeBranchIcon, EmptyClockIcon, WrenchIcon, + FlaskIcon, } from '@finos/legend-art'; import { useEditorStore } from './EditorStoreProvider'; import { forwardRef } from 'react'; @@ -245,6 +246,11 @@ export const ActivityBar = observer(() => { title: 'Workflow Manager', icon: , }, + !editorStore.isInConflictResolutionMode && { + mode: ACTIVITY_MODE.TEST, + title: 'Test Manager', + icon: , + }, ].filter((activity): activity is ActivityDisplay => Boolean(activity)); return ( diff --git a/packages/legend-studio/src/components/editor/side-bar/SideBar.tsx b/packages/legend-studio/src/components/editor/side-bar/SideBar.tsx index caa6e650f3..fc42999c84 100644 --- a/packages/legend-studio/src/components/editor/side-bar/SideBar.tsx +++ b/packages/legend-studio/src/components/editor/side-bar/SideBar.tsx @@ -24,6 +24,7 @@ import { WorkspaceUpdateConflictResolver } from './WorkspaceUpdateConflictResolv import { ProjectOverview } from './ProjectOverview'; import { WorkflowManager } from './WorkflowManager'; import { useEditorStore } from '../EditorStoreProvider'; +import { TestableManager } from './testable/TestableManager'; /** * Wrapper component around different implementations of sidebar, such as to view domain, to manage SDLC, etc. @@ -51,6 +52,12 @@ export const SideBar = observer(() => { workflowManagerState={editorStore.workspaceWorkflowManagerState} /> ); + case ACTIVITY_MODE.TEST: + return ( + + ); default: return null; } diff --git a/packages/legend-studio/src/components/editor/side-bar/testable/TestableManager.tsx b/packages/legend-studio/src/components/editor/side-bar/testable/TestableManager.tsx new file mode 100644 index 0000000000..1f149f401a --- /dev/null +++ b/packages/legend-studio/src/components/editor/side-bar/testable/TestableManager.tsx @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EDITOR_LANGUAGE } from '@finos/legend-application'; +import { + type TreeData, + type TreeNodeContainerProps, + clsx, + PanelLoadingIndicator, + PlayIcon, + TreeView, + ChevronDownIcon, + ChevronRightIcon, + RefreshIcon, + TimesCircleIcon, + CheckCircleIcon, + CircleIcon, + ContextMenu, + MenuContent, + MenuContentItem, + Dialog, + WarningIcon, +} from '@finos/legend-art'; +import { + AssertFail, + EqualToJsonAssertFail, + TestError, +} from '@finos/legend-graph'; +import { type GeneratorFn, isNonNullable } from '@finos/legend-shared'; +import { observer } from 'mobx-react-lite'; +import { forwardRef, useEffect } from 'react'; +import { + type TestableExplorerTreeNodeData, + type TestableManagerState, + type TestableState, + TESTABLE_RESULT, + AtomicTestTreeNodeData, + AssertionTestTreeNodeData, + TestableTreeNodeData, + TestSuiteTreeNodeData, + getNodeTestableResult, + getAtomicTest_TestResult, + getAssertionStatus, +} from '../../../../stores/sidebar-state/testable/TestableManagerState'; +import { LEGEND_STUDIO_TEST_ID } from '../../../LegendStudioTestID'; +import { TextDiffView } from '../../../shared/DiffView'; +import { StudioTextInputEditor } from '../../../shared/StudioTextInputEditor'; +import { useEditorStore } from '../../EditorStoreProvider'; + +const getTestableResultIcon = ( + testableResult: TESTABLE_RESULT, +): React.ReactNode => { + switch (testableResult) { + case TESTABLE_RESULT.PASSED: + return ( +
+ +
+ ); + case TESTABLE_RESULT.FAILED: + return ( +
+ +
+ ); + case TESTABLE_RESULT.ERROR: + return ( +
+ +
+ ); + default: + return ( +
+ +
+ ); + } +}; +const getOptionalError = ( + node: TestableExplorerTreeNodeData, + testableState: TestableState, +): TestError | AssertFail | undefined => { + if (node instanceof AtomicTestTreeNodeData) { + const result = getAtomicTest_TestResult( + node.atomicTest, + testableState.results, + ); + if (result instanceof TestError) { + return result; + } + } else if (node instanceof AssertionTestTreeNodeData) { + const status = getAssertionStatus(node.assertion, testableState.results); + if (status instanceof AssertFail) { + return status; + } + } + return undefined; +}; + +const TestFailViewer = observer( + (props: { + testableManager: TestableManagerState; + failure: TestError | AssertFail; + }) => { + const { testableManager, failure } = props; + const id = + failure instanceof TestError + ? `${failure.atomicTestId.atomicTest.parentSuite ?? ''}.${ + failure.atomicTestId.atomicTest.id + }` + : `${failure.assertion.parentTest?.parentSuite?.id ?? ''}.${ + failure.assertion.parentTest?.id ?? '' + }.${failure.assertion.id}`; + const closeLogViewer = (): void => + testableManager.setFailureViewing(undefined); + + return ( + +
+ +
+
{id}
+
+
+ {failure instanceof TestError && ( + + )} + {failure instanceof EqualToJsonAssertFail && ( + + )} + {failure instanceof AssertFail && + !(failure instanceof EqualToJsonAssertFail) && ( + + )} +
+
+ +
+
+
+ ); + }, +); + +const TestableExplorerContextMenu = observer( + forwardRef< + HTMLDivElement, + { + testableManagerState: TestableManagerState; + testableState: TestableState; + node: TestableExplorerTreeNodeData; + treeData: TreeData; + error?: TestError | AssertFail | undefined; + } + >(function TestableExplorerContextMenu(props, ref) { + const { node, error, testableManagerState, testableState } = props; + const runTest = (): void => { + testableState.run(node); + }; + const viewError = (): void => testableManagerState.setFailureViewing(error); + return ( + + + Run + + {error && ( + + {error instanceof TestError + ? 'View Error' + : 'Viewer assertion fail'} + + )} + + ); + }), +); + +const TestableTreeNodeContainer: React.FC< + TreeNodeContainerProps< + TestableExplorerTreeNodeData, + { + testableManagerState: TestableManagerState; + testableState: TestableState; + treeData: TreeData; + } + > +> = (props) => { + const { node, level, stepPaddingInRem, onNodeSelect } = props; + const { treeData, testableState, testableManagerState } = props.innerProps; + const results = testableState.results; + const expandIcon = + node instanceof AssertionTestTreeNodeData ? ( +
+ ) : node.isOpen ? ( + + ) : ( + + ); + const nodeIcon = + node instanceof TestableTreeNodeData ? node.testableMetadata.icon : null; + const resultIcon = getTestableResultIcon( + getNodeTestableResult(node, results), + ); + const optionalError = getOptionalError(node, testableState); + // ); + const selectNode: React.MouseEventHandler = (event) => onNodeSelect?.(node); + + return ( + + } + menuProps={{ elevation: 7 }} + > +
+
+
+ {expandIcon} +
+
+ {resultIcon} +
+ {nodeIcon && ( +
+ {nodeIcon} +
+ )} +
+ {node instanceof TestableTreeNodeData && ( +
+ + {node.testableMetadata.name} + +
+ )} + {node instanceof TestSuiteTreeNodeData && ( +
+ + {node.label} + +
+ )} + {node instanceof AtomicTestTreeNodeData && ( +
+ + {node.label} + +
+ )} + {node instanceof AssertionTestTreeNodeData && ( +
+ + {node.label} + +
+ )} +
+
+ ); +}; + +// TODO: +// Current Work in Progress +// 1. Add UI feedback when single test is running +// 2. Handle Multi Execution Test Results +// 3. Find Icon for `TestError` (different than Test Fail) +// 4. Add `Visit Test` to open test editors when Testable Editors are complete +export const TestableManager = observer( + (props: { testableManagerState: TestableManagerState }) => { + const editorStore = useEditorStore(); + const testableManagerState = props.testableManagerState; + const isDispatchingAction = testableManagerState.isDispatchingAction; + const renderTestables = (): React.ReactNode => ( + <> + {(testableManagerState._testableStates ?? []).map((testableState) => { + const onNodeSelect = (node: TestableExplorerTreeNodeData): void => { + testableState.onTreeNodeSelect(node, testableState.treeData); + }; + const getChildNodes = ( + node: TestableExplorerTreeNodeData, + ): TestableExplorerTreeNodeData[] => { + if (node.childrenIds) { + return node.childrenIds + .map((id) => testableState.treeData.nodes.get(id)) + .filter(isNonNullable); + } + return []; + }; + return ( + + ); + })} + + ); + + useEffect(() => { + editorStore.testableManagerState.init(); + }, [editorStore.testableManagerState]); + + const runAllTests = (): GeneratorFn => + testableManagerState.runAllTests(undefined); + + const reset = (): void => testableManagerState.init(true); + return ( +
+
+
+
+ TESTABLE MANAGER +
+
+
+ + +
+
+
+ +
+
+
+
+ TESTABLES (WIP) +
+
+
+
+
{renderTestables()}
+ {testableManagerState.failureViewing && ( + + )} +
+
+
+ ); + }, +); diff --git a/packages/legend-studio/src/stores/EditorConfig.ts b/packages/legend-studio/src/stores/EditorConfig.ts index 23fc663687..691f25f960 100644 --- a/packages/legend-studio/src/stores/EditorConfig.ts +++ b/packages/legend-studio/src/stores/EditorConfig.ts @@ -61,6 +61,7 @@ export enum ACTIVITY_MODE { REVIEW = 'REVIEW', PROJECT_OVERVIEW = 'PROJECT_OVERVIEW', WORKFLOW_MANAGER = 'WORKFLOW_MANAGER', + TEST = 'TEST', } export enum AUX_PANEL_MODE { diff --git a/packages/legend-studio/src/stores/EditorGraphState.ts b/packages/legend-studio/src/stores/EditorGraphState.ts index b0d8f6752f..ca24fadc03 100644 --- a/packages/legend-studio/src/stores/EditorGraphState.ts +++ b/packages/legend-studio/src/stores/EditorGraphState.ts @@ -99,6 +99,7 @@ import { import { CONFIGURATION_EDITOR_TAB } from './editor-state/ProjectConfigurationEditorState'; import type { DSLMapping_LegendStudioPlugin_Extension } from './DSLMapping_LegendStudioPlugin_Extension'; import { graph_dispose } from './graphModifier/GraphModifierHelper'; +import { TestableManagerState } from './sidebar-state/testable/TestableManagerState'; export enum GraphBuilderStatus { SUCCEEDED = 'SUCCEEDED', @@ -830,6 +831,12 @@ export class EditorGraphState { }, ); + // Activity States + this.editorStore.testableManagerState = new TestableManagerState( + this.editorStore, + this.editorStore.sdlcState, + ); + // NOTE: build model generation entities every-time we rebuild the graph - should we do this? yield this.editorStore.graphManagerState.graphManager.buildGenerations( newGraph, diff --git a/packages/legend-studio/src/stores/EditorStore.ts b/packages/legend-studio/src/stores/EditorStore.ts index ca333a7364..8a54ee6615 100644 --- a/packages/legend-studio/src/stores/EditorStore.ts +++ b/packages/legend-studio/src/stores/EditorStore.ts @@ -144,6 +144,7 @@ import { graph_deleteOwnElement, graph_renameElement, } from './graphModifier/GraphModifierHelper'; +import { TestableManagerState } from './sidebar-state/testable/TestableManagerState'; export abstract class EditorExtensionState { /** @@ -190,6 +191,7 @@ export class EditorStore { projectConfigurationEditorState: ProjectConfigurationEditorState; projectOverviewState: ProjectOverviewState; workspaceWorkflowManagerState: WorkspaceWorkflowManagerState; + testableManagerState: TestableManagerState; workspaceUpdaterState: WorkspaceUpdaterState; workspaceReviewState: WorkspaceReviewState; localChangesState: LocalChangesState; @@ -295,6 +297,7 @@ export class EditorStore { // side bar panels this.explorerTreeState = new ExplorerTreeState(this); this.projectOverviewState = new ProjectOverviewState(this, this.sdlcState); + this.testableManagerState = new TestableManagerState(this, this.sdlcState); this.workspaceWorkflowManagerState = new WorkspaceWorkflowManagerState( this, this.sdlcState, @@ -710,7 +713,6 @@ export class EditorStore { ), ]); yield flowResult(this.initMode()); - onLeave(true); } diff --git a/packages/legend-studio/src/stores/LegendStudioPlugin.ts b/packages/legend-studio/src/stores/LegendStudioPlugin.ts index 8e2a0a30b1..b68c44f942 100644 --- a/packages/legend-studio/src/stores/LegendStudioPlugin.ts +++ b/packages/legend-studio/src/stores/LegendStudioPlugin.ts @@ -22,11 +22,13 @@ import type { Class, PackageableElement, ModelGenerationConfiguration, + Testable, } from '@finos/legend-graph'; import { type LegendApplicationDocumentationEntry, LegendApplicationPlugin, } from '@finos/legend-application'; +import type { TestableMetadata } from '../components/TestableStudio'; export type ApplicationSetup = ( pluginManager: LegendStudioPluginManager, @@ -66,6 +68,11 @@ export type ModelLoaderExtensionConfiguration = { renderer: (editorStore: EditorStore) => React.ReactNode | undefined; }; +export type TestableMetadataGetter = ( + testable: Testable, + editorStore: EditorStore, +) => TestableMetadata | undefined; + export abstract class LegendStudioPlugin extends LegendApplicationPlugin { /** * This helps to better type-checking for this empty abtract type @@ -116,6 +123,11 @@ export abstract class LegendStudioPlugin extends LegendApplicationPlugin { * Get the list of extension configurations for model loader. */ getExtraModelLoaderExtensionConfigurations?(): ModelLoaderExtensionConfiguration[]; + + /** + * Get the list of extension for testables + */ + getExtraTestableMetadata?(): TestableMetadataGetter[]; } export type ElementTypeGetter = ( diff --git a/packages/legend-studio/src/stores/editor-state/element-editor-state/service/LegacyServiceTestState.ts b/packages/legend-studio/src/stores/editor-state/element-editor-state/service/LegacyServiceTestState.ts index 5ad3e3d421..80db5c9c30 100644 --- a/packages/legend-studio/src/stores/editor-state/element-editor-state/service/LegacyServiceTestState.ts +++ b/packages/legend-studio/src/stores/editor-state/element-editor-state/service/LegacyServiceTestState.ts @@ -36,7 +36,7 @@ import { } from '@finos/legend-shared'; import type { EditorStore } from '../../../EditorStore'; import { - type ServiceTestResult, + type DEPRECATED__ServiceTestResult, type DEPRECATED__KeyedSingleExecutionTest, type Runtime, type ExecutionResult, @@ -123,7 +123,7 @@ export class TestContainerState { this.initializeAssertionData(testContainter); } - get testResult(): ServiceTestResult | undefined { + get testResult(): DEPRECATED__ServiceTestResult | undefined { const idx = this.testState.test.asserts.findIndex( (assert) => assert === this.testContainer, ); @@ -455,7 +455,7 @@ export class LegacySingleExecutionTestState { isGeneratingTestData = false; anonymizeGeneratedData = true; testSuiteRunError?: Error | undefined; - testResults: ServiceTestResult[] = []; + testResults: DEPRECATED__ServiceTestResult[] = []; allTestRunTime = 0; constructor( @@ -499,7 +499,7 @@ export class LegacySingleExecutionTestState { setSelectedTestContainerState(testContainerState?: TestContainerState): void { this.selectedTestContainerState = testContainerState; } - setTestResults(assertResults: ServiceTestResult[]): void { + setTestResults(assertResults: DEPRECATED__ServiceTestResult[]): void { this.testResults = assertResults; } setAnonymizeGeneratedData(val: boolean): void { @@ -589,10 +589,10 @@ export class LegacySingleExecutionTestState { this.isRunningAllTests = true; this.setTestResults([]); const results = - (yield this.editorStore.graphManagerState.graphManager.runServiceTests( + (yield this.editorStore.graphManagerState.graphManager.runLegacyServiceTests( this.serviceEditorState.service, this.serviceEditorState.editorStore.graphManagerState.graph, - )) as ServiceTestResult[]; + )) as DEPRECATED__ServiceTestResult[]; this.setTestResults(results); } catch (error) { assertErrorThrown(error); diff --git a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestPassed.ts b/packages/legend-studio/src/stores/editor-state/element-editor-state/service/ServiceTestRunnerState.ts similarity index 60% rename from packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestPassed.ts rename to packages/legend-studio/src/stores/editor-state/element-editor-state/service/ServiceTestRunnerState.ts index 7e5f2422bb..8d85b1807d 100644 --- a/packages/legend-graph/src/models/protocols/pure/v1/model/test/result/V1_TestPassed.ts +++ b/packages/legend-studio/src/stores/editor-state/element-editor-state/service/ServiceTestRunnerState.ts @@ -14,16 +14,18 @@ * limitations under the License. */ -import { hashArray, type Hashable } from '@finos/legend-shared'; -import { CORE_HASH_STRUCTURE } from '../../../../../../../MetaModelConst'; -import { V1_TestResult } from './V1_TestResult'; +import type { EditorStore } from '../../../EditorStore'; +import type { ServiceEditorState } from './ServiceEditorState'; -export class V1_TestPassed extends V1_TestResult implements Hashable { - override get hashCode(): string { - return hashArray([ - CORE_HASH_STRUCTURE.TEST_PASSED, - this.testable, - this.atomicTestId, - ]); +export class ServiceTestRunnerState { + editorStore: EditorStore; + serviceEditorState: ServiceEditorState; + + constructor( + editorStore: EditorStore, + serviceEditorState: ServiceEditorState, + ) { + this.editorStore = editorStore; + this.serviceEditorState = serviceEditorState; } } diff --git a/packages/legend-studio/src/stores/sidebar-state/testable/TestableManagerState.ts b/packages/legend-studio/src/stores/sidebar-state/testable/TestableManagerState.ts new file mode 100644 index 0000000000..2509591ef3 --- /dev/null +++ b/packages/legend-studio/src/stores/sidebar-state/testable/TestableManagerState.ts @@ -0,0 +1,472 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TreeData, TreeNodeData } from '@finos/legend-art'; +import { + type AssertionStatus, + type Test, + type Testable, + type TestResult, + type TestAssertion, + RunTestsTestableInput, + TestSuite, + AtomicTest, + AtomicTestId, + TestError, + TestFailed, + TestPassed, + AssertPass, + AssertFail, +} from '@finos/legend-graph'; +import { + type GeneratorFn, + assertErrorThrown, + isNonNullable, + ActionState, + uuid, + assertTrue, + guaranteeNonNullable, +} from '@finos/legend-shared'; +import { action, flow, makeObservable, observable } from 'mobx'; +import { + type TestableMetadata, + getTestableMetadata, +} from '../../../components/TestableStudio'; +import type { EditorSDLCState } from '../../EditorSDLCState'; +import type { EditorStore } from '../../EditorStore'; +import type { + LegendStudioPlugin, + TestableMetadataGetter, +} from '../../LegendStudioPlugin'; + +// TreeData +export abstract class TestableExplorerTreeNodeData implements TreeNodeData { + isSelected?: boolean | undefined; + isOpen?: boolean | undefined; + id: string; + label: string; + childrenIds?: string[] | undefined; + constructor(id: string, label: string) { + this.id = id; + this.label = label; + } +} + +export class TestableTreeNodeData extends TestableExplorerTreeNodeData { + testableMetadata: TestableMetadata; + + constructor(testable: TestableMetadata) { + super(testable.id, testable.id); + this.testableMetadata = testable; + } +} + +export abstract class TestTreeNodeData extends TestableExplorerTreeNodeData {} + +export class AtomicTestTreeNodeData extends TestTreeNodeData { + atomicTest: AtomicTest; + constructor(id: string, atomicTest: AtomicTest) { + super(id, atomicTest.id); + this.atomicTest = atomicTest; + } +} + +export class TestSuiteTreeNodeData extends TestTreeNodeData { + testSuite: TestSuite; + + constructor(id: string, testSuite: TestSuite) { + super(id, testSuite.id); + this.testSuite = testSuite; + } +} + +export class AssertionTestTreeNodeData extends TestableExplorerTreeNodeData { + assertion: TestAssertion; + + constructor(id: string, assertion: TestAssertion) { + super(id, assertion.id); + this.assertion = assertion; + } +} + +const buildTestNodeData = ( + test: Test, + parentId: string, +): TestTreeNodeData | undefined => { + if (test instanceof AtomicTest) { + return new AtomicTestTreeNodeData(`${parentId}.${test.id}`, test); + } else if (test instanceof TestSuite) { + return new TestSuiteTreeNodeData(`${parentId}.${test.id}`, test); + } + return undefined; +}; +const buildChildrenIfPossible = ( + node: TestableExplorerTreeNodeData, + treeData: TreeData, +): void => { + if (!node.childrenIds) { + let children: TestableExplorerTreeNodeData[] = []; + if (node instanceof TestableTreeNodeData) { + children = node.testableMetadata.testable.tests + .map((t) => buildTestNodeData(t, node.id)) + .filter(isNonNullable); + } else if (node instanceof TestSuiteTreeNodeData) { + children = node.testSuite.tests + .map((t) => buildTestNodeData(t, node.id)) + .filter(isNonNullable); + } else if (node instanceof AtomicTestTreeNodeData) { + children = node.atomicTest.assertions.map((assertion) => { + const assertionNode = new AssertionTestTreeNodeData( + `${node.id}.${assertion.id}`, + assertion, + ); + return assertionNode; + }); + } + node.childrenIds = children.map((c) => c.id); + children.forEach((c) => treeData.nodes.set(c.id, c)); + } +}; + +const onTreeNodeSelect = ( + node: TestableExplorerTreeNodeData, + treeData: TreeData, +): void => { + buildChildrenIfPossible(node, treeData); + node.isOpen = !node.isOpen; +}; + +// Result Helpers +export const getAtomicTest_TestResult = ( + atomicTest: AtomicTest, + results: Map, +): TestResult | undefined => results.get(atomicTest); + +const getAssertion_TestResult = ( + assertion: TestAssertion, + results: Map, +): TestResult | undefined => { + const test = assertion.parentTest; + return test ? getAtomicTest_TestResult(test, results) : undefined; +}; + +export const getAssertionStatus = ( + assertion: TestAssertion, + results: Map, +): AssertionStatus | undefined => { + const result = getAssertion_TestResult(assertion, results); + if (result instanceof TestFailed) { + return result.assertStatuses.find((s) => s.assertion === assertion); + } + return undefined; +}; + +const getTestSuite_TestResults = ( + suite: TestSuite, + results: Map, +): (TestResult | undefined)[] => + suite.tests.map((t) => getAtomicTest_TestResult(t, results)); + +const getTest_TestResults = ( + test: Test, + results: Map, +): (TestResult | undefined)[] => { + if (test instanceof AtomicTest) { + return [getAtomicTest_TestResult(test, results)]; + } else if (test instanceof TestSuite) { + return getTestSuite_TestResults(test, results); + } + return [undefined]; +}; + +const getTestable_TestResult = ( + test: Testable, + results: Map, +): (TestResult | undefined)[] => + test.tests.flatMap((t) => getTest_TestResults(t, results)); +export enum TESTABLE_RESULT { + NONE = 'NONE', + ERROR = 'ERROR', + FAILED = 'FAILED', + PASSED = 'PASSED', +} + +const getTestableResultFromTestResult = ( + testResult: TestResult | undefined, +): TESTABLE_RESULT => { + if (testResult instanceof TestPassed) { + return TESTABLE_RESULT.PASSED; + } else if (testResult instanceof TestFailed) { + return TESTABLE_RESULT.FAILED; + } else if (testResult instanceof TestError) { + return TESTABLE_RESULT.ERROR; + } + return TESTABLE_RESULT.NONE; +}; + +const getTestableResultFromAssertionStatus = ( + assertionStatus: AssertionStatus | undefined, +): TESTABLE_RESULT => { + if (assertionStatus instanceof AssertPass) { + return TESTABLE_RESULT.PASSED; + } else if (assertionStatus instanceof AssertFail) { + return TESTABLE_RESULT.FAILED; + } + return TESTABLE_RESULT.NONE; +}; +const getTestableResultFromTestResults = ( + testResults: (TestResult | undefined)[], +): TESTABLE_RESULT => { + if (testResults.every((t) => t instanceof TestPassed)) { + return TESTABLE_RESULT.PASSED; + } else if (testResults.find((t) => t instanceof TestError)) { + return TESTABLE_RESULT.ERROR; + } else if (testResults.find((t) => t instanceof TestFailed)) { + return TESTABLE_RESULT.FAILED; + } + return TESTABLE_RESULT.NONE; +}; + +export const getNodeTestableResult = ( + node: TestableExplorerTreeNodeData, + results: Map, +): TESTABLE_RESULT => { + if (node instanceof AssertionTestTreeNodeData) { + const status = getAssertionStatus(node.assertion, results); + if (status) { + return getTestableResultFromAssertionStatus(status); + } + const result = node.assertion.parentTest + ? results.get(node.assertion.parentTest) + : undefined; + return getTestableResultFromTestResult(result); + } else if (node instanceof AtomicTestTreeNodeData) { + return getTestableResultFromTestResult( + getAtomicTest_TestResult(node.atomicTest, results), + ); + } else if (node instanceof TestSuiteTreeNodeData) { + return getTestableResultFromTestResults( + getTestSuite_TestResults(node.testSuite, results), + ); + } else if (node instanceof TestableTreeNodeData) { + return getTestableResultFromTestResults( + getTestable_TestResult(node.testableMetadata.testable, results), + ); + } + return TESTABLE_RESULT.NONE; +}; + +export class TestableState { + uuid = uuid(); + managerState: TestableManagerState; + editorStore: EditorStore; + testableMetadata: TestableMetadata; + treeData: TreeData; + results: Map = new Map(); + isRunningTests = ActionState.create(); + + constructor( + editorStore: EditorStore, + managerState: TestableManagerState, + testable: Testable, + ) { + makeObservable(this, { + editorStore: false, + testableMetadata: observable, + isRunningTests: observable, + results: observable, + treeData: observable.ref, + handleTestableResult: action, + setTreeData: action, + onTreeNodeSelect: action, + run: flow, + }); + this.editorStore = editorStore; + this.managerState = managerState; + this.testableMetadata = guaranteeNonNullable( + getTestableMetadata( + testable, + editorStore, + this.managerState.extraTestableMetadataGetters, + ), + ); + this.treeData = this.buildTreeData(this.testableMetadata); + } + + *run(node: TestableExplorerTreeNodeData): GeneratorFn { + try { + this.isRunningTests.inProgress(); + let input: RunTestsTestableInput; + if (node instanceof AssertionTestTreeNodeData) { + const atomicTest = guaranteeNonNullable(node.assertion.parentTest); + const suite = atomicTest.parentSuite; + input = new RunTestsTestableInput(this.testableMetadata.testable); + input.unitTestIds = [new AtomicTestId(suite, atomicTest)]; + } else if (node instanceof AtomicTestTreeNodeData) { + const atomicTest = node.atomicTest; + const suite = atomicTest.parentSuite; + input = new RunTestsTestableInput(this.testableMetadata.testable); + input.unitTestIds = [new AtomicTestId(suite, atomicTest)]; + } else if (node instanceof TestSuiteTreeNodeData) { + input = new RunTestsTestableInput(this.testableMetadata.testable); + input.unitTestIds = node.testSuite.tests.map( + (s) => new AtomicTestId(node.testSuite, s), + ); + } else { + input = new RunTestsTestableInput(this.testableMetadata.testable); + } + const testResults = + (yield this.editorStore.graphManagerState.graphManager.runTests( + this.editorStore.graphManagerState.graph, + [input], + )) as TestResult[]; + this.managerState.handleResults(testResults); + this.isRunningTests.complete(); + } catch (error) { + assertErrorThrown(error); + this.editorStore.applicationStore.notifyError(error); + this.isRunningTests.fail(); + } + } + + handleTestableResult(testResult: TestResult, openAssertions?: boolean): void { + try { + assertTrue(testResult.testable === this.testableMetadata.testable); + this.results.set(testResult.atomicTestId.atomicTest, testResult); + } catch (error) { + assertErrorThrown(error); + this.editorStore.applicationStore.notifyError( + `Unable to update test result: ${error.message}`, + ); + } + } + + buildTreeData( + testable: TestableMetadata, + ): TreeData { + const rootIds: string[] = []; + const nodes = new Map(); + const treeData = { rootIds, nodes }; + const testableTreeNodeData = new TestableTreeNodeData(testable); + treeData.rootIds.push(testableTreeNodeData.id); + treeData.nodes.set(testableTreeNodeData.id, testableTreeNodeData); + return treeData; + } + + setTreeData(data: TreeData): void { + this.treeData = data; + } + + onTreeNodeSelect( + node: TestableExplorerTreeNodeData, + treeData: TreeData, + ): void { + onTreeNodeSelect(node, treeData); + this.setTreeData({ ...treeData }); + } +} + +export class TestableManagerState { + editorStore: EditorStore; + sdlcState: EditorSDLCState; + _testableStates: TestableState[] | undefined; + isRunningTests = ActionState.create(); + extraTestableMetadataGetters: TestableMetadataGetter[] = []; + failureViewing: AssertFail | TestError | undefined; + + constructor(editorStore: EditorStore, sdlcState: EditorSDLCState) { + makeObservable(this, { + editorStore: false, + sdlcState: false, + _testableStates: observable, + init: action, + runAllTests: flow, + failureViewing: observable, + setFailureViewing: action, + }); + this.editorStore = editorStore; + this.sdlcState = sdlcState; + this.extraTestableMetadataGetters = editorStore.pluginManager + .getStudioPlugins() + .flatMap( + (plugin: LegendStudioPlugin) => + plugin.getExtraTestableMetadata?.() ?? [], + ) + .filter(isNonNullable); + } + + init(force?: boolean): void { + if (!this._testableStates || force) { + const testables = + this.editorStore.graphManagerState.graph.allOwnTestables; + this._testableStates = testables.map( + (testable) => new TestableState(this.editorStore, this, testable), + ); + } + } + + get testables(): TestableState[] { + return this._testableStates ?? []; + } + + get isDispatchingAction(): boolean { + return ( + this.isRunningTests.isInProgress || + this.testables.some((s) => s.isRunningTests.isInProgress) + ); + } + + setFailureViewing(val: AssertFail | TestError | undefined): void { + this.failureViewing = val; + } + *runAllTests(testableState: TestableState | undefined): GeneratorFn { + try { + this.isRunningTests.inProgress(); + let inputs: RunTestsTestableInput[] = []; + if (!testableState) { + inputs = (this._testableStates ?? []).map( + (e) => new RunTestsTestableInput(e.testableMetadata.testable), + ); + } else { + inputs = [ + new RunTestsTestableInput(testableState.testableMetadata.testable), + ]; + } + const testResults = + (yield this.editorStore.graphManagerState.graphManager.runTests( + this.editorStore.graphManagerState.graph, + inputs, + )) as TestResult[]; + + this.handleResults(testResults); + this.isRunningTests.complete(); + } catch (error) { + assertErrorThrown(error); + this.editorStore.applicationStore.notifyError(error); + this.isRunningTests.fail(); + } + } + handleResults(testResults: TestResult[]): void { + testResults.forEach((testResult) => { + const testableState = this.testables.find( + (tState) => tState.testableMetadata.testable === testResult.testable, + ); + if (testableState) { + testableState.handleTestableResult(testResult, true); + } + }); + } +} diff --git a/packages/legend-studio/style/components/editor/_side-bar.scss b/packages/legend-studio/style/components/editor/_side-bar.scss index 1f4d3d30d8..aceac851ab 100644 --- a/packages/legend-studio/style/components/editor/_side-bar.scss +++ b/packages/legend-studio/style/components/editor/_side-bar.scss @@ -19,6 +19,7 @@ @forward 'side-bar/explorer'; @forward 'side-bar/workspace-review'; @forward 'side-bar/workflow-manager'; +@forward 'side-bar/testable_manager'; @forward 'side-bar/workspace-updater'; @forward 'side-bar/local-changes'; @forward 'side-bar/patch-loader'; diff --git a/packages/legend-studio/style/components/editor/side-bar/_testable_manager.scss b/packages/legend-studio/style/components/editor/side-bar/_testable_manager.scss new file mode 100644 index 0000000000..8d10567ab0 --- /dev/null +++ b/packages/legend-studio/style/components/editor/side-bar/_testable_manager.scss @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@use 'mixins' as *; + +.testable-manager { + &__header__title { + width: calc(100% - 3.4rem); + } + + &__refresh-btn svg { + font-size: 1.7rem; + } + + &__refresh-btn--loading svg { + animation: spin 1s infinite ease; + } + + &__item__link { + color: var(--color-dark-grey-500); + text-decoration: none; + } + + &__item__link__content { + @include ellipsisTextOverflow; + } + + &__item__link__content__status__indicator { + display: inline-flex; + vertical-align: middle; + + svg { + font-size: 1.5rem; + } + + &--succeeded svg { + color: var(--color-green-100); + } + + &--failed svg { + color: var(--color-red-100); + } + + &--in-progress svg { + animation: spin 1s infinite ease; + color: var(--color-blue-100); + } + + &--suspended svg { + color: var(--color-yellow-400); + } + + &--unknown svg, + &--canceled svg { + color: var(--color-dark-grey-500); + } + } + + &__item__link__content__id { + color: var(--color-light-grey-400); + } + + &__item__link__content__created-at { + margin-left: 1rem; + font-size: 1.2rem; + color: var(--color-dark-grey-500); + } + + &__explorer { + &__testable-tree__node__container:hover { + background: var(--color-dark-blue-shade-100); + } + + &__testable-tree__node__container--selected, + &__testable-tree__node__container--selected:hover { + background: var(--color-light-blue-450); + } + + &__testable-tree__node__icon { + width: 4rem; + min-width: 4rem; + } + + &__testable-tree__node__icon__expand, + &__testable-tree__node__icon__type { + width: 2rem; + + @include flexHCenter; + } + + &__testable-tree__node__result__icon__type { + width: 2rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + + @include flexHCenter; + } + + &__testable-tree__node__icon__expand svg { + font-size: 1rem; + } + + &__testable-tree__node__label { + color: inherit; + } + } +}