Skip to content

Commit 8d8d2a9

Browse files
authored
chore(tesseract): add bridge regression test harness (cube-js#10838)
1 parent df1479d commit 8d8d2a9

17 files changed

Lines changed: 1332 additions & 0 deletions

.github/workflows/bridge-tests.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Bridge tests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.github/workflows/bridge-tests.yml'
7+
- 'rust/cube/cubesqlplanner/**'
8+
- 'rust/cube/cubenativeutils/**'
9+
- 'packages/cubejs-backend-native/src/bridge_test_exports.rs'
10+
- 'packages/cubejs-backend-native/test/bridge/**'
11+
- 'packages/cubejs-backend-native/jest-bridge.config.js'
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
bridge-tests:
18+
runs-on: ubuntu-24.04
19+
timeout-minutes: 40
20+
name: Bridge tests (debug, --features bridge-test-harness)
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
- name: Install Rust
26+
uses: actions-rust-lang/setup-rust-toolchain@v1
27+
with:
28+
toolchain: 1.90.0
29+
rustflags: ""
30+
cache: false
31+
- uses: Swatinem/rust-cache@v2
32+
with:
33+
workspaces: ./packages/cubejs-backend-native -> target
34+
key: bridge-tests-${{ runner.OS }}-x86_64-unknown-linux-gnu
35+
shared-key: bridge-tests-${{ runner.OS }}-x86_64-unknown-linux-gnu
36+
- name: Install Node.js
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: 22.x
40+
- name: Set Yarn version
41+
run: yarn policies set-version v1.22.22
42+
- name: Get yarn cache directory path
43+
id: yarn-cache-dir-path
44+
run: echo "dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
45+
- name: Restore yarn cache
46+
uses: actions/cache@v4
47+
with:
48+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
49+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
50+
restore-keys: |
51+
${{ runner.os }}-yarn-
52+
- name: Yarn install
53+
uses: nick-fields/retry@v3
54+
env:
55+
CUBESTORE_SKIP_POST_INSTALL: true
56+
with:
57+
max_attempts: 3
58+
retry_on: error
59+
retry_wait_seconds: 15
60+
timeout_minutes: 20
61+
command: yarn install --frozen-lockfile
62+
- name: Build native (debug, --features bridge-test-harness)
63+
working-directory: ./packages/cubejs-backend-native
64+
run: yarn run native:build-debug-bridge-tests
65+
- name: TypeScript compile (lerna, root)
66+
run: yarn tsc
67+
- name: Run bridge tests
68+
working-directory: ./packages/cubejs-backend-native
69+
run: yarn jest --config jest-bridge.config.js --forceExit

packages/cubejs-backend-native/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ neon-debug = []
5757
neon-entrypoint = []
5858
python = ["pyo3", "pyo3-asyncio"]
5959
async-log = ["log_nonblock"]
60+
# Exposes `__testBridge_*` endpoints used by the bridge regression test
61+
# harness. Off by default — never enable in production builds.
62+
bridge-test-harness = []
6063

6164
[lints.clippy]
6265
too_many_arguments = "allow"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const base = require('../../jest.base.config');
2+
3+
// Dedicated config for the Tesseract bridge regression test harness.
4+
//
5+
// Intentionally does NOT use `test/snapshotResolver.js` from the main jest
6+
// config — that resolver wipes the entire `test/__snapshots__/` directory
7+
// when running on a non-python build (see `isFallbackBuild()` check there).
8+
// Bridge tests don't use snapshots, but the side effect would still nuke
9+
// unrelated snapshots in this package.
10+
11+
/** @type {import('jest').Config} */
12+
module.exports = {
13+
...base,
14+
rootDir: '.',
15+
roots: [
16+
'<rootDir>/dist/test/bridge/'
17+
],
18+
collectCoverage: false,
19+
};

packages/cubejs-backend-native/jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ module.exports = {
1010
roots: [
1111
'<rootDir>/dist/test/'
1212
],
13+
// Bridge tests live under `dist/test/bridge/` and require a native module
14+
// built with `--features bridge-test-harness`. They are run separately via
15+
// `yarn test:bridge` (see jest-bridge.config.js); excluding them here keeps
16+
// `yarn unit` working with a regular debug build.
17+
testPathIgnorePatterns: ['/dist/test/bridge/'],
1318
snapshotResolver: '<rootDir>/test/snapshotResolver.js',
1419
};

packages/cubejs-backend-native/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
"native:build-release": "npm run native:build -- --release",
1919
"native:build-debug-python": "npm run native:build -- --features python",
2020
"native:build-release-python": "npm run native:build -- --release --features python",
21+
"native:build-debug-bridge-tests": "npm run native:build -- --features bridge-test-harness",
22+
"native:build-release-bridge-tests": "npm run native:build -- --release --features bridge-test-harness",
2123
"postinstall": "post-installer || echo 'Your system is not supported by @cubejs-backend/native, some feature will be unavailable.'",
2224
"test:server": "CUBEJS_NATIVE_INTERNAL_DEBUG=true CUBESQL_LOG_LEVEL=trace CUBESQL_PG_PORT=5555 node dist/test/server.js",
2325
"test:server:stream": "CUBESQL_STREAM_MODE=true CUBESQL_LOG_LEVEL=error CUBESQL_PG_PORT=5555 node dist/test/server.js",
2426
"test:python": "CUBEJS_NATIVE_INTERNAL_DEBUG=true CUBESQL_LOG_LEVEL=trace CUBESQL_PG_PORT=5555 node dist/test/python.js",
2527
"unit": "jest --forceExit",
2628
"test:unit": "yarn run unit",
29+
"test:bridge": "npm run native:build-debug-bridge-tests && npm run tsc && jest --config jest-bridge.config.js --forceExit",
2730
"test:cargo": "cargo test",
2831
"bench": "jest --config jest-bench.config.js --forceExit",
2932
"lint": "eslint test/ js/ --ext .ts",
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//! Test endpoints for the Tesseract bridge layer.
2+
//!
3+
//! These functions are exported on the native module under names prefixed with
4+
//! `__testBridge` (e.g. `__testBridgeCompileMemberSql`). They drive real V8
5+
//! through the production bridge code so
6+
//! that bridge logic can be regression-tested at the unit level rather than
7+
//! only via end-to-end JS planner tests.
8+
//!
9+
//! Stub implementations for trait dependencies (e.g. `BaseTools`) live in this
10+
//! module; they should fail loudly when an unsupported code path is exercised.
11+
12+
use cubenativeutils::wrappers::neon::neon_guarded_funcion_call;
13+
use cubenativeutils::wrappers::object::{NativeArray, NativeFunction, NativeStruct, NativeType};
14+
use cubenativeutils::wrappers::serializer::NativeSerialize;
15+
use cubenativeutils::wrappers::{inner_types::InnerTypes, NativeContextHolder, NativeObjectHandle};
16+
use cubenativeutils::CubeError;
17+
use cubesqlplanner::cube_bridge::base_tools::BaseTools;
18+
use cubesqlplanner::cube_bridge::driver_tools::DriverTools;
19+
use cubesqlplanner::cube_bridge::filter_params_callback::{
20+
FilterParamsCallback, NativeFilterParamsCallback,
21+
};
22+
use cubesqlplanner::cube_bridge::join_definition::JoinDefinition;
23+
use cubesqlplanner::cube_bridge::join_hints::JoinHintItem;
24+
use cubesqlplanner::cube_bridge::member_sql::{
25+
FilterGroupItem, FilterParamsItem, MemberSql, NativeMemberSql, SqlTemplate, SqlTemplateArgs,
26+
};
27+
use cubesqlplanner::cube_bridge::pre_aggregation_obj::PreAggregationObj;
28+
use cubesqlplanner::cube_bridge::security_context::{NativeSecurityContext, SecurityContext};
29+
use cubesqlplanner::cube_bridge::sql_templates_render::SqlTemplatesRender;
30+
use cubesqlplanner::cube_bridge::sql_utils::SqlUtils;
31+
use neon::prelude::*;
32+
use std::any::Any;
33+
use std::rc::Rc;
34+
35+
struct StubBaseTools;
36+
37+
fn stub_err(method: &str) -> CubeError {
38+
CubeError::internal(format!(
39+
"StubBaseTools::{} called from bridge test harness — \
40+
this test path requires a real BaseTools implementation",
41+
method
42+
))
43+
}
44+
45+
impl BaseTools for StubBaseTools {
46+
fn as_any(self: Rc<Self>) -> Rc<dyn Any> {
47+
self
48+
}
49+
fn driver_tools(&self, _external: bool) -> Result<Rc<dyn DriverTools>, CubeError> {
50+
Err(stub_err("driver_tools"))
51+
}
52+
fn sql_templates(&self) -> Result<Rc<dyn SqlTemplatesRender>, CubeError> {
53+
Err(stub_err("sql_templates"))
54+
}
55+
fn sql_utils_for_rust(&self) -> Result<Rc<dyn SqlUtils>, CubeError> {
56+
Err(stub_err("sql_utils_for_rust"))
57+
}
58+
fn generate_time_series(
59+
&self,
60+
_granularity: String,
61+
_date_range: Vec<String>,
62+
) -> Result<Vec<Vec<String>>, CubeError> {
63+
Err(stub_err("generate_time_series"))
64+
}
65+
fn generate_custom_time_series(
66+
&self,
67+
_granularity: String,
68+
_date_range: Vec<String>,
69+
_origin: String,
70+
) -> Result<Vec<Vec<String>>, CubeError> {
71+
Err(stub_err("generate_custom_time_series"))
72+
}
73+
fn get_allocated_params(&self) -> Result<Vec<String>, CubeError> {
74+
Err(stub_err("get_allocated_params"))
75+
}
76+
fn all_cube_members(&self, _path: String) -> Result<Vec<String>, CubeError> {
77+
Err(stub_err("all_cube_members"))
78+
}
79+
fn interval_and_minimal_time_unit(&self, _interval: String) -> Result<Vec<String>, CubeError> {
80+
Err(stub_err("interval_and_minimal_time_unit"))
81+
}
82+
fn get_pre_aggregation_by_name(
83+
&self,
84+
_cube_name: String,
85+
_name: String,
86+
) -> Result<Rc<dyn PreAggregationObj>, CubeError> {
87+
Err(stub_err("get_pre_aggregation_by_name"))
88+
}
89+
fn pre_aggregation_table_name(
90+
&self,
91+
_cube_name: String,
92+
_name: String,
93+
) -> Result<String, CubeError> {
94+
Err(stub_err("pre_aggregation_table_name"))
95+
}
96+
fn join_tree_for_hints(
97+
&self,
98+
_hints: Vec<JoinHintItem>,
99+
) -> Result<Rc<dyn JoinDefinition>, CubeError> {
100+
Err(stub_err("join_tree_for_hints"))
101+
}
102+
}
103+
104+
fn handles_to_array<IT: InnerTypes>(
105+
items: Vec<NativeObjectHandle<IT>>,
106+
context: NativeContextHolder<IT>,
107+
) -> Result<NativeObjectHandle<IT>, CubeError> {
108+
let arr = context.empty_array()?;
109+
for (i, item) in items.into_iter().enumerate() {
110+
arr.set(i as u32, item)?;
111+
}
112+
Ok(NativeObjectHandle::new(arr.into_object()))
113+
}
114+
115+
fn template_to_native<IT: InnerTypes>(
116+
template: &SqlTemplate,
117+
context: NativeContextHolder<IT>,
118+
) -> Result<NativeObjectHandle<IT>, CubeError> {
119+
match template {
120+
SqlTemplate::String(s) => s.to_native(context),
121+
SqlTemplate::StringVec(strings) => strings.to_native(context),
122+
}
123+
}
124+
125+
fn filter_params_to_native<IT: InnerTypes>(
126+
items: &[FilterParamsItem],
127+
context: NativeContextHolder<IT>,
128+
) -> Result<NativeObjectHandle<IT>, CubeError> {
129+
let serialized = items
130+
.iter()
131+
.map(|itm| itm.to_native(context.clone()))
132+
.collect::<Result<Vec<_>, _>>()?;
133+
handles_to_array(serialized, context)
134+
}
135+
136+
fn filter_group_to_native<IT: InnerTypes>(
137+
group: &FilterGroupItem,
138+
context: NativeContextHolder<IT>,
139+
) -> Result<NativeObjectHandle<IT>, CubeError> {
140+
let result = context.empty_struct()?;
141+
result.set_field(
142+
"filter_params",
143+
filter_params_to_native(&group.filter_params, context.clone())?,
144+
)?;
145+
Ok(NativeObjectHandle::new(result.into_object()))
146+
}
147+
148+
fn args_to_native<IT: InnerTypes>(
149+
args: &SqlTemplateArgs,
150+
context: NativeContextHolder<IT>,
151+
) -> Result<NativeObjectHandle<IT>, CubeError> {
152+
let result = context.empty_struct()?;
153+
result.set_field(
154+
"symbol_paths",
155+
args.symbol_paths.to_native(context.clone())?,
156+
)?;
157+
result.set_field(
158+
"filter_params",
159+
filter_params_to_native(&args.filter_params, context.clone())?,
160+
)?;
161+
let groups = args
162+
.filter_groups
163+
.iter()
164+
.map(|g| filter_group_to_native(g, context.clone()))
165+
.collect::<Result<Vec<_>, _>>()?;
166+
result.set_field("filter_groups", handles_to_array(groups, context.clone())?)?;
167+
let security_context = context.empty_struct()?;
168+
security_context.set_field(
169+
"values",
170+
args.security_context.values.to_native(context.clone())?,
171+
)?;
172+
result.set_field(
173+
"security_context",
174+
NativeObjectHandle::new(security_context.into_object()),
175+
)?;
176+
Ok(NativeObjectHandle::new(result.into_object()))
177+
}
178+
179+
fn compile_member_sql_inner<IT: InnerTypes>(
180+
context_holder: NativeContextHolder<IT>,
181+
js_fn: NativeObjectHandle<IT>,
182+
security_context_obj: NativeObjectHandle<IT>,
183+
) -> Result<NativeObjectHandle<IT>, CubeError> {
184+
let member_sql = NativeMemberSql::try_new(js_fn)?;
185+
let security_context: Rc<dyn SecurityContext> =
186+
Rc::new(NativeSecurityContext::try_new(security_context_obj)?);
187+
let base_tools: Rc<dyn BaseTools> = Rc::new(StubBaseTools);
188+
189+
let (template, args) = member_sql.compile_template_sql(base_tools, security_context)?;
190+
191+
let result = context_holder.empty_struct()?;
192+
result.set_field(
193+
"template",
194+
template_to_native(&template, context_holder.clone())?,
195+
)?;
196+
result.set_field("args", args_to_native(&args, context_holder.clone())?)?;
197+
Ok(NativeObjectHandle::new(result.into_object()))
198+
}
199+
200+
fn compile_member_sql(cx: FunctionContext) -> JsResult<JsValue> {
201+
neon_guarded_funcion_call(
202+
cx,
203+
|context_holder: NativeContextHolder<_>,
204+
js_fn: NativeObjectHandle<_>,
205+
security_context_obj: NativeObjectHandle<_>| {
206+
compile_member_sql_inner(context_holder, js_fn, security_context_obj)
207+
},
208+
)
209+
}
210+
211+
fn parse_args_names_inner<IT: InnerTypes>(
212+
context_holder: NativeContextHolder<IT>,
213+
js_fn: NativeObjectHandle<IT>,
214+
) -> Result<NativeObjectHandle<IT>, CubeError> {
215+
let func = js_fn.to_function()?;
216+
let names = func.args_names()?;
217+
names.to_native(context_holder)
218+
}
219+
220+
fn parse_args_names(cx: FunctionContext) -> JsResult<JsValue> {
221+
neon_guarded_funcion_call(
222+
cx,
223+
|context_holder: NativeContextHolder<_>, js_fn: NativeObjectHandle<_>| {
224+
parse_args_names_inner(context_holder, js_fn)
225+
},
226+
)
227+
}
228+
229+
fn invoke_filter_params_callback_inner<IT: InnerTypes>(
230+
context_holder: NativeContextHolder<IT>,
231+
js_fn: NativeObjectHandle<IT>,
232+
args: Vec<String>,
233+
) -> Result<NativeObjectHandle<IT>, CubeError> {
234+
let callback = NativeFilterParamsCallback::new(js_fn);
235+
let result = callback.call(&args)?;
236+
result.to_native(context_holder)
237+
}
238+
239+
fn invoke_filter_params_callback(cx: FunctionContext) -> JsResult<JsValue> {
240+
neon_guarded_funcion_call(
241+
cx,
242+
|context_holder: NativeContextHolder<_>,
243+
js_fn: NativeObjectHandle<_>,
244+
args: Vec<String>| {
245+
invoke_filter_params_callback_inner(context_holder, js_fn, args)
246+
},
247+
)
248+
}
249+
250+
pub fn register_module(cx: &mut ModuleContext) -> NeonResult<()> {
251+
cx.export_function("__testBridgeCompileMemberSql", compile_member_sql)?;
252+
cx.export_function("__testBridgeParseArgsNames", parse_args_names)?;
253+
cx.export_function(
254+
"__testBridgeInvokeFilterParamsCallback",
255+
invoke_filter_params_callback,
256+
)?;
257+
Ok(())
258+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
extern crate findshlibs;
55

66
pub mod auth;
7+
#[cfg(feature = "bridge-test-harness")]
8+
pub mod bridge_test_exports;
79
pub mod channel;
810
pub mod config;
911
pub mod cross;

0 commit comments

Comments
 (0)