char-slop/vscode-hubris
git clone https://git.t4t.associates/char-slop/vscode-hubris
f4210c2
main
1const assert = require ( "node:assert/strict" ); 2const { readFileSync} = require ( "node:fs" ); 3const { createRequire} = require ( "node:module" ); 4const path = require ( "node:path" ); 5const { setImmediate} = require ( "node:timers/promises" ); 6const { promisify} = require ( "node:util" ); 7const vm = require ( "node:vm" ); 8const { test} = require ( "node:test" ); 9 10const root = path . resolve ( "/workspace/firmware" ); 11const source = readFileSync ( path . join ( __dirname , "extension.js" ), "utf8" ); 12const usb = { Ok :{ app :"app.toml" , task :"usb" , hash :"usb" , buildOverrideCommand :[ "cargo" , "check" , "-pusb" , "--message-format=json" ] } }; 13const jefe = { Ok :{ app :"app.toml" , task :"jefe" , hash :"jefe" , buildOverrideCommand :[ "cargo" , "check" , "-pjefe" , "--message-format=json" ] } }; 14 15function editor ( resolve , trusted = true ) { 16const settings = {}; 17const calls = []; 18const errors = []; 19const files = new Map (); 20const timers = new Map (); 21const subscriptions = []; 22const status = { show () {}, dispose () {} }; 23let change ; 24let onUpdate ; 25const vscode = { 26ConfigurationTarget :{ Workspace :2 }, 27StatusBarAlignment :{ Left :1 }, 28workspace :{ 29isTrusted :trusted , 30workspaceFolders :[{ uri :{ fsPath :root } }], 31getConfiguration ( section ) { 32if ( section === "hubris" ) return { get :() => "app.toml" }; 33const snapshot = structuredClone ( settings ); 34return { 35get :( key ) => snapshot [ key ], 36async update ( key , value ) { 37await onUpdate ?.( key , value ); 38settings [ key ] = value ; 39}, 40}; 41}, 42onDidSaveTextDocument :() => ({ dispose () {} }), 43onDidChangeConfiguration :() => ({ dispose () {} }), 44}, 45window :{ 46createOutputChannel :() => ({ appendLine () {}, dispose () {} }), 47createStatusBarItem :() => status , 48showErrorMessage :( error ) => errors . push ( error ), 49onDidChangeActiveTextEditor :( callback ) => { change = callback ; return { dispose () {} }; }, 50}, 51commands :{ 52executeCommand :async ( command ) => { calls . push ( command ); }, 53registerCommand :() => ({ dispose () {} }), 54}, 55}; 56const execFile = () => {}; 57execFile [ promisify . custom ] = async ( command , args , options ) => { 58assert . equal ( command , "cargo" ); 59assert . deepEqual ( Array . from ( args . slice ( 0 , 2 )), [ "xtask" , "lsp" ]); 60assert . equal ( options . env . HUBRIS_APP , "app.toml" ); 61assert . equal ( options . env . HUBRIS_TASK , undefined ); 62return { stdout :JSON . stringify ( await resolve ( args )) }; 63}; 64const module = { exports :{} }; 65const require = createRequire ( __filename ); 66vm . runInNewContext ( source , { 67 module, 68process :{ env :{ HUBRIS_TASK :"stale-task" } }, 69require ( name ) { 70if ( name === "vscode" ) return vscode ; 71if ( name === "node:child_process" ) return { execFile}; 72if ( name === "node:fs/promises" ) return { mkdir :async () => {}, writeFile :async ( file , content ) => { files . set ( file , content ); } }; 73return require ( name ); 74}, 75setTimeout :( callback ) => { const id = Symbol (); timers . set ( id , callback ); return id ; }, 76clearTimeout :( id ) => timers . delete ( id ), 77}); 78module . exports . activate ({ subscriptions, storageUri :{ fsPath :"/storage/hubris" }, asAbsolutePath :( file ) => path . join ( __dirname , file ) }); 79return { 80 settings, calls, errors, files, status, 81open ( file ) { 82vscode . window . activeTextEditor = { document :{ languageId :"rust" , uri :{ scheme :"file" , fsPath :path . resolve ( root , file ) } } }; 83change ?.(); 84}, 85async flush () { 86for ( const [ id , callback ] of timers ) { timers . delete ( id ); callback (); } 87await setImmediate (); 88}, 89onUpdate ( callback ) { onUpdate = callback ; }, 90dispose () { for ( const subscription of subscriptions ) subscription . dispose (); }, 91}; 92} 93 94test ( "task changes update the launcher and build scripts without changing server environment" , async ( t ) => { 95const app = editor (( args ) => args . at ( - 1 ). includes ( "/usb/" ) ?usb :jefe ); 96t . after (() => app . dispose ()); 97app . open ( "tasks/usb/src/main.rs" ); 98await app . flush (); 99const env = JSON . stringify ( app . settings [ "server.extraEnv" ]); 100app . open ( "tasks/jefe/src/main.rs" ); 101await app . flush (); 102assert . equal ( app . files . get ( "/storage/hubris/target" ), "app.toml:jefe" ); 103assert . deepEqual ( Array . from ( app . settings [ "cargo.buildScripts.overrideCommand" ]), jefe . Ok . buildOverrideCommand ); 104assert . equal ( JSON . stringify ( app . settings [ "server.extraEnv" ]), env ); 105assert . equal ( app . calls . length , 4 ); 106assert . deepEqual ( app . errors , []); 107}); 108 109test ( "shared files pass the current client and do not restart an unchanged context" , async ( t ) => { 110const lookups = []; 111const app = editor (( args ) => { lookups . push ( args ); return usb ; }); 112t . after (() => app . dispose ()); 113app . open ( "tasks/usb/src/main.rs" ); 114await app . flush (); 115app . open ( "lib/shared/src/lib.rs" ); 116await app . flush (); 117assert . deepEqual ( JSON . parse ( lookups [ 1 ][ 3 ]), { toml :"app.toml" , task :"usb" }); 118assert . equal ( app . calls . length , 2 ); 119}); 120 121test ( "unresolved files retain the previous context and report the upstream error" , async ( t ) => { 122const app = editor (( args ) => args . at ( - 1 ). includes ( "/kernel/" ) ?{ Err :"kernel is not used" } :usb ); 123t . after (() => app . dispose ()); 124app . open ( "tasks/usb/src/main.rs" ); 125await app . flush (); 126app . open ( "kernel/src/main.rs" ); 127await app . flush (); 128assert . equal ( app . files . get ( "/storage/hubris/target" ), "app.toml:usb" ); 129assert . match ( app . status . text , / no task / ); 130assert . equal ( app . status . tooltip , "kernel is not used" ); 131assert . equal ( app . calls . length , 2 ); 132app . open ( "tasks/usb/src/main.rs" ); 133await app . flush (); 134assert . equal ( app . status . text , "Hubris: app.toml:usb" ); 135}); 136 137test ( "a slow lookup cannot replace the newer active file's context" , async ( t ) => { 138let release ; 139const pending = new Promise (( resolve ) => { release = resolve ; }); 140const app = editor (( args ) => args . at ( - 1 ). includes ( "/usb/" ) ?pending :jefe ); 141t . after (() => app . dispose ()); 142app . open ( "tasks/usb/src/main.rs" ); 143await app . flush (); 144app . open ( "tasks/jefe/src/main.rs" ); 145release ( usb ); 146await app . flush (); 147assert . equal ( app . files . get ( "/storage/hubris/target" ), "app.toml:jefe" ); 148assert . equal ( app . calls . length , 2 ); 149}); 150 151test ( "a file change during configuration application is not lost" , async ( t ) => { 152const app = editor (( args ) => args . at ( - 1 ). includes ( "/usb/" ) ?usb :jefe ); 153t . after (() => app . dispose ()); 154app . onUpdate (() => { 155app . onUpdate ( undefined ); 156app . open ( "tasks/jefe/src/main.rs" ); 157}); 158app . open ( "tasks/usb/src/main.rs" ); 159await app . flush (); 160assert . equal ( app . files . get ( "/storage/hubris/target" ), "app.toml:jefe" ); 161assert . equal ( app . calls . length , 4 ); 162}); 163 164test ( "a failed settings write does not leave rust-analyzer stopped" , async ( t ) => { 165const app = editor (() => usb ); 166t . after (() => app . dispose ()); 167app . onUpdate (() => { throw new Error ( "settings are read-only" ); }); 168app . open ( "tasks/usb/src/main.rs" ); 169await app . flush (); 170assert . deepEqual ( app . calls , [ "rust-analyzer.stopServer" , "rust-analyzer.startServer" ]); 171assert . match ( app . errors [ 0 ], / read-only / ); 172}); 173 174test ( "untrusted workspaces and external files never execute Cargo" , async ( t ) => { 175for ( const trusted of [ false , true ]) { 176const app = editor (() => { assert . fail ( "unexpected cargo invocation" ); }, trusted ); 177t . after (() => app . dispose ()); 178app . open ( trusted ?"../../../external.rs" :"tasks/usb/src/main.rs" ); 179await app . flush (); 180assert . deepEqual ( app . calls , []); 181} 182});