diff --git a/.github/workflows/build-distributions.yml b/.github/workflows/build-distributions.yml index 547a47cf47..12a3661dac 100644 --- a/.github/workflows/build-distributions.yml +++ b/.github/workflows/build-distributions.yml @@ -203,6 +203,24 @@ jobs: "${run_as[@]}" "${smoke_scripts}/sqb" --version "${run_as[@]}" "${smoke_scripts}/sqb" --help "${run_as[@]}" "${smoke_scripts}/sqlbuild" --help + "${run_as[@]}" "${smoke_scripts}/sqlbuild" --version + # The sqlbuild forwarder returns sqb's own exit code; an unknown command is a usage error. + code=0 + "${run_as[@]}" "${smoke_scripts}/sqlbuild" no-such-command > /dev/null 2>&1 || code=$? + if [[ "${code}" != "2" ]]; then + echo "sqlbuild no-such-command exited ${code}; sqb exits 2" >&2 + exit 1 + fi + exe="${{ runner.os == 'Windows' && '.exe' || '' }}" + { + echo "### Sizes (${{ matrix.platform.name }})" + echo + echo "| File | Bytes |" + echo "| --- | ---: |" + for file in "${GITHUB_WORKSPACE}"/dist/*.whl "${smoke_scripts}/sqb${exe}" "${smoke_scripts}/sqlbuild${exe}"; do + echo "| $(basename "${file}") | $(wc -c < "${file}" | tr -d ' ') |" + done + } >> "${GITHUB_STEP_SUMMARY}" if [[ "${{ inputs.prebuilt-run-id == '' }}" == "true" ]]; then cd "${GITHUB_WORKSPACE}" "${run_as[@]}" "${smoke_python}" -m tests.integration.src.sqlbuild.compiler.compiled_project.publication.wheel.helpers \ diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8d84971ec8..5661db7674 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -619,6 +619,8 @@ jobs: -l "${RUNNER_TEMP}/postgres.log" -w start - name: Worker claim ownership, recovery and stop run: cargo test --locked -p sqlbuild-process-ownership + - name: sqlbuild forwarder arguments, exit codes, streams, signals and missing sqb + run: cargo test --locked -p sqlbuild-cli --test launcher - name: Control store, scheduler and recovery contracts on SQLite and PostgreSQL env: SQLBUILD_TEST_CONTROL_POSTGRES_URL: postgresql://postgres@127.0.0.1:5433/postgres diff --git a/crates/sqlbuild-cli/src/bin/sqlbuild.rs b/crates/sqlbuild-cli/src/bin/sqlbuild.rs index 7a0aa9b7ae..ca32528190 100644 --- a/crates/sqlbuild-cli/src/bin/sqlbuild.rs +++ b/crates/sqlbuild-cli/src/bin/sqlbuild.rs @@ -1,9 +1,37 @@ -//! `sqlbuild`: the native SQLBuild command line, the same program as `sqb`. +//! `sqlbuild`: the same command line as `sqb`, by running the `sqb` installed beside it without linking the library. +#[path = "../launcher/_helpers/handoff.rs"] +mod handoff; + +use std::ffi::OsString; +use std::io::Write; +use std::path::PathBuf; use std::process::ExitCode; -use sqlbuild_cli::launcher::main::run_launcher::run_launcher; +use handoff::{beside, hand_off, process_exit}; fn main() -> ExitCode { - run_launcher() + let program: PathBuf = std::env::current_exe().unwrap_or_default(); + let sqb_name = format!("sqb{}", std::env::consts::EXE_SUFFIX); + let Some(sqb) = beside(&program, &sqb_name) else { + let _ = writeln!( + std::io::stderr(), + "sqlbuild: cannot find {sqb_name} beside {}; reinstall sqlbuild-cli", + program.display() + ); + return ExitCode::FAILURE; + }; + let arguments: Vec = std::env::args_os().skip(1).collect(); + match hand_off(&sqb, &arguments) { + Ok(Some(code)) => process_exit(code), + Ok(None) => ExitCode::FAILURE, + Err(error) => { + let _ = writeln!( + std::io::stderr(), + "sqlbuild: cannot start {}: {error}", + sqb.display() + ); + ExitCode::FAILURE + } + } } diff --git a/crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs b/crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs index 4b4165481c..b27b9e2f55 100644 --- a/crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs +++ b/crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs @@ -4,11 +4,13 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::time::{Instant, SystemTime}; -use crate::launcher::_helpers::handoff::{hand_off, python_cli}; +use crate::launcher::_helpers::handoff::{beside, hand_off}; use crate::launcher::_helpers::launch_key::{launch_key, launch_record_path}; -use crate::launcher::_helpers::messages::{print_version, report_missing_python_cli}; +use crate::launcher::_helpers::messages::{ + print_version, report_handoff_failure, report_missing_python_cli, +}; use crate::launcher::constants::{ - HANDOFF_FAILURE_EXIT_CODE, LAUNCH_RECORD_OPTION, VERSION_OPTIONS, + HANDOFF_FAILURE_EXIT_CODE, LAUNCH_RECORD_OPTION, PYTHON_CLI_SCRIPT, VERSION_OPTIONS, }; use crate::native_commands::main::parse_native_invocation::parse_native_invocation; use crate::native_commands::main::run_control_invocation::run_control_invocation; @@ -64,7 +66,7 @@ pub(crate) fn launch(arguments: Vec, started: (Instant, SystemTime)) - return code; } let program: PathBuf = std::env::current_exe().unwrap_or_default(); - let Some(script) = python_cli(&program) else { + let Some(script) = beside(&program, PYTHON_CLI_SCRIPT) else { report_missing_python_cli(&program); return HANDOFF_FAILURE_EXIT_CODE; }; @@ -94,5 +96,12 @@ pub(crate) fn launch(arguments: Vec, started: (Instant, SystemTime)) - handed.push(option); } handed.extend(arguments); - hand_off(&script, &handed) + match hand_off(&script, &handed) { + Ok(Some(code)) => code, + Ok(None) => HANDOFF_FAILURE_EXIT_CODE, + Err(error) => { + report_handoff_failure(&script, &error); + HANDOFF_FAILURE_EXIT_CODE + } + } } diff --git a/crates/sqlbuild-cli/src/launcher/_helpers/handoff.rs b/crates/sqlbuild-cli/src/launcher/_helpers/handoff.rs index d6c001178b..405f82a5b5 100644 --- a/crates/sqlbuild-cli/src/launcher/_helpers/handoff.rs +++ b/crates/sqlbuild-cli/src/launcher/_helpers/handoff.rs @@ -1,42 +1,41 @@ -//! Find the Python CLI script installed beside this program and run it with the same arguments. +//! Find a program installed beside this one and run it with the same arguments and streams; the `sqlbuild` forwarder compiles this file into its own binary. use std::ffi::OsString; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, ExitCode}; -use crate::launcher::_helpers::messages::report_handoff_failure; -use crate::launcher::constants::{HANDOFF_FAILURE_EXIT_CODE, PYTHON_CLI_SCRIPT}; - -/// The Python CLI script beside this program or beside the file it links to. -pub(crate) fn python_cli(program: &Path) -> Option { +/// The file `name` beside this program or beside the file it links to. +pub(crate) fn beside(program: &Path, name: &str) -> Option { let resolved = std::fs::canonicalize(program).unwrap_or_else(|_| program.to_path_buf()); [program, resolved.as_path()] .into_iter() .filter_map(Path::parent) - .map(|directory| directory.join(PYTHON_CLI_SCRIPT)) + .map(|directory| directory.join(name)) .find(|candidate| candidate.is_file()) } -fn failure(script: &Path, error: &std::io::Error) -> i32 { - report_handoff_failure(script, error); - HANDOFF_FAILURE_EXIT_CODE -} - -/// Replace this process with the Python CLI; returns only when it cannot start. +/// Replace this process with `target`; returns only the error that kept it from starting. #[cfg(unix)] -pub(crate) fn hand_off(script: &Path, arguments: &[OsString]) -> i32 { +pub(crate) fn hand_off(target: &Path, arguments: &[OsString]) -> std::io::Result> { use std::os::unix::process::CommandExt; - let error = Command::new(script).args(arguments).exec(); - failure(script, &error) + Err(Command::new(target).args(arguments).exec()) } -/// Run the Python CLI as a child that alone handles Ctrl+C, and return its exit code. +/// Run `target` as a child that alone handles Ctrl+C and Ctrl+Break, and return its exit code. #[cfg(not(unix))] -pub(crate) fn hand_off(script: &Path, arguments: &[OsString]) -> i32 { +pub(crate) fn hand_off(target: &Path, arguments: &[OsString]) -> std::io::Result> { let _ = ctrlc::set_handler(|| {}); - match Command::new(script).args(arguments).status() { - Ok(status) => status.code().unwrap_or(HANDOFF_FAILURE_EXIT_CODE), - Err(error) => failure(script, &error), + Command::new(target) + .args(arguments) + .status() + .map(|status| status.code()) +} + +/// End the process with `code`; a code beyond one byte ends it directly. +pub(crate) fn process_exit(code: i32) -> ExitCode { + match u8::try_from(code) { + Ok(code) => ExitCode::from(code), + Err(_) => std::process::exit(code), } } diff --git a/crates/sqlbuild-cli/src/launcher/main/run_launcher.rs b/crates/sqlbuild-cli/src/launcher/main/run_launcher.rs index cc1f26c7b2..4db160c179 100644 --- a/crates/sqlbuild-cli/src/launcher/main/run_launcher.rs +++ b/crates/sqlbuild-cli/src/launcher/main/run_launcher.rs @@ -1,18 +1,15 @@ -//! The `sqb` and `sqlbuild` executables' whole process. +//! The `sqb` executable's whole process. use std::ffi::OsString; use std::process::ExitCode; use std::time::{Instant, SystemTime}; use crate::launcher::_helpers::dispatch::launch; +use crate::launcher::_helpers::handoff::process_exit; /// Run this process's invocation; an exit code beyond one byte ends the process directly. pub fn run_launcher() -> ExitCode { let started = (Instant::now(), SystemTime::now()); let arguments: Vec = std::env::args_os().skip(1).collect(); - let code: i32 = launch(arguments, started); - match u8::try_from(code) { - Ok(code) => ExitCode::from(code), - Err(_) => std::process::exit(code), - } + process_exit(launch(arguments, started)) } diff --git a/crates/sqlbuild-cli/tests/launcher.rs b/crates/sqlbuild-cli/tests/launcher.rs new file mode 100644 index 0000000000..2b54f56ccf --- /dev/null +++ b/crates/sqlbuild-cli/tests/launcher.rs @@ -0,0 +1,6 @@ +#[path = "launcher/helpers.rs"] +mod helpers; +#[path = "launcher/test_forwarder.rs"] +mod test_forwarder; +#[path = "launcher/test_types.rs"] +mod test_types; diff --git a/crates/sqlbuild-cli/tests/launcher/helpers.rs b/crates/sqlbuild-cli/tests/launcher/helpers.rs new file mode 100644 index 0000000000..dd34f67289 --- /dev/null +++ b/crates/sqlbuild-cli/tests/launcher/helpers.rs @@ -0,0 +1,149 @@ +#[cfg(unix)] +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::process::Child; +use std::process::Command; + +const FAKE_SQB: &str = r#" +use std::io::{Read, Write}; + +fn main() { + let mut out = std::io::stdout(); + for argument in std::env::args_os().skip(1) { + writeln!(out, "arg:{:?}", argument).unwrap(); + } + if std::env::var_os("FAKE_STDIN").is_some() { + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input).unwrap(); + write!(out, "stdin:{input}").unwrap(); + } + out.flush().unwrap(); + eprint!("fake stderr"); + if std::env::var_os("FAKE_WAIT").is_some() { + #[cfg(windows)] + console::exit_on_break(); + writeln!(out, "ready:{}", std::process::id()).unwrap(); + out.flush().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(30)); + } + let code: i32 = std::env::var("FAKE_EXIT").ok().and_then(|code| code.parse().ok()).unwrap_or(0); + std::process::exit(code); +} + +#[cfg(windows)] +mod console { + type Handler = unsafe extern "system" fn(u32) -> i32; + + #[link(name = "kernel32")] + extern "system" { + fn SetConsoleCtrlHandler(handler: Option, add: i32) -> i32; + } + + unsafe extern "system" fn on_break(_event: u32) -> i32 { + std::thread::sleep(std::time::Duration::from_millis(500)); + std::process::exit(42) + } + + pub fn exit_on_break() { + unsafe { + SetConsoleCtrlHandler(Some(on_break), 1); + } + } +} +"#; + +#[cfg(windows)] +const BREAKER: &str = r#" +use std::io::BufRead; +use std::os::windows::process::CommandExt; +use std::process::{Command, Stdio}; + +#[link(name = "kernel32")] +extern "system" { + fn GenerateConsoleCtrlEvent(event: u32, group: u32) -> i32; +} + +fn main() { + let forwarder = std::env::args_os().nth(1).unwrap(); + let result = std::env::args_os().nth(2).unwrap(); + let mut child = Command::new(forwarder) + .creation_flags(0x0000_0200) + .env("FAKE_WAIT", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut ready = String::new(); + std::io::BufReader::new(child.stdout.take().unwrap()).read_line(&mut ready).unwrap(); + let sent = unsafe { GenerateConsoleCtrlEvent(1, child.id()) }; + let status = child.wait().unwrap(); + std::fs::write(result, format!("{sent} {}", status.code().unwrap())).unwrap(); +} +"#; + +fn compile(source: &str, directory: &Path, name: &str) -> PathBuf { + let path = directory.join(format!("{name}.rs")); + std::fs::write(&path, source).expect("program source"); + let binary = directory.join(format!("{name}{}", std::env::consts::EXE_SUFFIX)); + let output = Command::new(std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into())) + .args(["--edition", "2021", "-o"]) + .arg(&binary) + .arg(&path) + .output() + .expect("run rustc"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + binary +} + +/// The file name of `sqb` on this platform. +pub(crate) fn sqb_file() -> String { + format!("sqb{}", std::env::consts::EXE_SUFFIX) +} + +/// A copy of the real `sqlbuild` forwarder, alone in `directory`; a copy works across volumes. +pub(crate) fn forwarder_in(directory: &Path) -> PathBuf { + let forwarder = directory.join(format!("sqlbuild{}", std::env::consts::EXE_SUFFIX)); + std::fs::copy(env!("CARGO_BIN_EXE_sqlbuild"), &forwarder).expect("copy the forwarder"); + forwarder +} + +/// The real forwarder with a fake `sqb` beside it that reports what it received. +pub(crate) fn forwarder_with_fake_sqb(directory: &Path) -> PathBuf { + compile(FAKE_SQB, directory, "sqb"); + forwarder_in(directory) +} + +/// The pid the waiting fake `sqb` reports on its first stdout line. +#[cfg(unix)] +pub(crate) fn ready_pid(child: &mut Child) -> u32 { + let mut line = String::new(); + BufReader::new(child.stdout.take().expect("piped stdout")) + .read_line(&mut line) + .expect("ready line"); + line.trim() + .strip_prefix("ready:") + .and_then(|pid| pid.parse().ok()) + .expect("the fake sqb reports its pid") +} + +/// Ctrl+Break the forwarder's process group in a fresh console; returns ` `. +#[cfg(windows)] +pub(crate) fn break_forwarder(directory: &Path, forwarder: &Path) -> String { + use std::os::windows::process::CommandExt; + + let breaker = compile(BREAKER, directory, "breaker"); + let result = directory.join("result.txt"); + let status = Command::new(breaker) + .arg(forwarder) + .arg(&result) + .creation_flags(0x0000_0010) + .status() + .expect("run the breaker"); + assert!(status.success(), "breaker failed: {status:?}"); + std::fs::read_to_string(result).expect("breaker result") +} diff --git a/crates/sqlbuild-cli/tests/launcher/test_forwarder.rs b/crates/sqlbuild-cli/tests/launcher/test_forwarder.rs new file mode 100644 index 0000000000..392bc0d082 --- /dev/null +++ b/crates/sqlbuild-cli/tests/launcher/test_forwarder.rs @@ -0,0 +1,261 @@ +//! `sqlbuild` behaves exactly like the `sqb` installed beside it and fails loudly without one. + +use std::io::Write; +use std::process::{Command, Stdio}; + +#[cfg(windows)] +use crate::helpers::break_forwarder; +#[cfg(unix)] +use crate::helpers::ready_pid; +use crate::helpers::{forwarder_in, forwarder_with_fake_sqb, sqb_file}; +#[cfg(windows)] +use crate::test_types::ForwardedBreakTestCase; +#[cfg(unix)] +use crate::test_types::ForwardedSignalTestCase; +use crate::test_types::{ + ForwardedArgumentsTestCase, ForwardedExitTestCase, ForwardedStreamsTestCase, MissingSqbTestCase, +}; + +#[test] +fn given_arguments_when_running_sqlbuild_then_sqb_receives_them_unchanged() { + let test_cases = [ + ForwardedArgumentsTestCase { + description: "no arguments", + arguments: &[], + expected_lines: &[], + }, + ForwardedArgumentsTestCase { + description: "spaces, quotes, unicode, empty and dash-leading arguments", + arguments: &[ + "--project-dir", + "orders shop", + "say \"hi\"", + "commandé", + "", + "--", + "-x", + ], + expected_lines: &[ + r#"arg:"--project-dir""#, + r#"arg:"orders shop""#, + r#"arg:"say \"hi\"""#, + r#"arg:"commandé""#, + r#"arg:"""#, + r#"arg:"--""#, + r#"arg:"-x""#, + ], + }, + ]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_with_fake_sqb(directory.path()); + for test_case in test_cases { + let output = Command::new(&forwarder) + .args(test_case.arguments) + .output() + .expect("run sqlbuild"); + let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout"); + assert_eq!( + stdout.lines().collect::>(), + test_case.expected_lines, + "{}", + test_case.description + ); + } +} + +#[test] +fn given_sqb_exit_code_when_running_sqlbuild_then_sqlbuild_exits_with_the_same_code() { + let test_cases = [ + ForwardedExitTestCase { + description: "success", + exit: 0, + expected_code: 0, + }, + ForwardedExitTestCase { + description: "usage error", + exit: 2, + expected_code: 2, + }, + ForwardedExitTestCase { + description: "largest portable code", + exit: 255, + expected_code: 255, + }, + #[cfg(windows)] + ForwardedExitTestCase { + description: "Windows status beyond one byte", + exit: -1_073_741_510, + expected_code: -1_073_741_510, + }, + ]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_with_fake_sqb(directory.path()); + for test_case in test_cases { + let status = Command::new(&forwarder) + .env("FAKE_EXIT", test_case.exit.to_string()) + .stderr(Stdio::null()) + .status() + .expect("run sqlbuild"); + assert_eq!( + status.code(), + Some(test_case.expected_code), + "{}", + test_case.description + ); + } +} + +#[test] +fn given_input_when_running_sqlbuild_then_sqb_shares_stdin_stdout_and_stderr() { + let test_cases = [ + ForwardedStreamsTestCase { + description: "multi-line input", + stdin: "orders\nline two\n", + expected_stdout: "stdin:orders\nline two\n", + expected_stderr: "fake stderr", + }, + ForwardedStreamsTestCase { + description: "empty input", + stdin: "", + expected_stdout: "stdin:", + expected_stderr: "fake stderr", + }, + ]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_with_fake_sqb(directory.path()); + for test_case in test_cases { + let mut child = Command::new(&forwarder) + .env("FAKE_STDIN", "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("run sqlbuild"); + child + .stdin + .take() + .expect("stdin") + .write_all(test_case.stdin.as_bytes()) + .expect("write stdin"); + let output = child.wait_with_output().expect("sqlbuild output"); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + test_case.expected_stdout, + "{}", + test_case.description + ); + assert_eq!( + String::from_utf8_lossy(&output.stderr), + test_case.expected_stderr, + "{}", + test_case.description + ); + } +} + +#[test] +fn given_no_sqb_beside_it_when_running_sqlbuild_then_it_fails_and_names_the_missing_program() { + let test_cases = [ + MissingSqbTestCase { + description: "version request", + arguments: &["--version"], + expected_code: Some(1), + expected_stdout: "", + expected_stderr_start: format!("sqlbuild: cannot find {} beside ", sqb_file()), + }, + MissingSqbTestCase { + description: "command", + arguments: &["build", "--select", "orders"], + expected_code: Some(1), + expected_stdout: "", + expected_stderr_start: format!("sqlbuild: cannot find {} beside ", sqb_file()), + }, + ]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_in(directory.path()); + for test_case in test_cases { + let output = Command::new(&forwarder) + .args(test_case.arguments) + .output() + .expect("run sqlbuild"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!( + output.status.code(), + test_case.expected_code, + "{}", + test_case.description + ); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + test_case.expected_stdout, + "{}", + test_case.description + ); + assert!( + stderr.starts_with(&test_case.expected_stderr_start), + "{}: {stderr}", + test_case.description + ); + } +} + +#[cfg(unix)] +#[test] +fn given_a_signal_when_sqb_runs_through_sqlbuild_then_sqb_is_the_same_process_and_receives_it() { + use std::os::unix::process::ExitStatusExt; + + let test_cases = [ + ForwardedSignalTestCase { + description: "termination", + signal: "TERM", + expected_signal: Some(15), + }, + ForwardedSignalTestCase { + description: "interrupt", + signal: "INT", + expected_signal: Some(2), + }, + ]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_with_fake_sqb(directory.path()); + for test_case in test_cases { + let mut child = Command::new(&forwarder) + .env("FAKE_WAIT", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("run sqlbuild"); + let pid = ready_pid(&mut child); + assert_eq!(pid, child.id(), "{}", test_case.description); + let sent = Command::new("kill") + .args([format!("-{}", test_case.signal), pid.to_string()]) + .status() + .expect("send the signal"); + assert!(sent.success(), "{}", test_case.description); + assert_eq!( + child.wait().expect("wait").signal(), + test_case.expected_signal, + "{}", + test_case.description + ); + } +} + +#[cfg(windows)] +#[test] +fn given_ctrl_break_when_sqb_runs_through_sqlbuild_then_only_sqb_handles_it_and_its_code_returns() { + let test_cases = [ForwardedBreakTestCase { + description: "sqb exits 42 half a second after Ctrl+Break while sqlbuild waits", + expected_result: "1 42", + }]; + let directory = tempfile::tempdir().expect("isolated install"); + let forwarder = forwarder_with_fake_sqb(directory.path()); + for test_case in test_cases { + assert_eq!( + break_forwarder(directory.path(), &forwarder), + test_case.expected_result, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-cli/tests/launcher/test_types.rs b/crates/sqlbuild-cli/tests/launcher/test_types.rs new file mode 100644 index 0000000000..3bae20586c --- /dev/null +++ b/crates/sqlbuild-cli/tests/launcher/test_types.rs @@ -0,0 +1,39 @@ +pub(crate) struct ForwardedArgumentsTestCase { + pub(crate) description: &'static str, + pub(crate) arguments: &'static [&'static str], + pub(crate) expected_lines: &'static [&'static str], +} + +pub(crate) struct ForwardedExitTestCase { + pub(crate) description: &'static str, + pub(crate) exit: i32, + pub(crate) expected_code: i32, +} + +pub(crate) struct ForwardedStreamsTestCase { + pub(crate) description: &'static str, + pub(crate) stdin: &'static str, + pub(crate) expected_stdout: &'static str, + pub(crate) expected_stderr: &'static str, +} + +pub(crate) struct MissingSqbTestCase { + pub(crate) description: &'static str, + pub(crate) arguments: &'static [&'static str], + pub(crate) expected_code: Option, + pub(crate) expected_stdout: &'static str, + pub(crate) expected_stderr_start: String, +} + +#[cfg(unix)] +pub(crate) struct ForwardedSignalTestCase { + pub(crate) description: &'static str, + pub(crate) signal: &'static str, + pub(crate) expected_signal: Option, +} + +#[cfg(windows)] +pub(crate) struct ForwardedBreakTestCase { + pub(crate) description: &'static str, + pub(crate) expected_result: &'static str, +} diff --git a/fensu.toml b/fensu.toml index ed50609ba3..9458e916fc 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1765,6 +1765,16 @@ rule = "FPRSH002" path = "crates/sqlbuild-process-ownership/src/ownership/_helpers/windows.rs" reason = "The workspace allows unsafe code only in sqlbuild-process-ownership, for the platform calls that prove which process owns a worker claim; each call has an item-level allow and a SAFETY comment." +[[targets.rust.rule_exceptions]] +rule = "FPRSR701" +path = "crates/sqlbuild-cli/src/bin/sqlbuild.rs" +reason = "The sqlbuild forwarder compiles only the launcher hand-off file instead of linking the sqlbuild-cli library: linking the library grows the stripped binary from about 0.37 MB to 6.4 MB in every wheel." + +[[targets.rust.rule_exceptions]] +rule = "FPRSR702" +path = "crates/sqlbuild-cli/src/bin/sqlbuild.rs" +reason = "The sqlbuild forwarder compiles only the launcher hand-off file instead of linking the sqlbuild-cli library: linking the library grows the stripped binary from about 0.37 MB to 6.4 MB in every wheel." + [targets.rust.rule_options.FPRSL102] packages = [] restricted_paths = ["crates/sqlbuild-rules/src/rules"]