11// Runs the SDK end-to-end scenario in sdk/e2e against Lithe's Rust Core.
22//
33// node scripts/sdk-e2e.mjs [--ref <commit|branch>] [--source <Lithe git URL or path>]
4+ // node scripts/sdk-e2e.mjs --source <local Lithe root> --working-tree --build-workflows
45//
56// Core lives in the Lithe repository, so the scenario is copied into
67// rust/lithe-core/tests/ of a disposable Lithe checkout in .artifacts/lithe
1516// The Rust sample backend is built with Cargo unless
1617// LITHE_PLUGIN_SDK_RUST_SAMPLE names a built sample-backend executable.
1718import { spawn } from 'node:child_process' ;
18- import { access , copyFile , mkdir , readdir } from 'node:fs/promises' ;
19+ import { access , copyFile , mkdir , readdir , cp , realpath , readFile } from 'node:fs/promises' ;
20+ import { fileURLToPath } from 'node:url' ;
1921import { join , resolve } from 'node:path' ;
2022import { parseArgs } from 'node:util' ;
2123import { readLock , repository } from './contract.mjs' ;
@@ -24,6 +26,15 @@ const work = join(repository, '.artifacts', 'lithe');
2426const scenario = join ( repository , 'sdk' , 'e2e' ) ;
2527const tests = [ 'plugin_sdk_node' , 'plugin_sdk_java' , 'plugin_sdk_rust' ] ;
2628
29+ /** Once the contract mirror includes protocol 6, the current native workflows are mandatory. */
30+ export function includesNativeWorkflows ( schema , requested = false ) {
31+ const versions = schema ?. properties ?. requires ?. properties ?. buildProviders ?. enum ;
32+ if ( ! Array . isArray ( versions ) || versions . some ( version => ! Number . isInteger ( version ) || version < 1 ) ) {
33+ throw new Error ( 'The pinned contract must enumerate buildProviders versions.' ) ;
34+ }
35+ return requested || versions . includes ( 6 ) ;
36+ }
37+
2738function run ( command , args , options = { } ) {
2839 return new Promise ( ( resolvePromise , reject ) => {
2940 const child = spawn ( command , args , { stdio : 'inherit' , ...options } ) ;
@@ -35,14 +46,15 @@ function run(command, args, options = {}) {
3546 } ) ;
3647}
3748
38- function output ( command , args ) {
49+ function output ( command , args , trim = true ) {
3950 return new Promise ( ( resolvePromise , reject ) => {
40- const child = spawn ( command , args , { stdio : [ 'ignore' , 'pipe' , 'inherit' ] } ) ;
51+ const child = spawn ( command , args , { stdio : [ 'ignore' , 'pipe' , 'inherit' ] ,
52+ timeout : 15000 , killSignal : 'SIGKILL' , env : { ...process . env , GIT_OPTIONAL_LOCKS : '0' } } ) ;
4153 let text = '' ;
4254 child . stdout . on ( 'data' , chunk => { text += chunk ; } ) ;
4355 child . on ( 'error' , reject ) ;
4456 child . on ( 'close' , code => code === 0
45- ? resolvePromise ( text . trim ( ) )
57+ ? resolvePromise ( trim ? text . trim ( ) : text )
4658 : reject ( new Error ( `${ command } ${ args . join ( ' ' ) } failed (exit ${ code } )` ) ) ) ;
4759 } ) ;
4860}
@@ -64,12 +76,37 @@ async function checkoutCore(source, ref) {
6476 await run ( 'git' , [ '-C' , work , 'config' , 'remote.origin.url' , source ] ) ;
6577 await run ( 'git' , [ '-C' , work , 'fetch' , '-q' , '--depth' , '1' , '--filter=blob:none' , 'origin' , ref ] ) ;
6678 await run ( 'git' , [ '-C' , work , 'checkout' , '-q' , '--force' , '--detach' , 'FETCH_HEAD' ] ) ;
67- // Scenario files copied by an earlier run are untracked; drop them so a file
68- // removed from sdk/e2e does not linger .
69- await run ( 'git' , [ '-C' , work , 'clean' , '-q' , '-f' , '-d' , '--' , 'rust/lithe-core/tests ' ] ) ;
79+ // The checkout is disposable. Drop prior scenario/working-tree inputs so a
80+ // removed source cannot linger; ignored Cargo caches are left alone .
81+ await run ( 'git' , [ '-C' , work , 'clean' , '-q' , '-f' , '-d' , '--' , 'rust' , 'shared '] ) ;
7082 return output ( 'git' , [ '-C' , work , 'rev-parse' , 'HEAD' ] ) ;
7183}
7284
85+ /** Replays only rust/ and shared/ from an explicitly selected local working tree. */
86+ export async function copyWorkingTree ( source , destination ) {
87+ const directory = await realpath ( source ) ;
88+ const root = await output ( 'git' , [ '-C' , directory , 'rev-parse' , '--show-toplevel' ] ) ;
89+ if ( await realpath ( root ) !== directory ) throw new Error ( '--working-tree source must be the repository root.' ) ;
90+ const patch = await output ( 'git' , [ '-C' , directory , 'diff' , '--no-ext-diff' , '--no-textconv' , '--binary' , 'HEAD' , '--' , 'rust' , 'shared' ] , false ) ;
91+ if ( patch ) {
92+ await new Promise ( ( resolvePromise , reject ) => {
93+ const child = spawn ( 'git' , [ '-C' , destination , 'apply' , '--binary' , '-' ] , {
94+ stdio : [ 'pipe' , 'inherit' , 'inherit' ] , timeout : 15000 , killSignal : 'SIGKILL' } ) ;
95+ child . on ( 'error' , reject ) ;
96+ child . stdin . on ( 'error' , reject ) ;
97+ child . on ( 'close' , code => code === 0 ? resolvePromise ( ) : reject ( new Error ( 'Cannot replay working-tree changes.' ) ) ) ;
98+ child . stdin . end ( patch ) ;
99+ } ) ;
100+ }
101+ const files = ( await output ( 'git' , [ '-C' , directory , 'ls-files' , '--others' , '--exclude-standard' , '-z' , '--' , 'rust' , 'shared' ] , false ) )
102+ . split ( '\0' ) . filter ( Boolean ) ;
103+ for ( const file of files ) {
104+ const target = join ( destination , file ) ;
105+ await mkdir ( resolve ( target , '..' ) , { recursive : true } ) ;
106+ await cp ( join ( directory , file ) , target , { verbatimSymlinks : true } ) ;
107+ }
108+ }
109+
73110async function copyScenario ( destination ) {
74111 for ( const entry of await readdir ( scenario , { recursive : true , withFileTypes : true } ) ) {
75112 if ( ! entry . isFile ( ) ) continue ;
@@ -100,32 +137,43 @@ async function rustSample() {
100137}
101138
102139async function main ( ) {
103- const { values } = parseArgs ( { options : { ref : { type : 'string' } , source : { type : 'string' } } } ) ;
140+ const { values } = parseArgs ( { options : { ref : { type : 'string' } , source : { type : 'string' } ,
141+ 'working-tree' : { type : 'boolean' } , 'build-workflows' : { type : 'boolean' } } } ) ;
142+ if ( values [ 'working-tree' ] && ( ! values . source || values . ref ) ) {
143+ throw new Error ( '--working-tree requires a local --source and cannot be combined with --ref.' ) ;
144+ }
104145 const lock = await readLock ( ) ;
105- const ref = values . ref ?? lock . source . revision ;
146+ const schema = JSON . parse ( await readFile ( join ( repository , 'contract/v1/plugin-manifest.schema.json' ) , 'utf8' ) ) ;
147+ const buildWorkflows = includesNativeWorkflows ( schema , values [ 'build-workflows' ] ) ;
148+ const ref = values [ 'working-tree' ] ? 'HEAD' : values . ref ?? lock . source . revision ;
106149 const source = values . source ?? `${ lock . source . repository } .git` ;
107150
108151 const java = process . env . LITHE_PLUGIN_SDK_JAVA_RUNTIME || await which ( 'java' ) ;
109152 const jar = await javaSample ( ) ;
153+ const nativeJar = process . env . LITHE_PLUGIN_SDK_JAVA_NATIVE_SAMPLE
154+ ? resolve ( process . env . LITHE_PLUGIN_SDK_JAVA_NATIVE_SAMPLE )
155+ : join ( repository , 'sdk/java/examples/sample-native-build/target/sample-native-build.jar' ) ;
156+ if ( buildWorkflows ) await access ( nativeJar ) ;
110157 const rust = await rustSample ( ) ;
111158 const revision = await checkoutCore ( source , ref ) ;
112- console . log ( `Running the SDK end-to-end scenario against Lithe ${ revision } ` ) ;
159+ if ( values [ 'working-tree' ] ) await copyWorkingTree ( source , work ) ;
160+ console . log ( `Running the SDK end-to-end scenario against Lithe ${ revision } ${ values [ 'working-tree' ] ? ' plus explicit working-tree changes' : '' } ` ) ;
113161 await copyScenario ( join ( work , 'rust/lithe-core/tests' ) ) ;
114162
115163 await run ( 'cargo' , [ 'test' , '--locked' , '--manifest-path' , join ( work , 'rust/Cargo.toml' ) , '-p' , 'lithe-core' ,
116- ...tests . flatMap ( test => [ '--test' , test ] ) ] , {
164+ ...[ ... tests , ... ( buildWorkflows ? [ 'plugin_sdk_build' ] : [ ] ) ] . flatMap ( test => [ '--test' , test ] ) ] , {
117165 env : {
118166 ...process . env ,
119167 LITHE_PLUGIN_SDK_ROOT : repository ,
120168 LITHE_PLUGIN_SDK_NODE : process . execPath ,
121169 LITHE_PLUGIN_SDK_JAVA : java ,
122170 LITHE_PLUGIN_SDK_JAVA_SAMPLE : jar ,
171+ LITHE_PLUGIN_SDK_JAVA_NATIVE_SAMPLE : nativeJar ,
123172 LITHE_PLUGIN_SDK_RUST_SAMPLE : rust ,
124173 } ,
125174 } ) ;
126175}
127176
128- main ( ) . catch ( error => {
129- console . error ( error . message ) ;
130- process . exitCode = 1 ;
131- } ) ;
177+ if ( process . argv [ 1 ] && resolve ( process . argv [ 1 ] ) === fileURLToPath ( import . meta. url ) ) {
178+ main ( ) . catch ( error => { console . error ( error . message ) ; process . exitCode = 1 ; } ) ;
179+ }
0 commit comments