@finos/legend-application-studio 28.21.3 → 28.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +63 -20
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +9 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts +23 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts.map +1 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +230 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -0
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +39 -5
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +16 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +36 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +4 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +4 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +118 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +600 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -0
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts +17 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js +46 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js.map +1 -1
- package/package.json +9 -9
- package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +135 -49
- package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +14 -0
- package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +872 -0
- package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +169 -15
- package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +54 -1
- package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +4 -0
- package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +863 -0
- package/src/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.ts +66 -0
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type Accessor,
|
|
19
|
+
type AccessPoint,
|
|
20
|
+
type PackageableElement,
|
|
21
|
+
type TestSuite,
|
|
22
|
+
type RawLambda,
|
|
23
|
+
type AccessorOwner,
|
|
24
|
+
DataProduct,
|
|
25
|
+
FunctionAccessPoint,
|
|
26
|
+
LakehouseAccessPoint,
|
|
27
|
+
DataProductTestSuite,
|
|
28
|
+
BaseDataResolver,
|
|
29
|
+
DataProductAccessPointTest,
|
|
30
|
+
RelationElementsData,
|
|
31
|
+
RelationElement,
|
|
32
|
+
PackageableElementExplicitReference,
|
|
33
|
+
TestExecuted,
|
|
34
|
+
TestError,
|
|
35
|
+
TestExecutionStatus,
|
|
36
|
+
EqualToRelation,
|
|
37
|
+
observe_RelationElement,
|
|
38
|
+
observe_RelationElementsData,
|
|
39
|
+
observe_DataProductTestSuite,
|
|
40
|
+
IngestDefinition,
|
|
41
|
+
getAccessorItemLabelForElement,
|
|
42
|
+
} from '@finos/legend-graph';
|
|
43
|
+
import {
|
|
44
|
+
type GeneratorFn,
|
|
45
|
+
ActionState,
|
|
46
|
+
assertErrorThrown,
|
|
47
|
+
deleteEntry,
|
|
48
|
+
addUniqueEntry,
|
|
49
|
+
uuid,
|
|
50
|
+
noop,
|
|
51
|
+
} from '@finos/legend-shared';
|
|
52
|
+
import {
|
|
53
|
+
action,
|
|
54
|
+
flow,
|
|
55
|
+
flowResult,
|
|
56
|
+
makeObservable,
|
|
57
|
+
observable,
|
|
58
|
+
runInAction,
|
|
59
|
+
} from 'mobx';
|
|
60
|
+
import type { EditorStore } from '../../../../EditorStore.js';
|
|
61
|
+
import type { DataProductEditorState } from '../DataProductEditorState.js';
|
|
62
|
+
import { RelationElementState } from '../../data/EmbeddedDataState.js';
|
|
63
|
+
import { TESTABLE_RESULT } from '../../../../sidebar-state/testable/GlobalTestRunnerState.js';
|
|
64
|
+
import { testSuite_addTest } from '../../../../../graph-modifier/Testable_GraphModifierHelper.js';
|
|
65
|
+
import {
|
|
66
|
+
TestableTestEditorState,
|
|
67
|
+
TestableTestSuiteEditorState,
|
|
68
|
+
} from '../../testable/TestableEditorState.js';
|
|
69
|
+
|
|
70
|
+
const createEmptyRelationElement = (
|
|
71
|
+
itemId: string,
|
|
72
|
+
columns: string[] = [],
|
|
73
|
+
): RelationElement => {
|
|
74
|
+
const relationElement = new RelationElement();
|
|
75
|
+
relationElement.paths = [itemId];
|
|
76
|
+
relationElement.columns = columns;
|
|
77
|
+
relationElement.rows = [];
|
|
78
|
+
return observe_RelationElement(relationElement);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns the lambda for an access point (handles both LakehouseAccessPoint
|
|
83
|
+
* and FunctionAccessPoint).
|
|
84
|
+
*/
|
|
85
|
+
const getAccessPointLambda = (
|
|
86
|
+
accessPoint: AccessPoint,
|
|
87
|
+
): RawLambda | undefined =>
|
|
88
|
+
accessPoint instanceof LakehouseAccessPoint
|
|
89
|
+
? accessPoint.func
|
|
90
|
+
: accessPoint instanceof FunctionAccessPoint
|
|
91
|
+
? accessPoint.query
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds a RelationElementsData from resolved accessors using the accessor
|
|
96
|
+
* relation type as the source of truth for column definitions.
|
|
97
|
+
*/
|
|
98
|
+
const buildRelationElementsDataWithColumns = (
|
|
99
|
+
accs: Accessor[],
|
|
100
|
+
): RelationElementsData => {
|
|
101
|
+
const relData = new RelationElementsData();
|
|
102
|
+
relData.relationElements = accs.map((acc) => {
|
|
103
|
+
const itemId = acc.accessor || 'UNKNOWN';
|
|
104
|
+
const columns = acc.relationType.columns.map((column) => column.name);
|
|
105
|
+
return createEmptyRelationElement(itemId, columns);
|
|
106
|
+
});
|
|
107
|
+
return relData;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isIngestOrDataProductAccessor = (
|
|
111
|
+
accessor: Accessor,
|
|
112
|
+
): accessor is Accessor =>
|
|
113
|
+
accessor.parentElement instanceof DataProduct ||
|
|
114
|
+
accessor.parentElement instanceof IngestDefinition;
|
|
115
|
+
|
|
116
|
+
const getAccessPointDisplayLabel = (accessPoint: AccessPoint): string =>
|
|
117
|
+
accessPoint.id;
|
|
118
|
+
|
|
119
|
+
interface ElementDataItem {
|
|
120
|
+
id: string;
|
|
121
|
+
label: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const getElementDataItems = (
|
|
125
|
+
element: PackageableElement,
|
|
126
|
+
): ElementDataItem[] => {
|
|
127
|
+
if (element instanceof DataProduct) {
|
|
128
|
+
return element.accessPointGroups
|
|
129
|
+
.flatMap((g) => g.accessPoints)
|
|
130
|
+
.map((ap) => ({
|
|
131
|
+
id: ap.id,
|
|
132
|
+
label: getAccessPointDisplayLabel(ap),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
if (element instanceof IngestDefinition) {
|
|
136
|
+
return (element.TEMPORARY_MATVIEW_FUNCTION_DATA_SETS ?? []).map((ds) => ({
|
|
137
|
+
id: ds.name,
|
|
138
|
+
label: ds.name,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const inferDataProductItemColumns = async (
|
|
145
|
+
editorStore: EditorStore,
|
|
146
|
+
dataProduct: DataProduct,
|
|
147
|
+
itemId: string,
|
|
148
|
+
): Promise<string[] | undefined> => {
|
|
149
|
+
const accessPoint = dataProduct.accessPointGroups
|
|
150
|
+
.flatMap((group) => group.accessPoints)
|
|
151
|
+
.find((ap) => ap.id === itemId);
|
|
152
|
+
if (!accessPoint) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const lambda = getAccessPointLambda(accessPoint);
|
|
156
|
+
if (!lambda) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const relationMetadata =
|
|
160
|
+
await editorStore.graphManagerState.graphManager.getLambdaRelationType(
|
|
161
|
+
lambda,
|
|
162
|
+
editorStore.graphManagerState.graph,
|
|
163
|
+
);
|
|
164
|
+
return relationMetadata.columns.map((column) => column.name);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ─── Per-test state ──────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export class DataProductTestState extends TestableTestEditorState {
|
|
170
|
+
readonly suiteState: DataProductTestSuiteState;
|
|
171
|
+
override test: DataProductAccessPointTest;
|
|
172
|
+
readonly uuid = uuid();
|
|
173
|
+
|
|
174
|
+
/** Wraps assertion.expected — drives both column definitions and test data rows. */
|
|
175
|
+
testDataRelationState: RelationElementState | undefined;
|
|
176
|
+
|
|
177
|
+
constructor(
|
|
178
|
+
suiteState: DataProductTestSuiteState,
|
|
179
|
+
test: DataProductAccessPointTest,
|
|
180
|
+
) {
|
|
181
|
+
super(
|
|
182
|
+
suiteState.testableState.dataProduct,
|
|
183
|
+
test,
|
|
184
|
+
suiteState.testableState.dataProductEditorState.isReadOnly,
|
|
185
|
+
suiteState.editorStore,
|
|
186
|
+
);
|
|
187
|
+
makeObservable(this, {
|
|
188
|
+
// observable fields from base class
|
|
189
|
+
selectedAsertionState: observable,
|
|
190
|
+
selectedTab: observable,
|
|
191
|
+
assertionToRename: observable,
|
|
192
|
+
assertionEditorStates: observable,
|
|
193
|
+
testResultState: observable,
|
|
194
|
+
runningTestAction: observable,
|
|
195
|
+
// own observable
|
|
196
|
+
testDataRelationState: observable,
|
|
197
|
+
// actions from base class
|
|
198
|
+
setSelectedTab: action,
|
|
199
|
+
setAssertionToRename: action,
|
|
200
|
+
addAssertion: action,
|
|
201
|
+
deleteAssertion: action,
|
|
202
|
+
openAssertion: action,
|
|
203
|
+
resetResult: action,
|
|
204
|
+
handleTestResult: action,
|
|
205
|
+
// flow from base class
|
|
206
|
+
runTest: flow,
|
|
207
|
+
});
|
|
208
|
+
this.suiteState = suiteState;
|
|
209
|
+
this.test = test;
|
|
210
|
+
this.buildTestDataRelationState().catch(noop());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async buildTestDataRelationState(): Promise<void> {
|
|
214
|
+
const assertion = this.test.assertions.find(
|
|
215
|
+
(a): a is EqualToRelation => a instanceof EqualToRelation,
|
|
216
|
+
);
|
|
217
|
+
if (!assertion) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Populate columns from the engine if not yet set
|
|
221
|
+
if (assertion.expected.columns.length === 0) {
|
|
222
|
+
try {
|
|
223
|
+
const engineColumns = await inferDataProductItemColumns(
|
|
224
|
+
this.editorStore,
|
|
225
|
+
this.suiteState.testableState.dataProduct,
|
|
226
|
+
this.test.accessPointId,
|
|
227
|
+
);
|
|
228
|
+
if (engineColumns && engineColumns.length > 0) {
|
|
229
|
+
runInAction(() => {
|
|
230
|
+
assertion.expected.columns = engineColumns;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// best-effort; continue with empty columns
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
runInAction(() => {
|
|
238
|
+
this.testDataRelationState = new RelationElementState(assertion.expected);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
get accessPointLabel(): string {
|
|
243
|
+
return this.suiteState.testableState.getOwnAccessPointLabel(
|
|
244
|
+
this.test.accessPointId,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── Per-element test data state ─────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
export class DataProductElementTestDataState {
|
|
252
|
+
readonly testDataState: DataProductTestDataState;
|
|
253
|
+
readonly testData: BaseDataResolver;
|
|
254
|
+
readonly editorStore: EditorStore;
|
|
255
|
+
|
|
256
|
+
/** Currently selected dataset/item within this element's RelationElementsData */
|
|
257
|
+
selectedItemId: string | undefined;
|
|
258
|
+
/** RelationElementState for the currently selected item */
|
|
259
|
+
relationElementState: RelationElementState | undefined;
|
|
260
|
+
|
|
261
|
+
constructor(
|
|
262
|
+
testDataState: DataProductTestDataState,
|
|
263
|
+
testData: BaseDataResolver,
|
|
264
|
+
) {
|
|
265
|
+
makeObservable(this, {
|
|
266
|
+
selectedItemId: observable,
|
|
267
|
+
relationElementState: observable,
|
|
268
|
+
setSelectedItem: action,
|
|
269
|
+
});
|
|
270
|
+
this.testDataState = testDataState;
|
|
271
|
+
this.testData = testData;
|
|
272
|
+
this.editorStore = testDataState.editorStore;
|
|
273
|
+
// Select the first item that has data
|
|
274
|
+
if (testData.data instanceof RelationElementsData) {
|
|
275
|
+
const firstRel = testData.data.relationElements[0];
|
|
276
|
+
if (firstRel) {
|
|
277
|
+
this.selectedItemId = firstRel.paths[0];
|
|
278
|
+
this.relationElementState = new RelationElementState(firstRel);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get element(): PackageableElement {
|
|
284
|
+
return this.testData.element.value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get elementName(): string {
|
|
288
|
+
return this.testData.element.value.name;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get itemLabel(): string {
|
|
292
|
+
return getAccessorItemLabelForElement(this.element as AccessorOwner);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get configuredItemIds(): string[] {
|
|
296
|
+
if (this.testData.data instanceof RelationElementsData) {
|
|
297
|
+
return this.testData.data.relationElements.map((re) => re.paths[0] ?? '');
|
|
298
|
+
}
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
get configuredItems(): ElementDataItem[] {
|
|
303
|
+
const availableItemsById = new Map(
|
|
304
|
+
getElementDataItems(this.element).map((item) => [item.id, item.label]),
|
|
305
|
+
);
|
|
306
|
+
return this.configuredItemIds.map((id) => ({
|
|
307
|
+
id,
|
|
308
|
+
label: availableItemsById.get(id) ?? id,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setSelectedItem(itemId: string | undefined): void {
|
|
313
|
+
this.selectedItemId = itemId;
|
|
314
|
+
if (itemId && this.testData.data instanceof RelationElementsData) {
|
|
315
|
+
const relEl = this.testData.data.relationElements.find(
|
|
316
|
+
(re) => re.paths[0] === itemId,
|
|
317
|
+
);
|
|
318
|
+
this.relationElementState = relEl
|
|
319
|
+
? new RelationElementState(relEl)
|
|
320
|
+
: undefined;
|
|
321
|
+
} else {
|
|
322
|
+
this.relationElementState = undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Test data state for a suite ─────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
export class DataProductTestDataState {
|
|
330
|
+
readonly editorStore: EditorStore;
|
|
331
|
+
readonly suiteState: DataProductTestSuiteState;
|
|
332
|
+
|
|
333
|
+
elementTestDataStates: DataProductElementTestDataState[] = [];
|
|
334
|
+
selectedElementTestDataState: DataProductElementTestDataState | undefined;
|
|
335
|
+
|
|
336
|
+
constructor(
|
|
337
|
+
suiteState: DataProductTestSuiteState,
|
|
338
|
+
options?: {
|
|
339
|
+
selectedElementPath?: string | undefined;
|
|
340
|
+
selectedItemId?: string | undefined;
|
|
341
|
+
},
|
|
342
|
+
) {
|
|
343
|
+
makeObservable(this, {
|
|
344
|
+
elementTestDataStates: observable,
|
|
345
|
+
selectedElementTestDataState: observable,
|
|
346
|
+
setSelectedElementTestDataState: action,
|
|
347
|
+
refreshElementTestDataStates: action,
|
|
348
|
+
});
|
|
349
|
+
this.editorStore = suiteState.editorStore;
|
|
350
|
+
this.suiteState = suiteState;
|
|
351
|
+
this.refreshElementTestDataStates(options);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
refreshElementTestDataStates(options?: {
|
|
355
|
+
selectedElementPath?: string | undefined;
|
|
356
|
+
selectedItemId?: string | undefined;
|
|
357
|
+
}): void {
|
|
358
|
+
const previouslySelectedElementPath =
|
|
359
|
+
options?.selectedElementPath ??
|
|
360
|
+
this.selectedElementTestDataState?.element.path;
|
|
361
|
+
const previouslySelectedItemId =
|
|
362
|
+
options?.selectedItemId ??
|
|
363
|
+
this.selectedElementTestDataState?.selectedItemId;
|
|
364
|
+
const suite = this.suiteState.suite;
|
|
365
|
+
this.elementTestDataStates = (suite.testData ?? [])
|
|
366
|
+
.filter((td): td is BaseDataResolver => td instanceof BaseDataResolver)
|
|
367
|
+
.map((td) => new DataProductElementTestDataState(this, td));
|
|
368
|
+
|
|
369
|
+
this.selectedElementTestDataState =
|
|
370
|
+
this.elementTestDataStates.find(
|
|
371
|
+
(state) => state.element.path === previouslySelectedElementPath,
|
|
372
|
+
) ?? this.elementTestDataStates[0];
|
|
373
|
+
|
|
374
|
+
if (this.selectedElementTestDataState && previouslySelectedItemId) {
|
|
375
|
+
const nextSelectedItemId =
|
|
376
|
+
this.selectedElementTestDataState.configuredItemIds.find(
|
|
377
|
+
(itemId) => itemId === previouslySelectedItemId,
|
|
378
|
+
) ?? this.selectedElementTestDataState.configuredItemIds[0];
|
|
379
|
+
this.selectedElementTestDataState.setSelectedItem(nextSelectedItemId);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
setSelectedElementTestDataState(
|
|
384
|
+
val: DataProductElementTestDataState | undefined,
|
|
385
|
+
): void {
|
|
386
|
+
this.selectedElementTestDataState = val;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Per-suite state ─────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
export class DataProductTestSuiteState extends TestableTestSuiteEditorState {
|
|
393
|
+
readonly testableState: DataProductTestableState;
|
|
394
|
+
override suite: DataProductTestSuite;
|
|
395
|
+
override testStates: DataProductTestState[] = [];
|
|
396
|
+
declare selectTestState: DataProductTestState | undefined;
|
|
397
|
+
testDataState: DataProductTestDataState;
|
|
398
|
+
|
|
399
|
+
constructor(
|
|
400
|
+
editorStore: EditorStore,
|
|
401
|
+
testableState: DataProductTestableState,
|
|
402
|
+
suite: DataProductTestSuite,
|
|
403
|
+
) {
|
|
404
|
+
super(
|
|
405
|
+
testableState.dataProduct,
|
|
406
|
+
suite,
|
|
407
|
+
testableState.dataProductEditorState.isReadOnly,
|
|
408
|
+
editorStore,
|
|
409
|
+
);
|
|
410
|
+
makeObservable(this, {
|
|
411
|
+
testStates: observable,
|
|
412
|
+
selectTestState: observable,
|
|
413
|
+
testDataState: observable,
|
|
414
|
+
addNewTest: flow,
|
|
415
|
+
deleteTest: action,
|
|
416
|
+
runSuite: flow,
|
|
417
|
+
runFailingTests: flow,
|
|
418
|
+
buildTestStates: action,
|
|
419
|
+
});
|
|
420
|
+
this.testableState = testableState;
|
|
421
|
+
this.suite = suite;
|
|
422
|
+
this.testDataState = new DataProductTestDataState(this);
|
|
423
|
+
this.buildTestStates();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
refreshTestDataState(): void {
|
|
427
|
+
this.testDataState = new DataProductTestDataState(this, {
|
|
428
|
+
selectedElementPath:
|
|
429
|
+
this.testDataState.selectedElementTestDataState?.element.path,
|
|
430
|
+
selectedItemId:
|
|
431
|
+
this.testDataState.selectedElementTestDataState?.selectedItemId,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
buildTestStates(): void {
|
|
436
|
+
this.testStates = this.suite.tests.map(
|
|
437
|
+
(t) => new DataProductTestState(this, t as DataProductAccessPointTest),
|
|
438
|
+
);
|
|
439
|
+
this.selectTestState = this.testStates[0];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
override deleteTest(test: DataProductAccessPointTest): void {
|
|
443
|
+
super.deleteTest(test);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
*addNewTest(
|
|
447
|
+
testName: string,
|
|
448
|
+
accessPointId: string,
|
|
449
|
+
): GeneratorFn<string | undefined> {
|
|
450
|
+
const observerContext =
|
|
451
|
+
this.editorStore.changeDetectionState.observerContext;
|
|
452
|
+
|
|
453
|
+
let inferredColumns: string[] = [];
|
|
454
|
+
try {
|
|
455
|
+
const cols = (yield inferDataProductItemColumns(
|
|
456
|
+
this.editorStore,
|
|
457
|
+
this.testableState.dataProduct,
|
|
458
|
+
accessPointId,
|
|
459
|
+
)) as string[] | undefined;
|
|
460
|
+
if (cols) {
|
|
461
|
+
inferredColumns = cols;
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
// best-effort
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const test = new DataProductAccessPointTest();
|
|
468
|
+
test.id = testName;
|
|
469
|
+
test.__parent = this.suite;
|
|
470
|
+
test.accessPointId = accessPointId;
|
|
471
|
+
|
|
472
|
+
// Resolve sources through the graph (follows function calls, no self-refs)
|
|
473
|
+
const accessPointForTest = this.testableState.dataProduct.accessPointGroups
|
|
474
|
+
.flatMap((g) => g.accessPoints)
|
|
475
|
+
.find((ap) => ap.id === accessPointId);
|
|
476
|
+
const rawLambdaForTest = accessPointForTest
|
|
477
|
+
? getAccessPointLambda(accessPointForTest)
|
|
478
|
+
: undefined;
|
|
479
|
+
|
|
480
|
+
let resolvedAccessors: Accessor[] = [];
|
|
481
|
+
if (rawLambdaForTest) {
|
|
482
|
+
const all =
|
|
483
|
+
(yield this.editorStore.graphManagerState.graphManager.collectAccessorsInRawLambda(
|
|
484
|
+
rawLambdaForTest,
|
|
485
|
+
this.editorStore.graphManagerState.graph,
|
|
486
|
+
)) as Accessor[];
|
|
487
|
+
resolvedAccessors = all.filter(
|
|
488
|
+
(accessor) =>
|
|
489
|
+
isIngestOrDataProductAccessor(accessor) &&
|
|
490
|
+
accessor.parentElement.path !== this.testableState.dataProduct.path,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (resolvedAccessors.length > 0) {
|
|
495
|
+
// Group by element, merge into existing resolvers or create new ones
|
|
496
|
+
const byElement = new Map<string, Accessor[]>();
|
|
497
|
+
for (const acc of resolvedAccessors) {
|
|
498
|
+
const grp = byElement.get(acc.parentElement.path) ?? [];
|
|
499
|
+
grp.push(acc);
|
|
500
|
+
byElement.set(acc.parentElement.path, grp);
|
|
501
|
+
}
|
|
502
|
+
for (const [elementPath, accs] of byElement) {
|
|
503
|
+
const element =
|
|
504
|
+
this.editorStore.graphManagerState.graph.getNullableElement(
|
|
505
|
+
elementPath,
|
|
506
|
+
);
|
|
507
|
+
if (!element) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const existingResolver = this.suite.testData?.find(
|
|
511
|
+
(td): td is BaseDataResolver =>
|
|
512
|
+
td instanceof BaseDataResolver && td.element.value === element,
|
|
513
|
+
);
|
|
514
|
+
const relData =
|
|
515
|
+
existingResolver?.data instanceof RelationElementsData
|
|
516
|
+
? existingResolver.data
|
|
517
|
+
: undefined;
|
|
518
|
+
if (!existingResolver || !relData) {
|
|
519
|
+
const resolver = new BaseDataResolver();
|
|
520
|
+
resolver.element =
|
|
521
|
+
PackageableElementExplicitReference.create(element);
|
|
522
|
+
const newRelData = buildRelationElementsDataWithColumns(accs);
|
|
523
|
+
observe_RelationElementsData(newRelData);
|
|
524
|
+
resolver.data = newRelData;
|
|
525
|
+
this.suite.testData = [...(this.suite.testData ?? []), resolver];
|
|
526
|
+
} else {
|
|
527
|
+
for (const acc of accs) {
|
|
528
|
+
const itemId = acc.accessor;
|
|
529
|
+
if (
|
|
530
|
+
!relData.relationElements.find((re) => re.paths[0] === itemId)
|
|
531
|
+
) {
|
|
532
|
+
const columns = acc.relationType.columns.map(
|
|
533
|
+
(column) => column.name,
|
|
534
|
+
);
|
|
535
|
+
relData.relationElements.push(
|
|
536
|
+
createEmptyRelationElement(itemId, columns),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
// Fallback: single resolver on current DP
|
|
544
|
+
let relationData = this.suite.testData?.find(
|
|
545
|
+
(td): td is BaseDataResolver =>
|
|
546
|
+
td instanceof BaseDataResolver &&
|
|
547
|
+
td.element.value === this.testableState.dataProduct &&
|
|
548
|
+
td.data instanceof RelationElementsData,
|
|
549
|
+
)?.data as RelationElementsData | undefined;
|
|
550
|
+
|
|
551
|
+
if (!relationData) {
|
|
552
|
+
const testData = new BaseDataResolver();
|
|
553
|
+
testData.element = PackageableElementExplicitReference.create(
|
|
554
|
+
this.testableState.dataProduct,
|
|
555
|
+
);
|
|
556
|
+
relationData = new RelationElementsData();
|
|
557
|
+
relationData.relationElements = [];
|
|
558
|
+
observe_RelationElementsData(relationData);
|
|
559
|
+
testData.data = relationData;
|
|
560
|
+
this.suite.testData = [...(this.suite.testData ?? []), testData];
|
|
561
|
+
}
|
|
562
|
+
if (
|
|
563
|
+
!relationData.relationElements.find(
|
|
564
|
+
(re) => re.paths[0] === accessPointId,
|
|
565
|
+
)
|
|
566
|
+
) {
|
|
567
|
+
relationData.relationElements.push(
|
|
568
|
+
createEmptyRelationElement(accessPointId, inferredColumns),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const assertion = new EqualToRelation();
|
|
574
|
+
assertion.id = 'assert_1';
|
|
575
|
+
const expectedRelElement = new RelationElement();
|
|
576
|
+
expectedRelElement.paths = [accessPointId];
|
|
577
|
+
expectedRelElement.columns = inferredColumns;
|
|
578
|
+
expectedRelElement.rows = [];
|
|
579
|
+
observe_RelationElement(expectedRelElement);
|
|
580
|
+
assertion.expected = expectedRelElement;
|
|
581
|
+
test.assertions = [assertion];
|
|
582
|
+
|
|
583
|
+
testSuite_addTest(this.suite, test, observerContext);
|
|
584
|
+
this.refreshTestDataState();
|
|
585
|
+
|
|
586
|
+
const testState = new DataProductTestState(this, test);
|
|
587
|
+
this.testStates.push(testState);
|
|
588
|
+
this.selectTestState = testState;
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
get result(): TESTABLE_RESULT {
|
|
593
|
+
if (this.runningSuiteState.isInProgress) {
|
|
594
|
+
return TESTABLE_RESULT.IN_PROGRESS;
|
|
595
|
+
}
|
|
596
|
+
if (this.testStates.length === 0) {
|
|
597
|
+
return TESTABLE_RESULT.DID_NOT_RUN;
|
|
598
|
+
}
|
|
599
|
+
if (
|
|
600
|
+
this.testStates.every((ts) => ts.testResultState.result === undefined)
|
|
601
|
+
) {
|
|
602
|
+
return TESTABLE_RESULT.DID_NOT_RUN;
|
|
603
|
+
}
|
|
604
|
+
let hasFailure = false;
|
|
605
|
+
let hasError = false;
|
|
606
|
+
for (const testState of this.testStates) {
|
|
607
|
+
const result = testState.testResultState.result;
|
|
608
|
+
if (result instanceof TestError) {
|
|
609
|
+
hasError = true;
|
|
610
|
+
} else if (
|
|
611
|
+
result instanceof TestExecuted &&
|
|
612
|
+
result.testExecutionStatus === TestExecutionStatus.FAIL
|
|
613
|
+
) {
|
|
614
|
+
hasFailure = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (hasError) {
|
|
618
|
+
return TESTABLE_RESULT.ERROR;
|
|
619
|
+
}
|
|
620
|
+
if (hasFailure) {
|
|
621
|
+
return TESTABLE_RESULT.FAILED;
|
|
622
|
+
}
|
|
623
|
+
return TESTABLE_RESULT.PASSED;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── Top-level testable state ────────────────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
export class DataProductTestableState {
|
|
630
|
+
readonly editorStore: EditorStore;
|
|
631
|
+
readonly dataProductEditorState: DataProductEditorState;
|
|
632
|
+
|
|
633
|
+
suiteStates: DataProductTestSuiteState[] = [];
|
|
634
|
+
selectedSuiteState: DataProductTestSuiteState | undefined;
|
|
635
|
+
runningAllTestsState = ActionState.create();
|
|
636
|
+
showCreateSuiteModal = false;
|
|
637
|
+
showCreateTestModal = false;
|
|
638
|
+
suiteToRename: TestSuite | undefined;
|
|
639
|
+
|
|
640
|
+
constructor(dataProductEditorState: DataProductEditorState) {
|
|
641
|
+
makeObservable(this, {
|
|
642
|
+
suiteStates: observable,
|
|
643
|
+
selectedSuiteState: observable,
|
|
644
|
+
showCreateSuiteModal: observable,
|
|
645
|
+
showCreateTestModal: observable,
|
|
646
|
+
suiteToRename: observable,
|
|
647
|
+
setSelectedSuiteState: action,
|
|
648
|
+
setShowCreateSuiteModal: action,
|
|
649
|
+
setShowCreateTestModal: action,
|
|
650
|
+
setSuiteToRename: action,
|
|
651
|
+
changeSuite: action,
|
|
652
|
+
createSuite: flow,
|
|
653
|
+
deleteSuite: action,
|
|
654
|
+
init: action,
|
|
655
|
+
runAllTests: flow,
|
|
656
|
+
});
|
|
657
|
+
this.editorStore = dataProductEditorState.editorStore;
|
|
658
|
+
this.dataProductEditorState = dataProductEditorState;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
get dataProduct(): DataProduct {
|
|
662
|
+
return this.dataProductEditorState.product;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
setSelectedSuiteState(val: DataProductTestSuiteState | undefined): void {
|
|
666
|
+
this.selectedSuiteState = val;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
setShowCreateSuiteModal(val: boolean): void {
|
|
670
|
+
this.showCreateSuiteModal = val;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
setShowCreateTestModal(val: boolean): void {
|
|
674
|
+
this.showCreateTestModal = val;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
setSuiteToRename(val: TestSuite | undefined): void {
|
|
678
|
+
this.suiteToRename = val;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
changeSuite(suite: TestSuite): void {
|
|
682
|
+
const suiteState = this.suiteStates.find((s) => s.suite === suite);
|
|
683
|
+
if (suiteState) {
|
|
684
|
+
this.selectedSuiteState = suiteState;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Build suite states from the DataProduct.tests array.
|
|
690
|
+
* Call this on init and after grammar→form roundtrip.
|
|
691
|
+
*/
|
|
692
|
+
init(): void {
|
|
693
|
+
const dp = this.dataProduct;
|
|
694
|
+
this.suiteStates = dp.tests.map(
|
|
695
|
+
(s) => new DataProductTestSuiteState(this.editorStore, this, s),
|
|
696
|
+
);
|
|
697
|
+
this.selectedSuiteState = this.suiteStates[0];
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Returns all graph ingest elements available in the model. */
|
|
701
|
+
get availableIngestSources(): IngestDefinition[] {
|
|
702
|
+
const graph = this.editorStore.graphManagerState.graph;
|
|
703
|
+
return graph.ingests;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Access points on the DataProduct being edited (used for test's accessPointId). */
|
|
707
|
+
get ownAccessPoints(): AccessPoint[] {
|
|
708
|
+
return this.dataProduct.accessPointGroups.flatMap((g) => g.accessPoints);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
getOwnAccessPointLabel(accessPointId: string): string {
|
|
712
|
+
const accessPoint = this.ownAccessPoints.find(
|
|
713
|
+
(candidate) => candidate.id === accessPointId,
|
|
714
|
+
);
|
|
715
|
+
return accessPoint
|
|
716
|
+
? getAccessPointDisplayLabel(accessPoint)
|
|
717
|
+
: accessPointId;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Create a new test suite with one initial test on the DataProduct.
|
|
722
|
+
* Test data is auto-seeded for the selected access point on the current
|
|
723
|
+
* DataProduct (no element picker required).
|
|
724
|
+
* Columns are inferred via the engine when possible.
|
|
725
|
+
*/
|
|
726
|
+
*createSuite(
|
|
727
|
+
suiteName: string,
|
|
728
|
+
testName: string,
|
|
729
|
+
accessPointId: string,
|
|
730
|
+
): GeneratorFn<string | undefined> {
|
|
731
|
+
const dp = this.dataProduct;
|
|
732
|
+
const observerContext =
|
|
733
|
+
this.editorStore.changeDetectionState.observerContext;
|
|
734
|
+
|
|
735
|
+
const suite = new DataProductTestSuite();
|
|
736
|
+
suite.id = suiteName;
|
|
737
|
+
|
|
738
|
+
// Try to infer columns from the access-point lambda
|
|
739
|
+
let inferredColumns: string[] = [];
|
|
740
|
+
try {
|
|
741
|
+
const cols = (yield inferDataProductItemColumns(
|
|
742
|
+
this.editorStore,
|
|
743
|
+
dp,
|
|
744
|
+
accessPointId,
|
|
745
|
+
)) as string[] | undefined;
|
|
746
|
+
if (cols) {
|
|
747
|
+
inferredColumns = cols;
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
// Column inference is best-effort; continue with empty columns
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Resolve INPUT sources via the graph (follows function calls, avoids self-refs)
|
|
754
|
+
const accessPointForSuite = dp.accessPointGroups
|
|
755
|
+
.flatMap((g) => g.accessPoints)
|
|
756
|
+
.find((ap) => ap.id === accessPointId);
|
|
757
|
+
const rawLambdaForSuite = accessPointForSuite
|
|
758
|
+
? getAccessPointLambda(accessPointForSuite)
|
|
759
|
+
: undefined;
|
|
760
|
+
|
|
761
|
+
suite.testData = [];
|
|
762
|
+
if (rawLambdaForSuite) {
|
|
763
|
+
const all =
|
|
764
|
+
(yield this.editorStore.graphManagerState.graphManager.collectAccessorsInRawLambda(
|
|
765
|
+
rawLambdaForSuite,
|
|
766
|
+
this.editorStore.graphManagerState.graph,
|
|
767
|
+
)) as Accessor[];
|
|
768
|
+
const externalAccessors = all.filter(
|
|
769
|
+
(accessor) =>
|
|
770
|
+
isIngestOrDataProductAccessor(accessor) &&
|
|
771
|
+
accessor.parentElement.path !== dp.path,
|
|
772
|
+
);
|
|
773
|
+
const byElement = new Map<string, Accessor[]>();
|
|
774
|
+
for (const acc of externalAccessors) {
|
|
775
|
+
const grp = byElement.get(acc.parentElement.path) ?? [];
|
|
776
|
+
grp.push(acc);
|
|
777
|
+
byElement.set(acc.parentElement.path, grp);
|
|
778
|
+
}
|
|
779
|
+
for (const [elementPath, accs] of byElement) {
|
|
780
|
+
const element =
|
|
781
|
+
this.editorStore.graphManagerState.graph.getNullableElement(
|
|
782
|
+
elementPath,
|
|
783
|
+
);
|
|
784
|
+
if (!element) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const resolver = new BaseDataResolver();
|
|
788
|
+
resolver.element = PackageableElementExplicitReference.create(element);
|
|
789
|
+
const relData = buildRelationElementsDataWithColumns(accs);
|
|
790
|
+
observe_RelationElementsData(relData);
|
|
791
|
+
resolver.data = relData;
|
|
792
|
+
suite.testData.push(resolver);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Fallback: no external sources resolved — seed a single resolver on current DP
|
|
796
|
+
if (suite.testData.length === 0) {
|
|
797
|
+
const testData = new BaseDataResolver();
|
|
798
|
+
testData.element = PackageableElementExplicitReference.create(dp);
|
|
799
|
+
const relData = new RelationElementsData();
|
|
800
|
+
relData.relationElements = [
|
|
801
|
+
createEmptyRelationElement(accessPointId, inferredColumns),
|
|
802
|
+
];
|
|
803
|
+
observe_RelationElementsData(relData);
|
|
804
|
+
testData.data = relData;
|
|
805
|
+
suite.testData = [testData];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Create one initial test with EqualToRelation assertion
|
|
809
|
+
const test = new DataProductAccessPointTest();
|
|
810
|
+
test.id = testName;
|
|
811
|
+
test.__parent = suite;
|
|
812
|
+
test.accessPointId = accessPointId;
|
|
813
|
+
|
|
814
|
+
const assertion = new EqualToRelation();
|
|
815
|
+
assertion.id = 'assert_1';
|
|
816
|
+
const expectedRelElement = new RelationElement();
|
|
817
|
+
expectedRelElement.paths = [accessPointId];
|
|
818
|
+
expectedRelElement.columns = inferredColumns;
|
|
819
|
+
expectedRelElement.rows = [];
|
|
820
|
+
observe_RelationElement(expectedRelElement);
|
|
821
|
+
assertion.expected = expectedRelElement;
|
|
822
|
+
test.assertions = [assertion];
|
|
823
|
+
|
|
824
|
+
suite.tests = [test];
|
|
825
|
+
|
|
826
|
+
const observed = observe_DataProductTestSuite(suite, observerContext);
|
|
827
|
+
addUniqueEntry(dp.tests, observed);
|
|
828
|
+
|
|
829
|
+
const suiteState = new DataProductTestSuiteState(
|
|
830
|
+
this.editorStore,
|
|
831
|
+
this,
|
|
832
|
+
observed,
|
|
833
|
+
);
|
|
834
|
+
this.suiteStates.push(suiteState);
|
|
835
|
+
this.selectedSuiteState = suiteState;
|
|
836
|
+
return undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
deleteSuite(suiteState: DataProductTestSuiteState): void {
|
|
840
|
+
const dp = this.dataProduct;
|
|
841
|
+
deleteEntry(dp.tests, suiteState.suite);
|
|
842
|
+
deleteEntry(this.suiteStates, suiteState);
|
|
843
|
+
if (this.selectedSuiteState === suiteState) {
|
|
844
|
+
this.selectedSuiteState = this.suiteStates[0];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
*runAllTests(): GeneratorFn<void> {
|
|
849
|
+
try {
|
|
850
|
+
this.runningAllTestsState.inProgress();
|
|
851
|
+
for (const suiteState of this.suiteStates) {
|
|
852
|
+
if (suiteState.suite.tests.length > 0) {
|
|
853
|
+
yield flowResult(suiteState.runSuite());
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
this.runningAllTestsState.complete();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
assertErrorThrown(error);
|
|
859
|
+
this.editorStore.applicationStore.notificationService.notifyError(error);
|
|
860
|
+
this.runningAllTestsState.fail();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|