Skip to content

Commit 2529852

Browse files
authored
chore(tesseract): cover every macro-generated bridge with object tests (cube-js#10842)
1 parent a2212fe commit 2529852

24 files changed

Lines changed: 1572 additions & 59 deletions

packages/cubejs-backend-native/src/bridge_test_exports.rs

Lines changed: 637 additions & 13 deletions
Large diffs are not rendered by default.
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Reference JS-side shapes for every macro-generated bridge.
2+
//
3+
// Each factory returns a fresh value that must satisfy:
4+
// 1. `NativeX::try_new` — required trait fields and required serde-static
5+
// fields must be present with the right types.
6+
// 2. Per-bridge invoke dispatcher in bridge_test_exports.rs — every
7+
// `field` getter must deserialize successfully, and every `call` method
8+
// stub must return a value that marshals back into the declared Rust
9+
// return type.
10+
//
11+
// Treat these factories as the executable contract documenting what
12+
// schema-compiler and friends are expected to hand to Tesseract for each
13+
// bridge. If a Rust trait gains a new method, the bridge_registry guard
14+
// will fire from the Rust side; if a JS shape stops matching, the invoke
15+
// check fires here.
16+
//
17+
// Keys are JS-side identifiers (post-`#[serde(rename)]`, camelCase for trait
18+
// methods that the macro auto-converts).
19+
20+
/* eslint-disable @typescript-eslint/no-empty-function */
21+
22+
export const memberSqlFn = (): unknown => () => 'sql';
23+
24+
export const filterGroupFixture = (): unknown => ({});
25+
export const filterParamsFixture = (): unknown => ({});
26+
export const securityContextFixture = (): unknown => ({});
27+
export const sqlUtilsFixture = (): unknown => ({});
28+
export const preAggregationObjFixture = (): unknown => ({});
29+
30+
export const geoItemFixture = (): unknown => ({
31+
sql: memberSqlFn(),
32+
});
33+
34+
export const structWithSqlMemberFixture = (): unknown => ({
35+
sql: memberSqlFn(),
36+
});
37+
38+
// CaseElseItem.label -> StringOrSql; deserializer tries String first.
39+
export const caseElseItemFixture = (): unknown => ({
40+
label: 'else',
41+
});
42+
43+
export const caseItemFixture = (): unknown => ({
44+
sql: memberSqlFn(),
45+
label: 'when_label',
46+
});
47+
48+
export const caseDefinitionFixture = (): unknown => ({
49+
when: [caseItemFixture()],
50+
// CaseDefinition.else_label is renamed to "else" via #[nbridge(rename = "else")].
51+
else: caseElseItemFixture(),
52+
});
53+
54+
export const caseSwitchElseItemFixture = (): unknown => ({
55+
sql: memberSqlFn(),
56+
});
57+
58+
export const caseSwitchItemFixture = (): unknown => ({
59+
value: 'v',
60+
sql: memberSqlFn(),
61+
});
62+
63+
export const caseSwitchDefinitionFixture = (): unknown => ({
64+
switch: memberSqlFn(),
65+
when: [caseSwitchItemFixture()],
66+
// CaseSwitchDefinition.else_sql is renamed to "else" via #[nbridge(rename = "else")].
67+
else: caseSwitchElseItemFixture(),
68+
});
69+
70+
export const memberOrderByFixture = (): unknown => ({
71+
sql: memberSqlFn(),
72+
dir: 'asc',
73+
});
74+
75+
export const memberDefinitionFixture = (): unknown => ({
76+
type: 'dimension',
77+
// sql is optional
78+
});
79+
80+
export const segmentDefinitionFixture = (): unknown => ({
81+
sql: memberSqlFn(),
82+
// segment_type, owned_by_cube optional
83+
});
84+
85+
export const joinItemDefinitionFixture = (): unknown => ({
86+
relationship: 'many_to_one',
87+
sql: memberSqlFn(),
88+
});
89+
90+
export const joinItemFixture = (): unknown => ({
91+
from: 'orders',
92+
to: 'users',
93+
originalFrom: 'orders',
94+
originalTo: 'users',
95+
join: joinItemDefinitionFixture(),
96+
});
97+
98+
export const joinDefinitionFixture = (): unknown => ({
99+
root: 'orders',
100+
multiplicationFactor: {},
101+
joins: [joinItemFixture()],
102+
});
103+
104+
export const joinGraphFixture = (): unknown => ({
105+
buildJoin: () => joinDefinitionFixture(),
106+
});
107+
108+
export const granularityDefinitionFixture = (): unknown => ({
109+
interval: '1 day',
110+
// origin, offset optional
111+
sql: memberSqlFn(),
112+
});
113+
114+
export const timeShiftDefinitionFixture = (): unknown => ({
115+
// all static optional, sql optional
116+
sql: memberSqlFn(),
117+
interval: '1 day',
118+
type: 'prior',
119+
name: 'last_day',
120+
});
121+
122+
export const preAggregationTimeDimensionFixture = (): unknown => ({
123+
granularity: 'day',
124+
dimension: memberSqlFn(),
125+
});
126+
127+
export const preAggregationDescriptionFixture = (): unknown => ({
128+
name: 'main',
129+
type: 'rollup',
130+
// granularity, sqlAlias, external, allowNonStrictDateRangeMatch optional
131+
// measure_references, dimension_references, etc — all optional getters
132+
});
133+
134+
export const cubeDefinitionFixture = (): unknown => ({
135+
name: 'Orders',
136+
// sqlAlias, isView, isCalendar, joinMap optional
137+
// sql_table, sql optional getters
138+
});
139+
140+
export const dimensionDefinitionFixture = (): unknown => ({
141+
type: 'string',
142+
// owned_by_cube, multi_stage, etc. — all optional
143+
// sql/case/latitude/longitude/time_shift/mask_sql — all optional getters
144+
});
145+
146+
export const measureDefinitionFixture = (): unknown => ({
147+
type: 'count',
148+
// owned_by_cube, multi_stage, reduce_by_references, etc. — all optional
149+
// sql/case/filters/drill_filters/order_by/mask_sql — all optional getters
150+
});
151+
152+
export const expressionStructFixture = (): unknown => ({
153+
type: 'PatchMeasure',
154+
// sourceMeasure, replaceAggregationType, addFilters — all optional
155+
});
156+
157+
export const memberExpressionDefinitionFixture = (): unknown => ({
158+
// expressionName, name, cubeName, definition — all optional
159+
// expression — required, MemberExpressionExpressionDef tries MemberSql first
160+
expression: memberSqlFn(),
161+
});
162+
163+
// CubeEvaluator: every method is a `call`. Each stub must return a value
164+
// that marshals into the declared Rust return type. The cascade is real —
165+
// measureByPath has to hand back something that NativeMeasureDefinition
166+
// can wrap, and so on.
167+
export const cubeEvaluatorFixture = (): unknown => ({
168+
primaryKeys: {},
169+
parsePath: () => [],
170+
measureByPath: () => measureDefinitionFixture(),
171+
dimensionByPath: () => dimensionDefinitionFixture(),
172+
segmentByPath: () => segmentDefinitionFixture(),
173+
cubeFromPath: () => cubeDefinitionFixture(),
174+
isMeasure: () => false,
175+
isDimension: () => false,
176+
isSegment: () => false,
177+
cubeExists: () => false,
178+
resolveGranularity: () => granularityDefinitionFixture(),
179+
preAggregationsForCubeAsArray: () => [preAggregationDescriptionFixture()],
180+
preAggregationDescriptionByName: () => preAggregationDescriptionFixture(),
181+
// evaluate_rollup_references is invoke-skipped on the Rust side because
182+
// its `Rc<dyn MemberSql>` argument has no auto-default, but the JS object
183+
// still needs the key for try_new's has_field check.
184+
evaluateRollupReferences: () => [],
185+
});
186+
187+
export const driverToolsFixture = (): unknown => ({
188+
convertTz: () => 'tz',
189+
timeGroupedColumn: () => 'col',
190+
sqlTemplates: () => ({}),
191+
timestampPrecision: () => 6,
192+
timeStampCast: () => 'ts',
193+
dateTimeCast: () => 'dt',
194+
inDbTimeZone: () => 'tz',
195+
getAllocatedParams: () => [],
196+
subtractInterval: () => 'd',
197+
addInterval: () => 'd',
198+
intervalString: () => 's',
199+
addTimestampInterval: () => 'd',
200+
intervalAndMinimalTimeUnit: () => ['1', 'day'],
201+
hllInit: () => 'h',
202+
hllMerge: () => 'h',
203+
hllCardinalityMerge: () => 'h',
204+
countDistinctApprox: () => 'c',
205+
supportGeneratedSeriesForCustomTd: () => false,
206+
dateBin: () => 'b',
207+
});
208+
209+
export const baseToolsFixture = (): unknown => ({
210+
driverTools: () => driverToolsFixture(),
211+
sqlTemplates: () => ({}),
212+
sqlUtilsForRust: () => sqlUtilsFixture(),
213+
generateTimeSeries: () => [],
214+
generateCustomTimeSeries: () => [],
215+
getAllocatedParams: () => [],
216+
allCubeMembers: () => [],
217+
intervalAndMinimalTimeUnit: () => ['1', 'day'],
218+
getPreAggregationByName: () => preAggregationObjFixture(),
219+
preAggregationTableName: () => 'pre_aggr_table',
220+
joinTreeForHints: () => joinDefinitionFixture(),
221+
});
222+
223+
export const baseQueryOptionsFixture = (): unknown => ({
224+
// Static fields
225+
exportAnnotatedSql: false,
226+
disableExternalPreAggregations: false,
227+
// Optional static — omitted intentionally; serde fills None.
228+
//
229+
// Trait fields (all `field, optional, vec` except the four required ones)
230+
cubeEvaluator: cubeEvaluatorFixture(),
231+
baseTools: baseToolsFixture(),
232+
joinGraph: joinGraphFixture(),
233+
securityContext: securityContextFixture(),
234+
// Optional vec fields can be omitted
235+
});
236+
237+
export type BridgeFixtureFactory = () => unknown;
238+
239+
export const FIXTURES: Record<string, BridgeFixtureFactory> = {
240+
baseQueryOptions: baseQueryOptionsFixture,
241+
baseTools: baseToolsFixture,
242+
caseDefinition: caseDefinitionFixture,
243+
caseElseItem: caseElseItemFixture,
244+
caseItem: caseItemFixture,
245+
caseSwitchDefinition: caseSwitchDefinitionFixture,
246+
caseSwitchElseItem: caseSwitchElseItemFixture,
247+
caseSwitchItem: caseSwitchItemFixture,
248+
cubeDefinition: cubeDefinitionFixture,
249+
cubeEvaluator: cubeEvaluatorFixture,
250+
dimensionDefinition: dimensionDefinitionFixture,
251+
driverTools: driverToolsFixture,
252+
expressionStruct: expressionStructFixture,
253+
filterGroup: filterGroupFixture,
254+
filterParams: filterParamsFixture,
255+
geoItem: geoItemFixture,
256+
granularityDefinition: granularityDefinitionFixture,
257+
joinDefinition: joinDefinitionFixture,
258+
joinGraph: joinGraphFixture,
259+
joinItem: joinItemFixture,
260+
joinItemDefinition: joinItemDefinitionFixture,
261+
measureDefinition: measureDefinitionFixture,
262+
memberDefinition: memberDefinitionFixture,
263+
memberExpressionDefinition: memberExpressionDefinitionFixture,
264+
memberOrderBy: memberOrderByFixture,
265+
preAggregationDescription: preAggregationDescriptionFixture,
266+
preAggregationObj: preAggregationObjFixture,
267+
preAggregationTimeDimension: preAggregationTimeDimensionFixture,
268+
securityContext: securityContextFixture,
269+
segmentDefinition: segmentDefinitionFixture,
270+
sqlUtils: sqlUtilsFixture,
271+
structWithSqlMember: structWithSqlMemberFixture,
272+
timeShiftDefinition: timeShiftDefinitionFixture,
273+
};

packages/cubejs-backend-native/test/bridge/helpers.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,81 @@ export function invokeFilterParamsCallback(
5656
): string {
5757
return native.__testBridgeInvokeFilterParamsCallback(fn, args);
5858
}
59+
60+
export type BridgeFieldKind = 'field' | 'call' | 'static';
61+
62+
export interface BridgeFieldMeta {
63+
name: string;
64+
jsName: string;
65+
kind: BridgeFieldKind;
66+
optional: boolean;
67+
vec: boolean;
68+
}
69+
70+
export function listBridgeFields(name: string): BridgeFieldMeta[] {
71+
if (!bridgeHarnessAvailable) {
72+
throw new Error(
73+
'Bridge test harness is not built. Rebuild with `yarn native:build-debug-bridge-tests`.'
74+
);
75+
}
76+
return native.__testBridgeListFields(name);
77+
}
78+
79+
export function listBridgeNames(): string[] {
80+
if (!bridgeHarnessAvailable) {
81+
throw new Error(
82+
'Bridge test harness is not built. Rebuild with `yarn native:build-debug-bridge-tests`.'
83+
);
84+
}
85+
return native.__testBridgeListBridgeNames();
86+
}
87+
88+
export function parseBridge(name: string, obj: unknown): void {
89+
if (!bridgeHarnessAvailable) {
90+
throw new Error(
91+
'Bridge test harness is not built. Rebuild with `yarn native:build-debug-bridge-tests`.'
92+
);
93+
}
94+
native.__testBridgeParse(name, obj);
95+
}
96+
97+
export function fieldNames(meta: BridgeFieldMeta[]): string[] {
98+
return meta.map((m) => m.name).sort();
99+
}
100+
101+
export type InvokeStatus =
102+
| { status: 'ok' }
103+
| { status: 'error'; message: string }
104+
| { status: 'skipped'; reason: string };
105+
106+
export type InvokeResult = Record<string, InvokeStatus>;
107+
108+
export function invokeBridge(name: string, fixture: unknown): InvokeResult {
109+
if (!bridgeHarnessAvailable) {
110+
throw new Error(
111+
'Bridge test harness is not built. Rebuild with `yarn native:build-debug-bridge-tests`.'
112+
);
113+
}
114+
return native.__testBridgeInvoke(name, fixture);
115+
}
116+
117+
/**
118+
* Asserts every recorded invocation is `ok` or `skipped`. Errors surface
119+
* the offending field, the Rust-side message, and the kind of failure so
120+
* they read naturally in CI logs. Skipped entries are allowed because some
121+
* call-methods take Rust-only argument types (e.g. `Rc<dyn MemberSql>`)
122+
* that have no auto-default.
123+
*/
124+
export function expectAllInvocationsOk(result: InvokeResult): void {
125+
const failures: string[] = [];
126+
for (const [field, entry] of Object.entries(result)) {
127+
if (entry.status === 'error') {
128+
failures.push(`${field}: error: ${entry.message}`);
129+
}
130+
}
131+
if (failures.length > 0) {
132+
throw new Error(
133+
`Bridge invocation failed for ${failures.length} field(s):\n ${failures.join('\n ')}`
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)