diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 252f981..044810c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,16 +22,11 @@ jobs: toolchain: stable profile: minimal override: true - - name: Run cargo command + - name: Add nightly + run: rustup toolchain add nightly + - name: Run tests runner run: | - cargo run --manifest-path test-crates/bin/Cargo.toml + cd tests/ + cargo run + shell: bash continue-on-error: ${{ matrix.os == 'windows-latest' }} - - name: Run cargo command (release) - run: | - cargo run --manifest-path test-crates/bin/Cargo.toml --release - continue-on-error: ${{ matrix.os == 'windows-latest' }} - - name: Run cargo command (release with SOPRINTLN=1) - if: matrix.os != 'windows-latest' - run: | - export SOPRINTLN=1 - cargo run --manifest-path test-crates/bin/Cargo.toml --release diff --git a/Justfile b/Justfile index d083e5e..e9743ba 100644 --- a/Justfile +++ b/Justfile @@ -3,5 +3,16 @@ check: cargo hack --each-feature --exclude-all-features clippy --manifest-path rubicon/Cargo.toml -test: - SOPRINTLN=1 cargo run --manifest-path test-crates/bin/Cargo.toml +test *args: + #!/usr/bin/env bash -eux + BIN_CHANNEL="${BIN_CHANNEL:-stable}" + BIN_FLAGS="${BIN_FLAGS:-}" + + SOPRINTLN=1 cargo "+${BIN_CHANNEL}" build --manifest-path test-crates/samplebin/Cargo.toml ${BIN_FLAGS} + + export DYLD_LIBRARY_PATH=$(rustc "+stable" --print sysroot)/lib + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$(rustc "+nightly" --print sysroot)/lib + export LD_LIBRARY_PATH=$(rustc "+stable" --print sysroot)/lib + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(rustc "+nightly" --print sysroot)/lib + + ./test-crates/samplebin/target/debug/samplebin {{args}} diff --git a/rubicon/Cargo.lock b/rubicon/Cargo.lock index 40127dd..7133382 100644 --- a/rubicon/Cargo.lock +++ b/rubicon/Cargo.lock @@ -2,15 +2,84 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rubicon" version = "3.0.1" dependencies = [ + "ctor", + "libc", "paste", + "rustc_version", ] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/rubicon/Cargo.toml b/rubicon/Cargo.toml index 69573ad..3bfa0cf 100644 --- a/rubicon/Cargo.toml +++ b/rubicon/Cargo.toml @@ -14,9 +14,15 @@ keywords = ["ffi", "thread-local"] crate-type = ["dylib"] [dependencies] +ctor = { version = "0.2.8", optional = true } +libc = { version = "0.2.155", optional = true } paste = { version = "1.0.15", optional = true } +[build-dependencies] +rustc_version = { version = "0.4.0", optional = true } + [features] default = [] -export-globals = ["dep:paste"] -import-globals = ["dep:paste"] +export-globals = ["dep:paste", "dep:rustc_version"] +import-globals = ["dep:paste", "dep:rustc_version", "dep:ctor", "dep:libc"] +ctor = ["dep:ctor"] diff --git a/rubicon/build.rs b/rubicon/build.rs new file mode 100644 index 0000000..0b6b312 --- /dev/null +++ b/rubicon/build.rs @@ -0,0 +1,14 @@ +fn main() { + #[cfg(any(feature = "export-globals", feature = "import-globals"))] + { + use std::env; + + // Get the Rust compiler version and set it as an environment variable. + let rustc_version = rustc_version::version().unwrap(); + println!("cargo:rustc-env=RUBICON_RUSTC_VERSION={}", rustc_version); + + // Pass the target triple. + let target = env::var("TARGET").unwrap(); + println!("cargo:rustc-env=RUBICON_TARGET_TRIPLE={}", target); + } +} diff --git a/rubicon/src/lib.rs b/rubicon/src/lib.rs index 1bd3652..b64cc3c 100644 --- a/rubicon/src/lib.rs +++ b/rubicon/src/lib.rs @@ -36,6 +36,18 @@ compile_error!("The features `export-globals` and `import-globals` are mutually #[cfg(any(feature = "export-globals", feature = "import-globals"))] pub use paste::paste; +#[cfg(feature = "import-globals")] +pub use ctor; + +#[cfg(feature = "import-globals")] +pub use libc; + +#[cfg(any(feature = "export-globals", feature = "import-globals"))] +pub const RUBICON_RUSTC_VERSION: &str = env!("RUBICON_RUSTC_VERSION"); + +#[cfg(any(feature = "export-globals", feature = "import-globals"))] +pub const RUBICON_TARGET_TRIPLE: &str = env!("RUBICON_TARGET_TRIPLE"); + //============================================================================== // Wrappers //============================================================================== @@ -288,3 +300,322 @@ macro_rules! process_local_inner_mut { } }; } + +//============================================================================== +// Compatibility check +//============================================================================== + +#[cfg(feature = "export-globals")] +#[macro_export] +macro_rules! compatibility_check { + ($($feature:tt)*) => { + use std::env; + + $crate::paste! { + #[no_mangle] + #[export_name = concat!(env!("CARGO_PKG_NAME"), "_compatibility_info")] + static __RUBICON_COMPATIBILITY_INFO_: &'static [(&'static str, &'static str)] = &[ + ("rustc-version", $crate::RUBICON_RUSTC_VERSION), + ("target-triple", $crate::RUBICON_TARGET_TRIPLE), + $($feature)* + ]; + } + }; +} + +#[cfg(all(unix, feature = "import-globals"))] +#[macro_export] +macro_rules! compatibility_check { + ($($feature:tt)*) => { + use std::env; + use $crate::ctor::ctor; + + extern "Rust" { + #[link_name = concat!(env!("CARGO_PKG_NAME"), "_compatibility_info")] + static COMPATIBILITY_INFO: &'static [(&'static str, &'static str)]; + } + + + fn get_shared_object_name() -> Option { + use $crate::libc::{c_void, Dl_info}; + use std::ffi::CStr; + use std::ptr; + + extern "C" { + fn dladdr(addr: *const c_void, info: *mut Dl_info) -> i32; + } + + unsafe { + let mut info: Dl_info = std::mem::zeroed(); + if dladdr(get_shared_object_name as *const c_void, &mut info) != 0 { + let c_str = CStr::from_ptr(info.dli_fname); + return Some(c_str.to_string_lossy().into_owned()); + } + } + None + } + + struct AnsiEscape(u64, D); + + impl std::fmt::Display for AnsiEscape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = format!("\x1b[{}m{}\x1b[0m", self.0, self.1); + f.pad(&inner) + } + } + + #[derive(Clone, Copy)] + struct AnsiColor(u64); + + impl AnsiColor { + const BLUE: AnsiColor = AnsiColor(34); + const GREEN: AnsiColor = AnsiColor(32); + const RED: AnsiColor = AnsiColor(31); + const GREY: AnsiColor = AnsiColor(37); + } + + fn colored(color: AnsiColor, d: D) -> AnsiEscape { + AnsiEscape(color.0, d) + } + fn blue(d: D) -> AnsiEscape { + AnsiEscape(34, d) + } + fn green(d: D) -> AnsiEscape { + AnsiEscape(32, d) + } + fn red(d: D) -> AnsiEscape { + AnsiEscape(31, d) + } + fn grey(d: D) -> AnsiEscape { + AnsiEscape(35, d) + } + + // Helper function to count visible characters (ignoring ANSI escapes) + fn visible_len(s: &str) -> usize { + let mut len = 0; + let mut in_escape = false; + for c in s.chars() { + if c == '\x1b' { + in_escape = true; + } else if in_escape { + if c.is_alphabetic() { + in_escape = false; + } + } else { + len += 1; + } + } + len + } + + #[ctor] + fn check_compatibility() { + eprintln!("Entering check_compatibility function"); + let imported: &[(&str, &str)] = &[ + ("rustc-version", $crate::RUBICON_RUSTC_VERSION), + ("target-triple", $crate::RUBICON_TARGET_TRIPLE), + $($feature)* + ]; + let exported = unsafe { COMPATIBILITY_INFO }; + + eprintln!("Comparing imported and exported compatibility info"); + let missing: Vec<_> = imported.iter().filter(|&item| !exported.contains(item)).collect(); + let extra: Vec<_> = exported.iter().filter(|&item| !imported.contains(item)).collect(); + + if missing.is_empty() && extra.is_empty() { + eprintln!("No compatibility issues found"); + // all good + return; + } + + eprintln!("Compatibility issues detected, preparing error message"); + let so_name = get_shared_object_name().unwrap_or("unknown_so".to_string()); + // get only the last bit of the path + let so_name = so_name.rsplit('/').next().unwrap_or("unknown_so"); + + let exe_name = std::env::current_exe().map(|p| p.file_name().unwrap().to_string_lossy().to_string()).unwrap_or_else(|_| "unknown_exe".to_string()); + + let mut error_message = String::new(); + error_message.push_str("\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n"); + error_message.push_str(&format!(" 💀 Feature mismatch for crate \x1b[31m{}\x1b[0m\n\n", env!("CARGO_PKG_NAME"))); + + error_message.push_str(&format!("Loading {} would mix different configurations of the {} crate.\n\n", blue(so_name), red(env!("CARGO_PKG_NAME")))); + + eprintln!("Calculating column widths for grid display"); + // Compute max lengths for alignment + let max_exported_len = exported.iter().map(|(k, v)| format!("{}={}", k, v).len()).max().unwrap_or(0); + let max_ref_len = imported.iter().map(|(k, v)| format!("{}={}", k, v).len()).max().unwrap_or(0); + let column_width = max_exported_len.max(max_ref_len); + + // Gather all unique keys + let mut all_keys: Vec<&str> = Vec::new(); + for (key, _) in exported.iter() { + if !all_keys.contains(key) { + all_keys.push(key); + } + } + for (key, _) in imported.iter() { + if !all_keys.contains(key) { + all_keys.push(key); + } + } + + struct Grid { + rows: Vec>, + column_widths: Vec, + } + + impl Grid { + fn new() -> Self { + Grid { + rows: Vec::new(), + column_widths: Vec::new(), + } + } + + fn add_row(&mut self, row: Vec) { + if self.column_widths.len() < row.len() { + self.column_widths.resize(row.len(), 0); + } + for (i, cell) in row.iter().enumerate() { + self.column_widths[i] = self.column_widths[i].max(visible_len(cell)); + } + self.rows.push(row); + } + + fn write_to(&self, out: &mut String) { + let total_width: usize = self.column_widths.iter().sum::() + self.column_widths.len() * 3 - 1; + + // Top border + out.push_str(&format!("┌{}┐\n", "─".repeat(total_width))); + + for (i, row) in self.rows.iter().enumerate() { + if i == 1 { + // Separator after header + out.push_str(&format!("╞{}╡\n", "═".repeat(total_width))); + } + + for (j, cell) in row.iter().enumerate() { + out.push_str("│ "); + out.push_str(cell); + out.push_str(&" ".repeat(self.column_widths[j] - visible_len(cell))); + out.push_str(" "); + } + out.push_str("│\n"); + } + + // Bottom border + out.push_str(&format!("└{}┘\n", "─".repeat(total_width))); + } + } + + eprintln!("Creating grid for compatibility info display"); + let mut grid = Grid::new(); + + // Add header + grid.add_row(vec!["Key".to_string(), format!("Binary {}", blue(&exe_name)), format!("Module {}", blue(so_name))]); + + for key in all_keys.iter() { + let exported_value = exported.iter().find(|&(k, _)| k == key).map(|(_, v)| v); + let imported_value = imported.iter().find(|&(k, _)| k == key).map(|(_, v)| v); + + let key_column = colored(AnsiColor::GREY, key).to_string(); + let binary_column = format_column(imported_value.as_deref().copied(), exported_value.as_deref().copied(), AnsiColor::RED); + let module_column = format_column(exported_value.as_deref().copied(), imported_value.as_deref().copied(), AnsiColor::GREEN); + + fn format_column(primary: Option<&str>, secondary: Option<&str>, highlight_color: AnsiColor) -> String { + match primary { + Some(value) => { + if secondary.map_or(false, |v| v == value) { + colored(AnsiColor::GREY, value).to_string() + } else { + colored(highlight_color, value).to_string() + } + }, + None => colored(AnsiColor::GREY, "∅").to_string(), + } + } + + grid.add_row(vec![key_column, binary_column, module_column]); + } + + eprintln!("Writing grid to error message"); + grid.write_to(&mut error_message); + + struct MessageBox { + lines: Vec, + max_width: usize, + } + + impl MessageBox { + fn new() -> Self { + MessageBox { + lines: Vec::new(), + max_width: 0, + } + } + + fn add_line(&mut self, line: String) { + self.max_width = self.max_width.max(visible_len(&line)); + self.lines.push(line); + } + + fn add_empty_line(&mut self) { + self.lines.push(String::new()); + } + + fn write_to(&self, out: &mut String) { + let box_width = self.max_width + 4; + + out.push_str("\n"); + out.push_str(&format!("┌{}┐\n", "─".repeat(box_width - 2))); + + for line in &self.lines { + if line.is_empty() { + out.push_str(&format!("│{}│\n", " ".repeat(box_width - 2))); + } else { + let visible_line_len = visible_len(line); + let padding = " ".repeat(box_width - 4 - visible_line_len); + out.push_str(&format!("│ {}{} │\n", line, padding)); + } + } + + out.push_str(&format!("└{}┘", "─".repeat(box_width - 2))); + } + } + + error_message.push_str("\nDifferent feature sets may result in different struct layouts, which\n"); + error_message.push_str("would lead to memory corruption. Instead, we're going to panic now.\n\n"); + + error_message.push_str("More info: \x1b[4m\x1b[34mhttps://crates.io/crates/rubicon\x1b[0m\n"); + + eprintln!("Creating message box for additional information"); + let mut message_box = MessageBox::new(); + message_box.add_line(format!("To fix this issue, {} needs to enable", blue(so_name))); + message_box.add_line(format!("the same cargo features as {} for crate {}.", blue(&exe_name), red(env!("CARGO_PKG_NAME")))); + message_box.add_empty_line(); + message_box.add_line("\x1b[34mHINT:\x1b[0m".to_string()); + message_box.add_line(format!("Run `cargo tree -i {} -e features` from both.", red(env!("CARGO_PKG_NAME")))); + + message_box.write_to(&mut error_message); + error_message.push_str("\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n"); + + eprintln!("Panicking with error message"); + panic!("{}", error_message); + } + }; +} + +#[cfg(all(not(unix), feature = "import-globals"))] +#[macro_export] +macro_rules! compatibility_check { + ($($feature:tt)*) => { + // compatibility checks are only supported on unix-like system + }; +} + +#[cfg(not(any(feature = "export-globals", feature = "import-globals")))] +#[macro_export] +macro_rules! compatibility_check { + ($($feature:tt)*) => {}; +} diff --git a/test-crates/exports/Cargo.toml b/test-crates/exports/Cargo.toml index 268af60..8fcc4fc 100644 --- a/test-crates/exports/Cargo.toml +++ b/test-crates/exports/Cargo.toml @@ -8,3 +8,6 @@ crate-type = ["dylib"] [dependencies] mokio = { version = "0.1.0", path = "../mokio", features = ["export-globals"] } + +[features] +mokio-timer = ["mokio/timer"] diff --git a/test-crates/mod_a/Cargo.lock b/test-crates/mod_a/Cargo.lock index 80a3aab..4f8beef 100644 --- a/test-crates/mod_a/Cargo.lock +++ b/test-crates/mod_a/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "mod_a" version = "0.1.0" @@ -24,15 +40,68 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rubicon" version = "3.0.1" dependencies = [ + "ctor", + "libc", "paste", + "rustc_version", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "soprintln" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc96941d6cac2e2654f62398ae8319ff55a730d6ee2edc78d112f37e5507613" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/test-crates/mod_a/src/lib.rs b/test-crates/mod_a/src/lib.rs index bcfa624..dac6f6a 100644 --- a/test-crates/mod_a/src/lib.rs +++ b/test-crates/mod_a/src/lib.rs @@ -2,7 +2,7 @@ use soprintln::soprintln; use std::sync::atomic::Ordering; #[no_mangle] -pub fn init() { +pub extern "Rust" fn init() { soprintln::init!(); mokio::MOKIO_TL1.with(|s| s.fetch_add(1, Ordering::Relaxed)); mokio::MOKIO_PL1.fetch_add(1, Ordering::Relaxed); diff --git a/test-crates/mod_b/Cargo.lock b/test-crates/mod_b/Cargo.lock index 0ac7beb..f1cbf2f 100644 --- a/test-crates/mod_b/Cargo.lock +++ b/test-crates/mod_b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "mod_b" version = "0.1.0" @@ -24,15 +40,68 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rubicon" version = "3.0.1" dependencies = [ + "ctor", + "libc", "paste", + "rustc_version", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "soprintln" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc96941d6cac2e2654f62398ae8319ff55a730d6ee2edc78d112f37e5507613" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/test-crates/mod_b/src/lib.rs b/test-crates/mod_b/src/lib.rs index bcfa624..dac6f6a 100644 --- a/test-crates/mod_b/src/lib.rs +++ b/test-crates/mod_b/src/lib.rs @@ -2,7 +2,7 @@ use soprintln::soprintln; use std::sync::atomic::Ordering; #[no_mangle] -pub fn init() { +pub extern "Rust" fn init() { soprintln::init!(); mokio::MOKIO_TL1.with(|s| s.fetch_add(1, Ordering::Relaxed)); mokio::MOKIO_PL1.fetch_add(1, Ordering::Relaxed); diff --git a/test-crates/mokio/Cargo.lock b/test-crates/mokio/Cargo.lock index 299478c..2b05b96 100644 --- a/test-crates/mokio/Cargo.lock +++ b/test-crates/mokio/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "mokio" version = "0.1.0" @@ -15,9 +25,61 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rubicon" -version = "2.0.0" +version = "3.0.1" dependencies = [ + "ctor", "paste", + "rustc_version", ] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/test-crates/mokio/Cargo.toml b/test-crates/mokio/Cargo.toml index 88cad95..4b77f5d 100644 --- a/test-crates/mokio/Cargo.toml +++ b/test-crates/mokio/Cargo.toml @@ -7,5 +7,7 @@ edition = "2021" rubicon = { path = "../../rubicon" } [features] +default = [] +timer = [] import-globals = ["rubicon/import-globals"] export-globals = ["rubicon/export-globals"] diff --git a/test-crates/mokio/src/lib.rs b/test-crates/mokio/src/lib.rs index 349c9ab..e1c64b4 100644 --- a/test-crates/mokio/src/lib.rs +++ b/test-crates/mokio/src/lib.rs @@ -1,4 +1,34 @@ -use std::sync::atomic::AtomicU64; +use std::sync::{atomic::AtomicU64, Arc, Mutex}; + +rubicon::compatibility_check! { + ("mokio_pkg_version", env!("CARGO_PKG_VERSION")), + + #[cfg(not(feature = "timer"))] + ("timer", "disabled"), + #[cfg(feature = "timer")] + ("timer", "enabled"), + + #[cfg(not(feature = "timer"))] + ("timer_is_disabled", "1"), +} + +#[derive(Default)] +#[cfg(feature = "timer")] +struct TimerInternals { + #[allow(dead_code)] + random_stuff: [u64; 4], +} + +#[derive(Default)] +pub struct Runtime { + #[cfg(feature = "timer")] + #[allow(dead_code)] + timer: TimerInternals, + + // this field is second on purpose so that it'll be offset + // if the feature is enabled/disabled + pub counter: u64, +} rubicon::process_local! { pub static MOKIO_PL1: AtomicU64 = AtomicU64::new(0); @@ -10,7 +40,7 @@ rubicon::process_local! { rubicon::thread_local! { pub static MOKIO_TL1: AtomicU64 = AtomicU64::new(0); - pub static MOKIO_TL2: AtomicU64 = AtomicU64::new(0); + pub static MOKIO_TL2: Arc> = Arc::new(Mutex::new(Runtime::default())); } pub fn inc_dangerous() -> u64 { diff --git a/test-crates/bin/Cargo.lock b/test-crates/samplebin/Cargo.lock similarity index 82% rename from test-crates/bin/Cargo.lock rename to test-crates/samplebin/Cargo.lock index 4447dcb..91bad90 100644 --- a/test-crates/bin/Cargo.lock +++ b/test-crates/samplebin/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "bin" -version = "0.1.0" -dependencies = [ - "cfg-if", - "exports", - "libloading", - "rubicon", - "soprintln", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -28,9 +17,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets", @@ -54,13 +43,40 @@ name = "rubicon" version = "3.0.1" dependencies = [ "paste", + "rustc_version", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "samplebin" +version = "0.1.0" +dependencies = [ + "cfg-if", + "exports", + "libloading", + "rubicon", + "soprintln", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "soprintln" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc96941d6cac2e2654f62398ae8319ff55a730d6ee2edc78d112f37e5507613" +checksum = "7561c6c12cc21c549193bb82f86800ee0d5d69e85a4393ee1a2766917c2a35cb" [[package]] name = "windows-targets" diff --git a/test-crates/bin/Cargo.toml b/test-crates/samplebin/Cargo.toml similarity index 92% rename from test-crates/bin/Cargo.toml rename to test-crates/samplebin/Cargo.toml index 8b5f6e7..3cff505 100644 --- a/test-crates/bin/Cargo.toml +++ b/test-crates/samplebin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bin" +name = "samplebin" version = "0.1.0" edition = "2021" diff --git a/test-crates/bin/src/main.rs b/test-crates/samplebin/src/main.rs similarity index 66% rename from test-crates/bin/src/main.rs rename to test-crates/samplebin/src/main.rs index a05ff3d..afdaf36 100644 --- a/test-crates/bin/src/main.rs +++ b/test-crates/samplebin/src/main.rs @@ -4,6 +4,64 @@ use exports::{self as _, mokio}; use soprintln::soprintln; fn main() { + struct ModuleSpec { + name: &'static str, + channel: String, + features: Vec, + } + + let mut modules = [ + ModuleSpec { + name: "mod_a", + channel: "stable".to_string(), + features: Default::default(), + }, + ModuleSpec { + name: "mod_b", + channel: "stable".to_string(), + features: Default::default(), + }, + ]; + + for arg in std::env::args().skip(1) { + if let Some(rest) = arg.strip_prefix("--features:") { + let parts: Vec<&str> = rest.splitn(2, '=').collect(); + if parts.len() != 2 { + panic!("Invalid argument format: expected --features:module=feature1,feature2"); + } + let mod_name = parts[0]; + let features = parts[1].split(',').map(|s| s.to_owned()); + let module = modules + .iter_mut() + .find(|m| m.name == mod_name) + .unwrap_or_else(|| panic!("Unknown module: {}", mod_name)); + + for feature in features { + module.features.push(feature); + } + } else if let Some(rest) = arg.strip_prefix("--channel:") { + let parts: Vec<&str> = rest.splitn(2, '=').collect(); + if parts.len() != 2 { + panic!("Invalid argument format: expected --channel:module=(stable|nightly)"); + } + let mod_name = parts[0]; + let channel = parts[1]; + if channel != "stable" && channel != "nightly" { + panic!( + "Invalid channel: {}. Expected 'stable' or 'nightly'", + channel + ); + } + let module = modules + .iter_mut() + .find(|m| m.name == mod_name) + .unwrap_or_else(|| panic!("Unknown module: {}", mod_name)); + module.channel = channel.to_string(); + } else { + panic!("Unknown argument: {}", arg); + } + } + soprintln::init!(); let exe_path = std::env::current_exe().expect("Failed to get current exe path"); let project_root = exe_path @@ -17,10 +75,7 @@ fn main() { soprintln!("app starting up..."); - let modules = ["../mod_a", "../mod_b"]; for module in modules { - soprintln!("building {module}"); - cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let rustflags = "-Clink-arg=-undefined -Clink-arg=dynamic_lookup"; @@ -31,19 +86,24 @@ fn main() { } } - let output = std::process::Command::new("cargo") - .arg("b") + let mut cmd = std::process::Command::new("cargo"); + cmd.arg(format!("+{}", module.channel)) + .arg("build") .env("RUSTFLAGS", rustflags) - .current_dir(module) - .output() - .expect("Failed to execute cargo build"); + .current_dir(format!("../{}", module.name)); + if !module.features.is_empty() { + cmd.arg("--features").arg(module.features.join(",")); + } + + let output = cmd.output().expect("Failed to execute cargo build"); if !output.status.success() { eprintln!( "Error building {}: {}", - module, + module.name, String::from_utf8_lossy(&output.stderr) ); + std::process::exit(1); } } diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..2c085d1 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +node_modules +target diff --git a/tests/Cargo.lock b/tests/Cargo.lock new file mode 100644 index 0000000..7f9e7f1 --- /dev/null +++ b/tests/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "tests" +version = "0.1.0" diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 0000000..9800495 --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 0000000..5259b3f --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^5.3.0" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + } + } +} diff --git a/tests/src/main.rs b/tests/src/main.rs new file mode 100644 index 0000000..cb65fba --- /dev/null +++ b/tests/src/main.rs @@ -0,0 +1,478 @@ +use std::env; +use std::io; +use std::path::Path; +use std::process::{Command, Stdio}; + +#[derive(Clone, Default)] +struct EnvVars { + library_search_paths: Vec, +} + +impl EnvVars { + fn new() -> Self { + EnvVars { + library_search_paths: Vec::new(), + } + } + + fn add_library_path(&mut self, path: String) { + self.library_search_paths.push(path); + } + + fn each_kv(&self, mut f: F) + where + F: FnMut(&str, &str), + { + let platform = env::consts::OS; + let (env_var, separator) = match platform { + "macos" => ("DYLD_LIBRARY_PATH", ":"), + "windows" => ("PATH", ";"), + "linux" => ("LD_LIBRARY_PATH", ":"), + _ => { + eprintln!("❌ Unsupported platform: {}", platform); + std::process::exit(1); + } + }; + + let value = self.library_search_paths.join(separator); + f(env_var, &value); + } + + fn with_additional_library_path(&self, path: String) -> Self { + let mut new_env_vars = self.clone(); + new_env_vars.add_library_path(path); + new_env_vars + } +} + +fn set_env_variables(git_root: &Path) -> EnvVars { + let mut env_vars = EnvVars::new(); + + let rust_sysroot = Command::new("rustc") + .arg("--print") + .arg("sysroot") + .output() + .expect("Failed to execute rustc") + .stdout; + let rust_sysroot = String::from_utf8_lossy(&rust_sysroot).trim().to_string(); + + let rust_nightly_sysroot = Command::new("rustc") + .args(["+nightly", "--print", "sysroot"]) + .output() + .expect("Failed to execute rustc +nightly") + .stdout; + let rust_nightly_sysroot = String::from_utf8_lossy(&rust_nightly_sysroot) + .trim() + .to_string(); + + let platform = env::consts::OS; + + env_vars.add_library_path(format!("{}/lib", rust_sysroot)); + env_vars.add_library_path(format!("{}/lib", rust_nightly_sysroot)); + + match platform { + "macos" | "linux" => { + // okay + } + "windows" => { + let current_path = env::var("PATH").unwrap_or_default(); + env_vars.add_library_path(current_path); + } + _ => { + eprintln!("❌ Unsupported platform: {}", platform); + std::process::exit(1); + } + } + + println!("\nEnvironment Variables Summary:"); + env_vars.each_kv(|key, value| { + println!("{}: {}", key, value); + }); + + env_vars +} + +fn run_command(command: &[&str], env_vars: &EnvVars) -> io::Result<(bool, String)> { + use std::io::{BufRead, BufReader}; + use std::sync::mpsc; + use std::thread; + + let program = command[0]; + let args = &command[1..]; + + println!("Running command: {} {:?}", program, args); + + let mut command = Command::new(program); + command + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + env_vars.each_kv(|key, value| { + command.env(key, value); + }); + + let mut child = command.spawn()?; + + let (tx_stdout, rx_stdout) = mpsc::channel(); + let (tx_stderr, rx_stderr) = mpsc::channel(); + + let stdout = child.stdout.take().expect("Failed to capture stdout"); + let stderr = child.stderr.take().expect("Failed to capture stderr"); + + let stdout_thread = thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines() { + let line = line.expect("Failed to read line from stdout"); + println!("{}", line); + tx_stdout.send(line).expect("Failed to send stdout line"); + } + }); + + let stderr_thread = thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + let line = line.expect("Failed to read line from stderr"); + eprintln!("{}", line); + tx_stderr.send(line).expect("Failed to send stderr line"); + } + }); + + let mut output = String::new(); + + for line in rx_stdout.iter() { + output.push_str(&line); + output.push('\n'); + } + + for line in rx_stderr.iter() { + output.push_str(&line); + output.push('\n'); + } + + stdout_thread.join().expect("stdout thread panicked"); + stderr_thread.join().expect("stderr thread panicked"); + + let status = child.wait()?; + if !status.success() { + if let Some(exit_code) = status.code() { + eprintln!( + "\n🔍 \x1b[1;90mProcess exited with code {} (0x{:X})\x1b[0m", + exit_code, exit_code + ); + } else { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + if let Some(signal) = status.signal() { + let signal_name = match signal { + 1 => "SIGHUP", + 2 => "SIGINT", + 3 => "SIGQUIT", + 4 => "SIGILL", + 6 => "SIGABRT", + 8 => "SIGFPE", + 9 => "SIGKILL", + 11 => "SIGSEGV", + 13 => "SIGPIPE", + 14 => "SIGALRM", + 15 => "SIGTERM", + _ => "Unknown", + }; + eprintln!( + "\n🔍 \x1b[1;90mProcess terminated by signal {} ({})\x1b[0m", + signal, signal_name + ); + } else { + eprintln!("\n🔍 \x1b[1;90mProcess exited with unknown status\x1b[0m"); + } + } + #[cfg(not(unix))] + { + eprintln!("\n🔍 \x1b[1;90mProcess exited with unknown status\x1b[0m"); + } + } + } + Ok((status.success(), output)) +} + +fn check_feature_mismatch(output: &str) -> bool { + output.contains("Feature mismatch for crate") +} + +struct TestCase { + name: &'static str, + build_command: &'static [&'static str], + run_command: &'static [&'static str], + expected_result: &'static str, + check_feature_mismatch: bool, + allowed_to_fail: bool, +} + +static TEST_CASES: &[TestCase] = &[ + TestCase { + name: "Tests pass (debug)", + build_command: &[ + "cargo", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &["./test-crates/samplebin/target/debug/samplebin"], + expected_result: "success", + check_feature_mismatch: false, + allowed_to_fail: false, + }, + TestCase { + name: "Tests pass (release)", + build_command: &[ + "cargo", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + "--release", + ], + run_command: &["./test-crates/samplebin/target/release/samplebin"], + expected_result: "success", + check_feature_mismatch: false, + allowed_to_fail: false, + }, + TestCase { + name: "Bin stable, mod_a nightly (should fail)", + build_command: &[ + "cargo", + "+stable", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--channel:mod_a=nightly", + ], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: cfg!(target_os = "linux"), + }, + TestCase { + name: "Bin nightly, mod_a stable (should fail)", + build_command: &[ + "cargo", + "+nightly", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--channel:mod_a=stable", + ], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: cfg!(target_os = "linux"), + }, + TestCase { + name: "All nightly (should work)", + build_command: &[ + "cargo", + "+nightly", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--channel:mod_a=nightly", + "--channel:mod_b=nightly", + ], + expected_result: "success", + check_feature_mismatch: false, + allowed_to_fail: false, + }, + TestCase { + name: "Bin has mokio-timer feature (should fail)", + build_command: &[ + "cargo", + "build", + "--features=exports/mokio-timer", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &["./test-crates/samplebin/target/debug/samplebin"], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: false, + }, + TestCase { + name: "mod_a has mokio-timer feature (should fail)", + build_command: &[ + "cargo", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--features:mod_a=mokio/timer", + ], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: false, + }, + TestCase { + name: "mod_b has mokio-timer feature (should fail)", + build_command: &[ + "cargo", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--features:mod_b=mokio/timer", + ], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: false, + }, + TestCase { + name: "all mods have mokio-timer feature (should fail)", + build_command: &[ + "cargo", + "build", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--features:mod_a=mokio/timer", + "--features:mod_b=mokio/timer", + ], + expected_result: "fail", + check_feature_mismatch: true, + allowed_to_fail: false, + }, + TestCase { + name: "bin and mods have mokio-timer feature (should work)", + build_command: &[ + "cargo", + "build", + "--features=exports/mokio-timer", + "--manifest-path", + "test-crates/samplebin/Cargo.toml", + ], + run_command: &[ + "./test-crates/samplebin/target/debug/samplebin", + "--features:mod_a=mokio/timer", + "--features:mod_b=mokio/timer", + ], + expected_result: "success", + check_feature_mismatch: false, + allowed_to_fail: false, + }, +]; + +fn run_tests() -> io::Result<()> { + println!("\n🚀 \x1b[1;36mChanging working directory to Git root...\x1b[0m"); + let mut git_root = env::current_dir()?; + + while !Path::new(&git_root).join(".git").exists() { + if let Some(parent) = git_root.parent() { + git_root = parent.to_path_buf(); + } else { + eprintln!("❌ \x1b[1;31mGit root not found. Exiting.\x1b[0m"); + std::process::exit(1); + } + } + + env::set_current_dir(&git_root)?; + println!( + "📂 \x1b[1;32mChanged working directory to:\x1b[0m {}", + git_root.display() + ); + + println!("🌟 \x1b[1;36mSetting up environment variables...\x1b[0m"); + let env_vars = set_env_variables(&git_root); + + println!("🌙 \x1b[1;34mInstalling nightly Rust...\x1b[0m"); + run_command(&["rustup", "toolchain", "add", "nightly"], &env_vars)?; + + println!("\n🧪 \x1b[1;35mRunning tests...\x1b[0m"); + + for (index, test) in TEST_CASES.iter().enumerate() { + { + let test_info = format!("Running test {}: {}", index + 1, test.name); + let box_width = test_info.chars().count() + 4; + let padding = box_width - 2 - test_info.chars().count(); + let left_padding = padding / 2; + let right_padding = padding - left_padding; + + println!("\n\x1b[1;33m╔{}╗\x1b[0m", "═".repeat(box_width - 2)); + println!( + "\x1b[1;33m║\x1b[0m{}\x1b[1;36m{}\x1b[0m{}\x1b[1;33m║\x1b[0m", + " ".repeat(left_padding), + test_info, + " ".repeat(right_padding), + ); + println!("\x1b[1;33m╚{}╝\x1b[0m", "═".repeat(box_width - 2)); + } + + println!("🏗️ \x1b[1;34mBuilding...\x1b[0m"); + let build_result = run_command(test.build_command, &Default::default())?; + if !build_result.0 { + eprintln!("❌ \x1b[1;31mBuild failed. Exiting tests.\x1b[0m"); + std::process::exit(1); + } + + println!("▶️ \x1b[1;32mRunning...\x1b[0m"); + let profile = if test.build_command.contains(&"--release") { + "release" + } else { + "debug" + }; + let additional_path = git_root + .join("test-crates") + .join("samplebin") + .join("target") + .join(profile); + let env_vars = + env_vars.with_additional_library_path(additional_path.to_string_lossy().into_owned()); + + let (success, output) = run_command(test.run_command, &env_vars)?; + + match (test.expected_result, success) { + ("success", true) => println!("✅ \x1b[1;32mTest passed as expected.\x1b[0m"), + ("fail", false) if test.check_feature_mismatch && check_feature_mismatch(&output) => { + println!("✅ \x1b[1;33mTest failed with feature mismatch as expected.\x1b[0m") + } + ("fail", false) if test.check_feature_mismatch => { + eprintln!("❌ \x1b[1;31mTest failed, but not with the expected feature mismatch error.\x1b[0m"); + if test.allowed_to_fail || cfg!(windows) { + println!("⚠️ \x1b[1;33mTest was allowed to fail.\x1b[0m"); + } else { + std::process::exit(1); + } + } + _ => { + eprintln!( + "❌ \x1b[1;31mTest result unexpected. Expected {}, but got {}.\x1b[0m", + test.expected_result, + if success { "success" } else { "failure" } + ); + if test.allowed_to_fail { + println!("⚠️ \x1b[1;33mTest was allowed to fail.\x1b[0m"); + } else { + std::process::exit(1); + } + } + } + } + + println!("\n🎉 \x1b[1;32mAll tests passed successfully.\x1b[0m"); + Ok(()) +} + +fn main() -> io::Result<()> { + run_tests() +}