From 5f031bbb8cce13f3a2ff88a384b019b243cf5239 Mon Sep 17 00:00:00 2001 From: chethanuk Date: Tue, 19 Aug 2025 17:30:56 +0000 Subject: [PATCH 1/9] Adding windows os in ci --- .github/workflows/ci.yaml | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 300a4f9..9721b2b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,19 +11,23 @@ on: permissions: checks: write pull-requests: write + env: CARGO_TERM_COLOR: always jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: Swatinem/rust-cache@v2 with: - prefix-key: v1-rust + prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: true - name: Setup Rust @@ -31,10 +35,18 @@ jobs: with: toolchain: stable override: true - - name: Install moonbit + - name: Install moonbit (Unix) + if: runner.os != 'Windows' run: | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s -- 0.6.19 echo "$HOME/.moon/bin" >> $GITHUB_PATH + - name: Install moonbit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + Invoke-WebRequest -Uri "https://cli.moonbitlang.com/binaries/moonbit-win32_x64-0.6.19.zip" -OutFile "moonbit.zip" + Expand-Archive -Path "moonbit.zip" -DestinationPath "$env:USERPROFILE\.moon" + echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm working-directory: core @@ -44,16 +56,20 @@ jobs: run: cargo clippy -- -Dwarnings - name: Build run: cargo build --all-features --all-targets + test: needs: [build] - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: Swatinem/rust-cache@v2 with: - prefix-key: v1-rust + prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: false - name: Setup Rust @@ -64,10 +80,18 @@ jobs: - uses: cargo-bins/cargo-binstall@main - name: Install wasmtime-cli run: cargo binstall --force --locked wasmtime-cli@33.0.0 - - name: Install moonbit + - name: Install moonbit (Unix) + if: runner.os != 'Windows' run: | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s -- 0.6.19 echo "$HOME/.moon/bin" >> $GITHUB_PATH + - name: Install moonbit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + Invoke-WebRequest -Uri "https://cli.moonbitlang.com/binaries/moonbit-win32_x64-0.6.19.zip" -OutFile "moonbit.zip" + Expand-Archive -Path "moonbit.zip" -DestinationPath "$env:USERPROFILE\.moon" + echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm working-directory: core @@ -80,10 +104,11 @@ jobs: report_paths: "**/target/report-*.xml" detailed_summary: true include_passed: true + publish: needs: [test] if: "startsWith(github.ref, 'refs/tags/v')" - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Publish on Ubuntu only steps: - name: Checkout uses: actions/checkout@v4 From 615cfb3a3787a9c27c02b55c3590e39434eef67d Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Tue, 19 Aug 2025 14:18:58 -0700 Subject: [PATCH 2/9] Adding support for windows os --- moonc_wasm/.mise.toml | 32 ++ moonc_wasm/Cargo.toml | 20 + moonc_wasm/build.rs | 10 + moonc_wasm/src/cross_platform.rs | 546 +++++++++++++++++++++++ moonc_wasm/src/lib.rs | 1 + moonc_wasm/src/wasmoo_extern.rs | 191 ++++---- moonc_wasm/tests/cross_platform_tests.rs | 295 ++++++++++++ moonc_wasm/tests/integration_tests.rs | 294 ++++++++++++ moonc_wasm/tests/simple_test.rs | 30 ++ moonc_wasm/tests/unit_tests.rs | 281 ++++++++++++ 10 files changed, 1618 insertions(+), 82 deletions(-) create mode 100644 moonc_wasm/.mise.toml create mode 100644 moonc_wasm/build.rs create mode 100644 moonc_wasm/src/cross_platform.rs create mode 100644 moonc_wasm/tests/cross_platform_tests.rs create mode 100644 moonc_wasm/tests/integration_tests.rs create mode 100644 moonc_wasm/tests/simple_test.rs create mode 100644 moonc_wasm/tests/unit_tests.rs diff --git a/moonc_wasm/.mise.toml b/moonc_wasm/.mise.toml new file mode 100644 index 0000000..773da5d --- /dev/null +++ b/moonc_wasm/.mise.toml @@ -0,0 +1,32 @@ +[tools] +# Install stable Rust via rustup through mise and add wasm32-wasip1 target +rust = { version = "stable", targets = "wasm32-wasip1" } + +[tasks.build_windows] +run = "cargo build --release --target x86_64-pc-windows-msvc" + +[tasks.build_windows_aarch64] +run = "cargo build --release --target aarch64-pc-windows-msvc" + +[tasks.test] +description = "Run all tests with output" +run = "cargo test --all -- --nocapture" + +[tasks.test-unit] +description = "Run unit tests only" +run = "cargo test --lib -- --nocapture" + +[tasks.test-integration] +description = "Run integration tests only" +run = "cargo test --test '*' -- --nocapture" + +[tasks.test-quiet] +description = "Run all tests quietly" +run = "cargo test --all" + +[tasks.test-verbose] +description = "Run all tests with verbose output" +run = "cargo test --all --verbose -- --nocapture" + +[tasks.release_windows] +depends = ["build_windows"] diff --git a/moonc_wasm/Cargo.toml b/moonc_wasm/Cargo.toml index 238327f..5fd4ba1 100644 --- a/moonc_wasm/Cargo.toml +++ b/moonc_wasm/Cargo.toml @@ -9,3 +9,23 @@ edition = "2024" anyhow = { version = "1.0.86" } v8 = "0.106.0" libc = "0.2.172" +cfg-if = "1.0" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61.3", features = [ + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_Foundation", + "Win32_System_Console", + "Win32_System_IO", + "Win32_System_Registry", + "Win32_System_Diagnostics_Etw" +] } +windows-sys = { version = "0.60.2", features = [ + "Win32_Foundation", + "Win32_Storage_FileSystem", +] } + +[dev-dependencies] +rstest = "0.21" +tempfile = "3.8" \ No newline at end of file diff --git a/moonc_wasm/build.rs b/moonc_wasm/build.rs new file mode 100644 index 0000000..27fb705 --- /dev/null +++ b/moonc_wasm/build.rs @@ -0,0 +1,10 @@ +fn main() { + #[cfg(target_os = "windows")] + { + // Link Windows system libraries required by V8 + println!("cargo:rustc-link-lib=advapi32"); // For registry functions + println!("cargo:rustc-link-lib=tdh"); // For ETW (Event Tracing for Windows) + println!("cargo:rustc-link-lib=user32"); // For additional Windows APIs + println!("cargo:rustc-link-lib=kernel32"); // For kernel functions + } +} diff --git a/moonc_wasm/src/cross_platform.rs b/moonc_wasm/src/cross_platform.rs new file mode 100644 index 0000000..de5c585 --- /dev/null +++ b/moonc_wasm/src/cross_platform.rs @@ -0,0 +1,546 @@ +//! Cross-platform file system operations for moonbit-component-generator +//! Provides Windows-compatible alternatives to Unix-only file operations. + +#![allow(dead_code)] + +use std::fs::{self, File, Metadata, Permissions, FileType}; +use std::path::Path; +use std::io; +use std::io::IsTerminal; + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(windows)] { + use std::os::windows::fs::MetadataExt; + use std::os::windows::io::{AsRawHandle, RawHandle}; + use windows::Win32::Foundation::{HANDLE, FILETIME, INVALID_HANDLE_VALUE}; + use windows::Win32::Storage::FileSystem::{ + CreateFileW, + FILE_ATTRIBUTE_NORMAL, + FILE_FLAG_OVERLAPPED, + FILE_GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING, + SetFileTime, GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, + }; + use windows::Win32::System::Console::{GetConsoleMode, CONSOLE_MODE}; + use windows::core::{PCWSTR, Error as WindowsError}; + pub type RawFd = RawHandle; + } else { + use std::os::unix::fs::{MetadataExt, PermissionsExt, OpenOptionsExt, FileTypeExt}; + use std::os::unix::io::{AsRawFd, RawFd}; + use libc::{timeval, utimes as libc_utimes}; + pub type RawFd = RawFd; + } +} + +/// Cross-platform file operation constants +pub mod platform_constants { + cfg_if::cfg_if! { + if #[cfg(windows)] { + pub const O_NONBLOCK: i32 = 0x1000; // Custom flag for Windows async I/O + pub const O_NOCTTY: i32 = 0x0000; // No-op on Windows (no TTY concept) + pub const O_DSYNC: i32 = 0x0000; // No direct equivalent on Windows + pub const O_SYNC: i32 = 0x0000; // No direct equivalent on Windows + } else if #[cfg(unix)] { + pub const O_NONBLOCK: i32 = libc::O_NONBLOCK; + pub const O_NOCTTY: i32 = libc::O_NOCTTY; + pub const O_DSYNC: i32 = libc::O_DSYNC; + pub const O_SYNC: i32 = libc::O_SYNC; + } else { + pub const O_NONBLOCK: i32 = 0; + pub const O_NOCTTY: i32 = 0; + pub const O_DSYNC: i32 = 0; + pub const O_SYNC: i32 = 0; + } + } +} + +/// Cross-platform metadata extractor that works on Windows and Unix +pub struct MetadataExtractor; + +impl MetadataExtractor { + /// Extract file mode (Unix permissions or Windows attributes) + pub fn mode(metadata: &Metadata) -> u32 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + metadata.file_attributes() + } else if #[cfg(unix)] { + metadata.mode() + } else { + 0o644 // Default file permissions + } + } + } + + /// Extract device ID + pub fn dev(metadata: &Metadata) -> u64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Use stable Windows API - file attributes as device ID + metadata.file_attributes() as u64 + } else if #[cfg(unix)] { + metadata.dev() + } else { + 0 + } + } + } + + /// Extract inode number + pub fn ino(metadata: &Metadata) -> u64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Use file size + creation time as pseudo-inode + metadata.file_size().wrapping_add(metadata.creation_time()) + } else if #[cfg(unix)] { + metadata.ino() + } else { + 0 + } + } + } + + /// Extract number of hard links + pub fn nlink(metadata: &Metadata) -> u64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Windows files typically have 1 link + 1 + } else if #[cfg(unix)] { + metadata.nlink() + } else { + 1 + } + } + } + + /// Extract user ID (Windows: simulate with 0, Unix: actual UID) + pub fn uid(metadata: &Metadata) -> u32 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Windows uses SIDs, not UIDs. For compatibility, return 0 + // In production, you might want to hash the current user SID + 0 + } else if #[cfg(unix)] { + metadata.uid() + } else { + 0 + } + } + } + + /// Extract group ID (Windows: simulate with 0, Unix: actual GID) + pub fn gid(metadata: &Metadata) -> u32 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Windows uses SIDs, not GIDs. For compatibility, return 0 + 0 + } else if #[cfg(unix)] { + metadata.gid() + } else { + 0 + } + } + } + + /// Extract device ID + pub fn rdev(metadata: &Metadata) -> u64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Use file attributes as rdev equivalent + metadata.file_attributes() as u64 + } else if #[cfg(unix)] { + metadata.rdev() + } else { + 0 + } + } + } + + /// Extract file size + pub fn size(metadata: &Metadata) -> u64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + metadata.file_size() + } else if #[cfg(unix)] { + metadata.size() + } else { + metadata.len() // Standard library fallback + } + } + } + + /// Extract access time as Unix timestamp + pub fn atime(metadata: &Metadata) -> i64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + Self::filetime_to_unix_timestamp(metadata.last_access_time()) + } else if #[cfg(unix)] { + metadata.atime() + } else { + 0 + } + } + } + + /// Extract modification time as Unix timestamp + pub fn mtime(metadata: &Metadata) -> i64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + Self::filetime_to_unix_timestamp(metadata.last_write_time()) + } else if #[cfg(unix)] { + metadata.mtime() + } else { + 0 + } + } + } + + /// Extract change/creation time as Unix timestamp + pub fn ctime(metadata: &Metadata) -> i64 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Windows has creation time, not change time + Self::filetime_to_unix_timestamp(metadata.creation_time()) + } else if #[cfg(unix)] { + metadata.ctime() + } else { + 0 + } + } + } + + /// Convert Windows FILETIME to Unix timestamp + #[cfg(windows)] + fn filetime_to_unix_timestamp(filetime: u64) -> i64 { + // Windows FILETIME: 100-nanosecond intervals since January 1, 1601 UTC + // Unix timestamp: seconds since January 1, 1970 UTC + const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; // seconds between epochs + const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second + + let unix_seconds = (filetime / FILETIME_UNITS_PER_SECOND).saturating_sub(WINDOWS_TO_UNIX_OFFSET); + unix_seconds as i64 + } +} + +/// Cross-platform permissions constructor +pub struct PermissionsBuilder; + +impl PermissionsBuilder { + /// Create permissions from Unix-style mode + pub fn from_mode(mode: u32) -> Permissions { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Windows doesn't use Unix-style modes + // Return default permissions or create from attributes + Self::create_windows_permissions(mode) + } else if #[cfg(unix)] { + use std::os::unix::fs::PermissionsExt; + Permissions::from_mode(mode) + } else { + // Fallback: create default permissions + fs::metadata(".").unwrap().permissions() + } + } + } + + #[cfg(windows)] + fn create_windows_permissions(mode: u32) -> Permissions { + // Convert Unix mode to Windows file attributes + let read_only = (mode & 0o200) == 0; // Owner write bit + + // Create a temporary file to get permissions, then modify + let temp_path = std::env::temp_dir().join("temp_permissions"); + if let Ok(_) = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&temp_path) + { + let mut perms = fs::metadata(&temp_path).unwrap().permissions(); + perms.set_readonly(read_only); + + // Clean up + let _ = fs::remove_file(temp_path); + + perms + } else { + // Fallback to current directory permissions + fs::metadata(".").unwrap().permissions() + } + } +} + +/// Cross-platform raw file descriptor extraction +pub trait RawFdExt { + fn as_raw_fd(&self) -> RawFd; +} + +impl RawFdExt for File { + fn as_raw_fd(&self) -> RawFd { + cfg_if::cfg_if! { + if #[cfg(windows)] { + use std::os::windows::io::AsRawHandle; + self.as_raw_handle() + } else if #[cfg(unix)] { + use std::os::unix::io::AsRawFd; + self.as_raw_fd() + } else { + 0 as RawFd + } + } + } +} + +/// Cross-platform isatty function (renamed to avoid conflicts) +pub fn host_isatty(fd: RawFd) -> i32 { + cfg_if::cfg_if! { + if #[cfg(windows)] { + // Check standard streams first + if fd.is_null() { // stdin + return if std::io::stdin().is_terminal() { 1 } else { 0 }; + } + if fd == std::io::stdout().as_raw_handle() { // stdout + return if std::io::stdout().is_terminal() { 1 } else { 0 }; + } + if fd == std::io::stderr().as_raw_handle() { // stderr + return if std::io::stderr().is_terminal() { 1 } else { 0 }; + } + + // For other handles, use Win32 API + unsafe { + let handle = HANDLE(fd); + let mut mode = CONSOLE_MODE(0); + if GetConsoleMode(handle, &mut mode).is_ok() { + 1 + } else { + 0 + } + } + } else if #[cfg(unix)] { + unsafe { libc::isatty(fd) } + } else { + 0 + } + } +} + +/// Trait to extend std::fs::Metadata with cross-platform methods +pub trait CrossPlatformMetadataExt { + fn cross_dev(&self) -> u64; + fn cross_ino(&self) -> u64; + fn cross_mode(&self) -> u32; + fn cross_nlink(&self) -> u64; + fn cross_uid(&self) -> u32; + fn cross_gid(&self) -> u32; + fn cross_rdev(&self) -> u64; + fn cross_size(&self) -> u64; + fn cross_atime(&self) -> i64; + fn cross_mtime(&self) -> i64; + fn cross_ctime(&self) -> i64; +} + +impl CrossPlatformMetadataExt for Metadata { + fn cross_dev(&self) -> u64 { MetadataExtractor::dev(self) } + fn cross_ino(&self) -> u64 { MetadataExtractor::ino(self) } + fn cross_mode(&self) -> u32 { MetadataExtractor::mode(self) } + fn cross_nlink(&self) -> u64 { MetadataExtractor::nlink(self) } + fn cross_uid(&self) -> u32 { MetadataExtractor::uid(self) } + fn cross_gid(&self) -> u32 { MetadataExtractor::gid(self) } + fn cross_rdev(&self) -> u64 { MetadataExtractor::rdev(self) } + fn cross_size(&self) -> u64 { MetadataExtractor::size(self) } + fn cross_atime(&self) -> i64 { MetadataExtractor::atime(self) } + fn cross_mtime(&self) -> i64 { MetadataExtractor::mtime(self) } + fn cross_ctime(&self) -> i64 { MetadataExtractor::ctime(self) } +} + +/// Cross-platform FileType extensions +pub trait CrossPlatformFileTypeExt { + fn cross_is_char_device(&self) -> bool; + fn cross_is_block_device(&self) -> bool; + fn cross_is_fifo(&self) -> bool; + fn cross_is_socket(&self) -> bool; +} + +impl CrossPlatformFileTypeExt for FileType { + fn cross_is_char_device(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::FileTypeExt; + self.is_char_device() + } else { + false // Windows doesn't have char devices in the same way + } + } + } + + fn cross_is_block_device(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::FileTypeExt; + self.is_block_device() + } else { + false // Windows doesn't have block devices in the same way + } + } + } + + fn cross_is_fifo(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::FileTypeExt; + self.is_fifo() + } else { + false // Windows doesn't have FIFOs + } + } + } + + fn cross_is_socket(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::FileTypeExt; + self.is_socket() + } else { + false // Windows doesn't have Unix domain sockets in filesystem + } + } + } +} + +pub fn cross_utimes(path: &str, atime: f64, mtime: f64) -> io::Result<()> { + cfg_if::cfg_if! { + if #[cfg(unix)] { + use libc::{timeval, suseconds_t, utimes as libc_utimes}; + let c_path = std::ffi::CString::new(path) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid path"))?; + + let atime_tv = timeval { + tv_sec: atime as i64, + tv_usec: (atime.fract() * 1_000_000.0) as suseconds_t, + }; + let mtime_tv = timeval { + tv_sec: mtime as i64, + tv_usec: (mtime.fract() * 1_000_000.0) as suseconds_t, + }; + + let times = [atime_tv, mtime_tv]; + + unsafe { + if libc_utimes(c_path.as_ptr(), times.as_ptr()) == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + } else if #[cfg(windows)] { + // FIXED Windows implementation + use windows::core::HSTRING; + use windows::Win32::Storage::FileSystem::{ + CreateFileW, FILE_GENERIC_WRITE, FILE_SHARE_READ, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, SetFileTime + }; + use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, FILETIME}; + use windows::core::PCWSTR; + + let wide_path = HSTRING::from(path); + + unsafe { + // FIX ERROR 3: Correct CreateFileW parameter types + let handle = CreateFileW( + PCWSTR(wide_path.as_ptr()), + FILE_GENERIC_WRITE.0, + FILE_SHARE_READ, + None, // Option<*const SECURITY_ATTRIBUTES> ✓ + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, // Remove .0 ✓ + None, // Option ✓ + )?; + + if handle == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + + // Convert Unix timestamps to Windows FILETIME + let unix_to_filetime = |timestamp: f64| -> FILETIME { + const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; + const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; + + let windows_time = ((timestamp as u64) + WINDOWS_TO_UNIX_OFFSET) * FILETIME_UNITS_PER_SECOND; + + FILETIME { + dwLowDateTime: (windows_time & 0xFFFFFFFF) as u32, + dwHighDateTime: (windows_time >> 32) as u32, + } + }; + + let atime_ft = unix_to_filetime(atime); + let mtime_ft = unix_to_filetime(mtime); + + // FIX ERROR 4: SetFileTime returns Result, not Option + SetFileTime( + handle, + None, + Some(&atime_ft), + Some(&mtime_ft), + ).map_err(|_| io::Error::last_os_error()) // Result::map_err ✓ + } + } else { + Err(io::Error::new(io::ErrorKind::Unsupported, "utimes not supported on this platform")) + } + } +} + + +/// Convert Unix timestamp to Windows FILETIME +#[cfg(windows)] +fn unix_to_filetime(timestamp: f64) -> FILETIME { + const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; // seconds between epochs + const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second + + let windows_time = ((timestamp as u64) + WINDOWS_TO_UNIX_OFFSET) * FILETIME_UNITS_PER_SECOND; + + FILETIME { + dwLowDateTime: (windows_time & 0xFFFFFFFF) as u32, + dwHighDateTime: (windows_time >> 32) as u32, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + + #[test] + fn test_cross_platform_metadata() { + let test_path = "test_metadata.txt"; + + // Create test file + { + let mut file = File::create(test_path).unwrap(); + writeln!(file, "Test content for metadata").unwrap(); + } + + // Test metadata extraction + let metadata = fs::metadata(test_path).unwrap(); + + assert!(MetadataExtractor::mode(&metadata) > 0); + assert!(MetadataExtractor::size(&metadata) > 0); + assert_eq!(MetadataExtractor::nlink(&metadata), 1); + + // Test trait extension + assert!(metadata.cross_mode() > 0); + assert!(metadata.cross_size() > 0); + + // Cleanup + fs::remove_file(test_path).unwrap(); + } + + #[test] + fn test_permission_creation() { + let perms = PermissionsBuilder::from_mode(0o644); + // Just ensure it doesn't panic and creates something + assert!(!perms.readonly() || perms.readonly()); // Always true, just test creation + } +} diff --git a/moonc_wasm/src/lib.rs b/moonc_wasm/src/lib.rs index d24512f..5b1f19b 100644 --- a/moonc_wasm/src/lib.rs +++ b/moonc_wasm/src/lib.rs @@ -1,4 +1,5 @@ mod wasmoo_extern; +pub mod cross_platform; pub fn run_wasmoo(argv: Vec) -> anyhow::Result<()> { let isolate = &mut v8::Isolate::new(Default::default()); diff --git a/moonc_wasm/src/wasmoo_extern.rs b/moonc_wasm/src/wasmoo_extern.rs index f99e82f..e8bf18d 100644 --- a/moonc_wasm/src/wasmoo_extern.rs +++ b/moonc_wasm/src/wasmoo_extern.rs @@ -3,14 +3,21 @@ use std::{ ffi::CString, fs::{self, File, OpenOptions, Permissions, metadata}, io::{IsTerminal, Read, Write}, - os::{ - fd::AsRawFd, - unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}, - }, path::Path, process::{Command, Stdio}, }; +use crate::cross_platform::{ + MetadataExtractor, CrossPlatformMetadataExt, CrossPlatformFileTypeExt, PermissionsBuilder, RawFdExt, + platform_constants, host_isatty, cross_utimes +}; + +cfg_if::cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::OpenOptionsExt; + } +} + // getenv : JSString -> JSString fn getenv( scope: &mut v8::HandleScope, @@ -150,7 +157,7 @@ fn chmod( let path = Path::new(&path); let mode = args.get(1); let mode = mode.to_number(scope).unwrap().value() as u32; - let permission = Permissions::from_mode(mode); + let permission = PermissionsBuilder::from_mode(mode); match fs::set_permissions(path, permission) { Err(err) => { let message = v8::String::new(scope, &err.to_string()).unwrap(); @@ -299,19 +306,33 @@ fn open( } let mut custom_flags = 0; if (flags & O_NONBLOCK) != 0 { - custom_flags |= libc::O_NONBLOCK; + custom_flags |= platform_constants::O_NONBLOCK; } if (flags & O_NOCTTY) != 0 { - custom_flags |= libc::O_NOCTTY; + custom_flags |= platform_constants::O_NOCTTY; } if (flags & O_DSYNC) != 0 { - custom_flags |= libc::O_DSYNC; + custom_flags |= platform_constants::O_DSYNC; } if (flags & O_SYNC) != 0 { - custom_flags |= libc::O_SYNC; + custom_flags |= platform_constants::O_SYNC; } - opts.custom_flags(custom_flags); - opts.mode((mode & 0o777) as u32); // assure permission is legal + cfg_if::cfg_if! { + if #[cfg(unix)] { + opts.custom_flags(custom_flags as u32); + opts.mode((mode & 0o777) as u32); + } else { + // Windows: set the common FILE_ATTRIBUTE_NORMAL and optionally overlapped I/O + use std::os::windows::fs::OpenOptionsExt; + use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; + use windows::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; + + opts.attributes(FILE_ATTRIBUTE_NORMAL.0); + // If you want nonblocking async I/O: + opts.custom_flags(FILE_FLAG_OVERLAPPED.0); + } + } + match opts.open(path) { Err(err) => { let message = v8::String::new(scope, &err.to_string()).unwrap(); @@ -413,7 +434,7 @@ fn access( return; } Ok(metadata) => { - let mode = metadata.permissions().mode(); + let mode_bits = MetadataExtractor::mode(&metadata); if mode & 0o111 == 0 { let message = v8::String::new(scope, "execute permission denied").unwrap(); let exn = v8::Exception::error(scope, message); @@ -617,34 +638,40 @@ fn timeval_from_f64(t: f64) -> std::io::Result { "Time value must be finite", )); } - + let total_usec = (t * 1_000_000.0).round() as i64; - let sec = total_usec.div_euclid(1_000_000); let usec = total_usec.rem_euclid(1_000_000); - - Ok(libc::timeval { - tv_sec: sec as libc::time_t, - tv_usec: usec as libc::suseconds_t, - }) + + // Build timeval with platform-specific field types + cfg_if::cfg_if! { + if #[cfg(unix)] { + Ok(libc::timeval { + tv_sec: sec as libc::time_t, // Fixed: use 'sec' not 'sec_i64' + tv_usec: usec as libc::suseconds_t, // Fixed: use 'usec' not 'usec_i64' + }) + } else if #[cfg(windows)] { + Ok(libc::timeval { + // On Windows both fields are c_long (i32) + tv_sec: sec as libc::c_long, // Fixed: use 'sec' not 'atime_tv.tv_sec' + tv_usec: usec as libc::c_long, // Fixed: use 'usec' not 'atime_tv.tv_usec' + }) + } else { + // Other platforms - use safe defaults + Ok(libc::timeval { + tv_sec: sec as i64, + tv_usec: usec as i64, + }) + } + } } -fn __utimes(path: String, atime: f64, mtime: f64) -> std::io::Result<()> { - let c_path = CString::new(path)?; - - let atime_tv = timeval_from_f64(atime)?; - let mtime_tv = timeval_from_f64(mtime)?; - let times = [atime_tv, mtime_tv]; +fn __utimes(path: String, atime: f64, mtime: f64) -> std::io::Result<()> { + cross_utimes(&path, atime, mtime) +} - let result = unsafe { libc::utimes(c_path.as_ptr(), times.as_ptr()) }; - if result == 0 { - Ok(()) - } else { - Err(std::io::Error::last_os_error()) - } -} // utimes: JSString as Path, F64 as AccessTime, F64 as ModifyTime -> undefined fn utimes( @@ -678,7 +705,7 @@ fn exit(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, _ret: } // isatty: FileDescriptor -> Number(1 | 0) -fn isatty( +fn js_isatty( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut ret: v8::ReturnValue, @@ -703,7 +730,7 @@ fn isatty( let context = scope.get_current_context(); let fd_table = context.get_slot_mut::().unwrap(); match fd_table.get(fd) { - Ok(file) => unsafe { libc::isatty(file.as_raw_fd()) }, + Ok(file) => host_isatty(file.as_raw_fd()), Err(_) => 0, } }; @@ -772,7 +799,7 @@ fn mkdir( scope.throw_exception(exn); return; } - let permissions = fs::Permissions::from_mode(mode as u32); + let permissions = PermissionsBuilder::from_mode(mode as u32); match fs::set_permissions(path, permissions) { Err(err) => { let message = v8::String::new(scope, &err.to_string()).unwrap(); @@ -1110,15 +1137,15 @@ fn stat( 0 } else if filetype.is_dir() { 1 - } else if filetype.is_char_device() { + } else if filetype.cross_is_char_device() { 2 - } else if filetype.is_block_device() { + } else if filetype.cross_is_block_device() { 3 } else if filetype.is_symlink() { 4 - } else if filetype.is_fifo() { + } else if filetype.cross_is_fifo() { 5 - } else if filetype.is_socket() { + } else if filetype.cross_is_socket() { 6 } else { panic!() @@ -1131,47 +1158,47 @@ fn stat( stat.set(scope, id.into(), kind.into()); let id = v8::String::new(scope, "dev").unwrap(); - let dev = v8::Number::new(scope, metadata.dev() as f64); + let dev = v8::Number::new(scope, metadata.cross_dev() as f64); stat.set(scope, id.into(), dev.into()); let id = v8::String::new(scope, "ino").unwrap(); - let ino = v8::Number::new(scope, metadata.ino() as f64); + let ino = v8::Number::new(scope, metadata.cross_ino() as f64); stat.set(scope, id.into(), ino.into()); let id = v8::String::new(scope, "mode").unwrap(); - let mode = v8::Number::new(scope, metadata.mode() as f64); + let mode = v8::Number::new(scope, metadata.cross_mode() as f64); stat.set(scope, id.into(), mode.into()); let id = v8::String::new(scope, "nlink").unwrap(); - let nlink = v8::Number::new(scope, metadata.nlink() as f64); + let nlink = v8::Number::new(scope, metadata.cross_nlink() as f64); stat.set(scope, id.into(), nlink.into()); let id = v8::String::new(scope, "uid").unwrap(); - let uid = v8::Number::new(scope, metadata.uid() as f64); + let uid = v8::Number::new(scope, metadata.cross_uid() as f64); stat.set(scope, id.into(), uid.into()); let id = v8::String::new(scope, "gid").unwrap(); - let gid = v8::Number::new(scope, metadata.gid() as f64); + let gid = v8::Number::new(scope, metadata.cross_gid() as f64); stat.set(scope, id.into(), gid.into()); let id = v8::String::new(scope, "rdev").unwrap(); - let rdev = v8::Number::new(scope, metadata.rdev() as f64); + let rdev = v8::Number::new(scope, metadata.cross_rdev() as f64); stat.set(scope, id.into(), rdev.into()); let id = v8::String::new(scope, "size").unwrap(); - let size = v8::Number::new(scope, metadata.size() as f64); + let size = v8::Number::new(scope, metadata.cross_size() as f64); stat.set(scope, id.into(), size.into()); let id = v8::String::new(scope, "atime").unwrap(); - let atime = v8::Number::new(scope, metadata.atime() as f64); + let atime = v8::Number::new(scope, metadata.cross_atime() as f64); stat.set(scope, id.into(), atime.into()); let id = v8::String::new(scope, "mtime").unwrap(); - let mtime = v8::Number::new(scope, metadata.mtime() as f64); + let mtime = v8::Number::new(scope, metadata.cross_mtime() as f64); stat.set(scope, id.into(), mtime.into()); let id = v8::String::new(scope, "ctime").unwrap(); - let ctime = v8::Number::new(scope, metadata.ctime() as f64); + let ctime = v8::Number::new(scope, metadata.cross_ctime() as f64); stat.set(scope, id.into(), ctime.into()); ret.set(stat.into()); @@ -1201,15 +1228,15 @@ fn lstat( 0 } else if filetype.is_dir() { 1 - } else if filetype.is_char_device() { + } else if filetype.cross_is_char_device() { 2 - } else if filetype.is_block_device() { + } else if filetype.cross_is_block_device() { 3 } else if filetype.is_symlink() { 4 - } else if filetype.is_fifo() { + } else if filetype.cross_is_fifo() { 5 - } else if filetype.is_socket() { + } else if filetype.cross_is_socket() { 6 } else { panic!() @@ -1222,47 +1249,47 @@ fn lstat( stat.set(scope, id.into(), kind.into()); let id = v8::String::new(scope, "dev").unwrap(); - let dev = v8::Number::new(scope, metadata.dev() as f64); + let dev = v8::Number::new(scope, metadata.cross_dev() as f64); stat.set(scope, id.into(), dev.into()); let id = v8::String::new(scope, "ino").unwrap(); - let ino = v8::Number::new(scope, metadata.ino() as f64); + let ino = v8::Number::new(scope, metadata.cross_ino() as f64); stat.set(scope, id.into(), ino.into()); let id = v8::String::new(scope, "mode").unwrap(); - let mode = v8::Number::new(scope, metadata.mode() as f64); + let mode = v8::Number::new(scope, metadata.cross_mode() as f64); stat.set(scope, id.into(), mode.into()); let id = v8::String::new(scope, "nlink").unwrap(); - let nlink = v8::Number::new(scope, metadata.nlink() as f64); + let nlink = v8::Number::new(scope, metadata.cross_nlink() as f64); stat.set(scope, id.into(), nlink.into()); let id = v8::String::new(scope, "uid").unwrap(); - let uid = v8::Number::new(scope, metadata.uid() as f64); + let uid = v8::Number::new(scope, metadata.cross_uid() as f64); stat.set(scope, id.into(), uid.into()); let id = v8::String::new(scope, "gid").unwrap(); - let gid = v8::Number::new(scope, metadata.gid() as f64); + let gid = v8::Number::new(scope, metadata.cross_gid() as f64); stat.set(scope, id.into(), gid.into()); let id = v8::String::new(scope, "rdev").unwrap(); - let rdev = v8::Number::new(scope, metadata.rdev() as f64); + let rdev = v8::Number::new(scope, metadata.cross_rdev() as f64); stat.set(scope, id.into(), rdev.into()); let id = v8::String::new(scope, "size").unwrap(); - let size = v8::Number::new(scope, metadata.size() as f64); + let size = v8::Number::new(scope, metadata.cross_size() as f64); stat.set(scope, id.into(), size.into()); let id = v8::String::new(scope, "atime").unwrap(); - let atime = v8::Number::new(scope, metadata.atime() as f64); + let atime = v8::Number::new(scope, metadata.cross_atime() as f64); stat.set(scope, id.into(), atime.into()); let id = v8::String::new(scope, "mtime").unwrap(); - let mtime = v8::Number::new(scope, metadata.mtime() as f64); + let mtime = v8::Number::new(scope, metadata.cross_mtime() as f64); stat.set(scope, id.into(), mtime.into()); let id = v8::String::new(scope, "ctime").unwrap(); - let ctime = v8::Number::new(scope, metadata.ctime() as f64); + let ctime = v8::Number::new(scope, metadata.cross_ctime() as f64); stat.set(scope, id.into(), ctime.into()); ret.set(stat.into()); @@ -1301,15 +1328,15 @@ fn fstat( 0 } else if filetype.is_dir() { 1 - } else if filetype.is_char_device() { + } else if filetype.cross_is_char_device() { 2 - } else if filetype.is_block_device() { + } else if filetype.cross_is_block_device() { 3 } else if filetype.is_symlink() { 4 - } else if filetype.is_fifo() { + } else if filetype.cross_is_fifo() { 5 - } else if filetype.is_socket() { + } else if filetype.cross_is_socket() { 6 } else { panic!() @@ -1322,47 +1349,47 @@ fn fstat( stat.set(scope, id.into(), kind.into()); let id = v8::String::new(scope, "dev").unwrap(); - let dev = v8::Number::new(scope, metadata.dev() as f64); + let dev = v8::Number::new(scope, metadata.cross_dev() as f64); stat.set(scope, id.into(), dev.into()); let id = v8::String::new(scope, "ino").unwrap(); - let ino = v8::Number::new(scope, metadata.ino() as f64); + let ino = v8::Number::new(scope, metadata.cross_ino() as f64); stat.set(scope, id.into(), ino.into()); let id = v8::String::new(scope, "mode").unwrap(); - let mode = v8::Number::new(scope, metadata.mode() as f64); + let mode = v8::Number::new(scope, metadata.cross_mode() as f64); stat.set(scope, id.into(), mode.into()); let id = v8::String::new(scope, "nlink").unwrap(); - let nlink = v8::Number::new(scope, metadata.nlink() as f64); + let nlink = v8::Number::new(scope, metadata.cross_nlink() as f64); stat.set(scope, id.into(), nlink.into()); let id = v8::String::new(scope, "uid").unwrap(); - let uid = v8::Number::new(scope, metadata.uid() as f64); + let uid = v8::Number::new(scope, metadata.cross_uid() as f64); stat.set(scope, id.into(), uid.into()); let id = v8::String::new(scope, "gid").unwrap(); - let gid = v8::Number::new(scope, metadata.gid() as f64); + let gid = v8::Number::new(scope, metadata.cross_gid() as f64); stat.set(scope, id.into(), gid.into()); let id = v8::String::new(scope, "rdev").unwrap(); - let rdev = v8::Number::new(scope, metadata.rdev() as f64); + let rdev = v8::Number::new(scope, metadata.cross_rdev() as f64); stat.set(scope, id.into(), rdev.into()); let id = v8::String::new(scope, "size").unwrap(); - let size = v8::Number::new(scope, metadata.size() as f64); + let size = v8::Number::new(scope, metadata.cross_size() as f64); stat.set(scope, id.into(), size.into()); let id = v8::String::new(scope, "atime").unwrap(); - let atime = v8::Number::new(scope, metadata.atime() as f64); + let atime = v8::Number::new(scope, metadata.cross_atime() as f64); stat.set(scope, id.into(), atime.into()); let id = v8::String::new(scope, "mtime").unwrap(); - let mtime = v8::Number::new(scope, metadata.mtime() as f64); + let mtime = v8::Number::new(scope, metadata.cross_mtime() as f64); stat.set(scope, id.into(), mtime.into()); let id = v8::String::new(scope, "ctime").unwrap(); - let ctime = v8::Number::new(scope, metadata.ctime() as f64); + let ctime = v8::Number::new(scope, metadata.cross_ctime() as f64); stat.set(scope, id.into(), ctime.into()); ret.set(stat.into()); @@ -1378,7 +1405,7 @@ fn fchmod( let fd = fd.to_number(scope).unwrap().value() as i32; let mode = args.get(1); let mode = mode.to_number(scope).unwrap().value() as u32; - let permission = Permissions::from_mode(mode); + let permission = PermissionsBuilder::from_mode(mode); let context = scope.get_current_context(); let fd_table = context.get_slot_mut::().unwrap(); let file = match fd_table.get(fd) { @@ -1753,7 +1780,7 @@ pub fn init_wasmoo<'s>( let ident = v8::String::new(scope, "fchmod").unwrap(); obj.set(scope, ident.into(), fchmod.into()); - let isatty = v8::FunctionTemplate::new(scope, isatty); + let isatty = v8::FunctionTemplate::new(scope, js_isatty); let isatty = isatty.get_function(scope).unwrap(); let ident = v8::String::new(scope, "isatty").unwrap(); obj.set(scope, ident.into(), isatty.into()); diff --git a/moonc_wasm/tests/cross_platform_tests.rs b/moonc_wasm/tests/cross_platform_tests.rs new file mode 100644 index 0000000..3e5b4b2 --- /dev/null +++ b/moonc_wasm/tests/cross_platform_tests.rs @@ -0,0 +1,295 @@ +//! Integration tests for cross-platform file system operations +//! +//! This module contains comprehensive parameterized tests using rstest +//! to verify cross-platform compatibility of metadata operations. + +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::PathBuf; +use tempfile::TempDir; +use rstest::rstest; + +use moonc_wasm::cross_platform::{ + CrossPlatformMetadataExt, PermissionsBuilder, MetadataExtractor, platform_constants +}; + +/// Test fixture that creates a temporary directory for each test +#[rstest] +fn temp_dir() -> TempDir { + TempDir::new().expect("Failed to create temporary directory") +} + +/// Test cross-platform metadata extraction with various file permissions +#[rstest] +#[case::read_only(0o444, "read-only file")] +#[case::read_write(0o644, "read-write file")] +#[case::executable(0o755, "executable file")] +#[case::full_permissions(0o777, "full permissions")] +#[case::no_write(0o555, "no write permissions")] +#[case::owner_only(0o600, "owner only permissions")] +fn test_metadata_permissions( + temp_dir: TempDir, + #[case] mode: u32, + #[case] description: &str, +) { + let file_path = temp_dir.path().join("test_file.txt"); + + // Create test file with content + let mut file = File::create(&file_path).expect("Failed to create test file"); + writeln!(file, "Test content for {}", description).expect("Failed to write to file"); + drop(file); + + // Set permissions using cross-platform builder + let permissions = PermissionsBuilder::from_mode(mode); + fs::set_permissions(&file_path, permissions).expect("Failed to set permissions"); + + // Test metadata extraction + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Verify cross-platform methods work + let extracted_mode = metadata.cross_mode(); + let size = metadata.cross_size(); + let uid = metadata.cross_uid(); + let gid = metadata.cross_gid(); + let nlink = metadata.cross_nlink(); + + // Basic assertions + assert!(size > 0, "File size should be greater than 0"); + assert!(nlink >= 1, "Number of links should be at least 1"); + + // Platform-specific assertions + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, mode should reflect the permissions we set + assert_eq!(extracted_mode & 0o777, mode, "Mode should match set permissions"); + } else if #[cfg(windows)] { + // On Windows, mode represents file attributes + assert!(extracted_mode > 0, "Windows file attributes should be present"); + } + } + + println!("✅ {} - Mode: 0o{:o}, Size: {}, UID: {}, GID: {}, Links: {}", + description, extracted_mode, size, uid, gid, nlink); +} + +/// Test metadata extraction for different file types and edge cases +#[rstest] +#[case::empty_file(0, "empty file")] +#[case::small_file(42, "small file")] +#[case::medium_file(1024, "medium file")] +#[case::large_content(65536, "large file")] +fn test_metadata_file_sizes( + temp_dir: TempDir, + #[case] content_size: usize, + #[case] description: &str, +) { + let file_path = temp_dir.path().join("size_test.txt"); + + // Create file with specific content size + let content = "x".repeat(content_size); + fs::write(&file_path, &content).expect("Failed to write file"); + + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Test all cross-platform metadata methods + let size = metadata.cross_size(); + let dev = metadata.cross_dev(); + let ino = metadata.cross_ino(); + let atime = metadata.cross_atime(); + let mtime = metadata.cross_mtime(); + let ctime = metadata.cross_ctime(); + + // Verify size matches expected + assert_eq!(size as usize, content_size, "File size should match written content"); + + // Verify other metadata fields are reasonable + assert!(dev >= 0, "Device ID should be non-negative"); + assert!(ino >= 0, "Inode should be non-negative"); + assert!(atime > 0 || atime == 0, "Access time should be valid timestamp or 0"); + assert!(mtime > 0, "Modification time should be positive timestamp"); + assert!(ctime >= 0, "Creation/change time should be non-negative"); + + println!("✅ {} - Size: {}, Dev: {}, Ino: {}, ATime: {}, MTime: {}, CTime: {}", + description, size, dev, ino, atime, mtime, ctime); +} + +/// Test platform constants compatibility +#[rstest] +#[case::nonblock(platform_constants::O_NONBLOCK, "O_NONBLOCK")] +#[case::noctty(platform_constants::O_NOCTTY, "O_NOCTTY")] +#[case::dsync(platform_constants::O_DSYNC, "O_DSYNC")] +#[case::sync(platform_constants::O_SYNC, "O_SYNC")] +fn test_platform_constants(#[case] constant: i32, #[case] name: &str) { + // Verify constants are defined and have reasonable values + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, constants should match libc values or be reasonable + assert!(constant >= 0, "{} should be non-negative on Unix", name); + } else if #[cfg(windows)] { + // On Windows, constants should be defined (may be 0 for unsupported features) + assert!(constant >= 0, "{} should be non-negative on Windows", name); + } + } + + println!("✅ {} = 0x{:x} ({})", name, constant, constant); +} + +/// Test permissions builder with various Unix permission modes +#[rstest] +#[case::no_permissions(0o000)] +#[case::owner_read(0o400)] +#[case::owner_write(0o200)] +#[case::owner_execute(0o100)] +#[case::group_read(0o040)] +#[case::group_write(0o020)] +#[case::group_execute(0o010)] +#[case::other_read(0o004)] +#[case::other_write(0o002)] +#[case::other_execute(0o001)] +#[case::common_file(0o644)] +#[case::common_executable(0o755)] +#[case::restricted(0o600)] +#[case::public_read(0o644)] +fn test_permissions_builder(temp_dir: TempDir, #[case] mode: u32) { + let file_path = temp_dir.path().join("perm_test.txt"); + + // Create test file + File::create(&file_path).expect("Failed to create test file"); + + // Test permissions builder + let permissions = PermissionsBuilder::from_mode(mode); + + // Apply permissions + let result = fs::set_permissions(&file_path, permissions); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, setting permissions should generally succeed + assert!(result.is_ok(), "Setting permissions 0o{:o} should succeed on Unix", mode); + + // Verify the permissions were set correctly + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + let actual_mode = metadata.cross_mode() & 0o777; + + // Account for umask and filesystem limitations + if mode & 0o200 == 0 { + // If we tried to remove write permission, verify it's readonly + assert!(metadata.permissions().readonly() || (actual_mode & 0o200) == 0, + "File should be readonly when write bit is cleared"); + } + } else if #[cfg(windows)] { + // On Windows, some permission operations may not be supported + // but the builder should not panic + println!("Windows permissions builder test for mode 0o{:o}: {:?}", mode, result); + } + } + + println!("✅ Permissions 0o{:o} - Builder succeeded", mode); +} + +/// Test edge cases and error conditions +#[rstest] +#[case::nonexistent_file("nonexistent.txt")] +#[case::empty_filename("")] +fn test_metadata_edge_cases(temp_dir: TempDir, #[case] filename: &str) { + let file_path = temp_dir.path().join(filename); + + // Attempt to get metadata for nonexistent file + let result = fs::metadata(&file_path); + + match filename { + "" => { + // Empty filename should fail + assert!(result.is_err(), "Empty filename should fail"); + } + "nonexistent.txt" => { + // Nonexistent file should fail + assert!(result.is_err(), "Nonexistent file should fail"); + } + _ => {} + } + + println!("✅ Edge case '{}' handled correctly", filename); +} + +/// Test cross-platform file operations with OpenOptions +#[rstest] +#[case::read_only(true, false, false)] +#[case::write_only(false, true, false)] +#[case::read_write(true, true, false)] +#[case::append_mode(false, true, true)] +fn test_file_operations_cross_platform( + temp_dir: TempDir, + #[case] read: bool, + #[case] write: bool, + #[case] append: bool, +) { + let file_path = temp_dir.path().join("ops_test.txt"); + + // Create initial file + fs::write(&file_path, "initial content").expect("Failed to create initial file"); + + // Test OpenOptions with different combinations + let mut opts = OpenOptions::new(); + opts.read(read).write(write).append(append); + + let result = opts.open(&file_path); + + if read || write { + assert!(result.is_ok(), "File should open with read={}, write={}, append={}", + read, write, append); + + if let Ok(file) = result { + let metadata = file.metadata().expect("Failed to get file metadata"); + + // Test metadata extraction on open file + let size = metadata.cross_size(); + let mode = metadata.cross_mode(); + + assert!(size > 0, "File should have content"); + assert!(mode > 0, "File should have valid mode"); + + println!("✅ File operations (r={}, w={}, a={}) - Size: {}, Mode: 0o{:o}", + read, write, append, size, mode); + } + } +} + +/// Benchmark-style test to verify performance of cross-platform operations +#[rstest] +fn test_metadata_performance(temp_dir: TempDir) { + let file_path = temp_dir.path().join("perf_test.txt"); + + // Create test file + let content = "x".repeat(1024); + fs::write(&file_path, &content).expect("Failed to create test file"); + + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Perform multiple metadata extractions to test performance + let iterations = 1000; + let start = std::time::Instant::now(); + + for _ in 0..iterations { + let _mode = metadata.cross_mode(); + let _size = metadata.cross_size(); + let _uid = metadata.cross_uid(); + let _gid = metadata.cross_gid(); + let _nlink = metadata.cross_nlink(); + let _dev = metadata.cross_dev(); + let _ino = metadata.cross_ino(); + let _rdev = metadata.cross_rdev(); + let _atime = metadata.cross_atime(); + let _mtime = metadata.cross_mtime(); + let _ctime = metadata.cross_ctime(); + } + + let duration = start.elapsed(); + let avg_time = duration.as_nanos() / iterations as u128; + + // Performance should be reasonable (less than 1ms per operation set) + assert!(avg_time < 1_000_000, "Average metadata extraction should be under 1ms"); + + println!("✅ Performance test - {} iterations in {:?} (avg: {}ns per iteration)", + iterations, duration, avg_time); +} diff --git a/moonc_wasm/tests/integration_tests.rs b/moonc_wasm/tests/integration_tests.rs new file mode 100644 index 0000000..342fe18 --- /dev/null +++ b/moonc_wasm/tests/integration_tests.rs @@ -0,0 +1,294 @@ +//! Integration tests for wasmoo_extern functionality +//! +//! Tests the complete integration of cross-platform operations with the v8 JavaScript engine + +use std::fs::{self, File}; +use tempfile::TempDir; +use rstest::rstest; + +use moonc_wasm::cross_platform::{CrossPlatformMetadataExt, PermissionsBuilder, RawFdExt, host_isatty}; + +#[cfg(test)] +mod file_operations_integration { + use super::*; + + /// Test file descriptor operations across platforms + #[rstest] + fn test_file_descriptor_operations() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("fd_test.txt"); + + // Create and open file + let content = "File descriptor test content"; + fs::write(&file_path, content).expect("Failed to write file"); + + let file = File::open(&file_path).expect("Failed to open file"); + + // Test raw file descriptor extraction + let raw_fd = file.as_raw_fd(); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + assert!(raw_fd >= 0, "Unix file descriptor should be non-negative"); + + // Test host_isatty function + let tty_result = host_isatty(raw_fd); + assert_eq!(tty_result, 0, "Regular file should not be a TTY"); + } else if #[cfg(windows)] { + // On Windows, raw_fd is a HANDLE + println!("Windows file handle: {:?}", raw_fd); + + // Test host_isatty function (should return 0 for regular files) + let tty_result = host_isatty(raw_fd); + assert_eq!(tty_result, 0, "Regular file should not be a TTY on Windows"); + } + } + + println!("✅ File descriptor operations work across platforms"); + } + + /// Test standard I/O file descriptors + #[rstest] + #[case::stdin(0, "stdin")] + #[case::stdout(1, "stdout")] + #[case::stderr(2, "stderr")] + fn test_standard_io_descriptors(#[case] fd: i32, #[case] name: &str) { + // Test host_isatty on standard file descriptors + let tty_result = host_isatty(fd as _); + + // Standard streams might or might not be TTYs depending on environment + assert!(tty_result == 0 || tty_result == 1, + "{} host_isatty should return 0 or 1", name); + + println!("✅ {} (fd={}) host_isatty result: {}", name, fd, tty_result); + } +} + +#[cfg(test)] +mod metadata_integration_tests { + use super::*; + + /// Test complete metadata workflow as used in wasmoo_extern + #[rstest] + #[case::regular_file("regular.txt", "Regular file content")] + #[case::executable_script("script.sh", "#!/bin/bash\necho 'Hello World'")] + fn test_complete_metadata_workflow( + #[case] filename: &str, + #[case] content: &str, + ) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join(filename); + + // Create file with specific content + fs::write(&file_path, content).expect("Failed to write file"); + + // Set executable permissions for script files + if filename.ends_with(".sh") { + let permissions = PermissionsBuilder::from_mode(0o755); + fs::set_permissions(&file_path, permissions).expect("Failed to set permissions"); + } + + // Get metadata and extract all fields (as done in wasmoo_extern stat functions) + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Extract all metadata fields using cross-platform methods + let kind = if metadata.is_file() { 0 } else { 1 }; // File type classification + let dev = metadata.cross_dev(); + let ino = metadata.cross_ino(); + let mode = metadata.cross_mode(); + let nlink = metadata.cross_nlink(); + let uid = metadata.cross_uid(); + let gid = metadata.cross_gid(); + let rdev = metadata.cross_rdev(); + let size = metadata.cross_size(); + let atime = metadata.cross_atime(); + let mtime = metadata.cross_mtime(); + let ctime = metadata.cross_ctime(); + + // Verify all fields have reasonable values + assert_eq!(kind, 0, "Should be classified as regular file"); + assert_eq!(size as usize, content.len(), "Size should match content"); + assert!(nlink >= 1, "Should have at least one link"); + assert!(mtime > 0, "Modification time should be positive"); + + // Simulate v8 object creation (as done in wasmoo_extern) + let stat_object = format!( + "{{ kind: {}, dev: {}, ino: {}, mode: {}, nlink: {}, uid: {}, gid: {}, rdev: {}, size: {}, atime: {}, mtime: {}, ctime: {} }}", + kind, dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime + ); + + println!("✅ Complete metadata workflow for {} - {}", filename, stat_object); + } + + /// Test metadata operations on different file types + #[rstest] + fn test_directory_metadata() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let dir_path = temp_dir.path().join("test_subdir"); + + // Create subdirectory + fs::create_dir(&dir_path).expect("Failed to create directory"); + + // Get directory metadata + let metadata = fs::metadata(&dir_path).expect("Failed to get directory metadata"); + + // Test directory-specific metadata + assert!(metadata.is_dir(), "Should be identified as directory"); + + let kind = if metadata.is_dir() { 1 } else { 0 }; + let mode = metadata.cross_mode(); + let nlink = metadata.cross_nlink(); + + assert_eq!(kind, 1, "Directory should have kind = 1"); + assert!(mode > 0, "Directory should have valid mode"); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, directories typically have nlink >= 2 (. and ..) + assert!(nlink >= 2, "Unix directory should have nlink >= 2"); + } else if #[cfg(windows)] { + // On Windows, nlink behavior may differ + assert!(nlink >= 1, "Windows directory should have nlink >= 1"); + } + } + + println!("✅ Directory metadata - Kind: {}, Mode: 0o{:o}, Links: {}", kind, mode, nlink); + } +} + +#[cfg(test)] +mod error_handling_integration { + use super::*; + + /// Test error handling in cross-platform operations + #[rstest] + fn test_nonexistent_file_handling() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let nonexistent_path = temp_dir.path().join("does_not_exist.txt"); + + // Attempt to get metadata for nonexistent file + let result = fs::metadata(&nonexistent_path); + assert!(result.is_err(), "Should fail for nonexistent file"); + + // Verify error type + let error = result.unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "Should be NotFound error"); + + println!("✅ Nonexistent file error handling works correctly"); + } + + /// Test permission denied scenarios + #[rstest] + fn test_permission_scenarios() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("restricted.txt"); + + // Create file + fs::write(&file_path, "restricted content").expect("Failed to create file"); + + // Try to set very restrictive permissions + let permissions = PermissionsBuilder::from_mode(0o000); + let perm_result = fs::set_permissions(&file_path, permissions); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, this should succeed + assert!(perm_result.is_ok(), "Setting restrictive permissions should succeed on Unix"); + + // Verify the file is now readonly + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + assert!(metadata.permissions().readonly(), "File should be readonly"); + } else if #[cfg(windows)] { + // On Windows, behavior may vary + println!("Windows restrictive permissions result: {:?}", perm_result); + } + } + + println!("✅ Permission scenarios handled appropriately"); + } +} + +#[cfg(test)] +mod performance_integration { + use super::*; + + /// Test performance of metadata operations under load + #[rstest] + fn test_metadata_performance_integration() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + + // Create multiple test files + let file_count = 100; + let mut file_paths = Vec::new(); + + for i in 0..file_count { + let file_path = temp_dir.path().join(format!("perf_test_{}.txt", i)); + let content = format!("Performance test file {} content", i); + fs::write(&file_path, &content).expect("Failed to create test file"); + file_paths.push(file_path); + } + + // Measure time for complete metadata extraction workflow + let start = std::time::Instant::now(); + + for file_path in &file_paths { + let metadata = fs::metadata(file_path).expect("Failed to get metadata"); + + // Extract all metadata fields (simulating wasmoo_extern usage) + let _dev = metadata.cross_dev(); + let _ino = metadata.cross_ino(); + let _mode = metadata.cross_mode(); + let _nlink = metadata.cross_nlink(); + let _uid = metadata.cross_uid(); + let _gid = metadata.cross_gid(); + let _rdev = metadata.cross_rdev(); + let _size = metadata.cross_size(); + let _atime = metadata.cross_atime(); + let _mtime = metadata.cross_mtime(); + let _ctime = metadata.cross_ctime(); + } + + let duration = start.elapsed(); + let avg_time = duration.as_micros() / file_count as u128; + + // Performance should be reasonable (less than 1ms per file) + assert!(avg_time < 1000, "Average metadata extraction should be under 1ms per file"); + + println!("✅ Performance integration - {} files in {:?} (avg: {}μs per file)", + file_count, duration, avg_time); + } + + /// Test concurrent metadata operations + #[rstest] + fn test_concurrent_metadata_operations() { + use std::thread; + use std::sync::Arc; + + let temp_dir = Arc::new(TempDir::new().expect("Failed to create temp dir")); + let file_path = temp_dir.path().join("concurrent_test.txt"); + + // Create test file + fs::write(&file_path, "Concurrent access test").expect("Failed to create file"); + + // Spawn multiple threads to access metadata concurrently + let handles: Vec<_> = (0..10).map(|i| { + let file_path = file_path.clone(); + thread::spawn(move || { + for _ in 0..10 { + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + let _size = metadata.cross_size(); + let _mode = metadata.cross_mode(); + let _mtime = metadata.cross_mtime(); + } + i + }) + }).collect(); + + // Wait for all threads to complete + for handle in handles { + handle.join().expect("Thread should complete successfully"); + } + + println!("✅ Concurrent metadata operations completed successfully"); + } +} diff --git a/moonc_wasm/tests/simple_test.rs b/moonc_wasm/tests/simple_test.rs new file mode 100644 index 0000000..ea72dc7 --- /dev/null +++ b/moonc_wasm/tests/simple_test.rs @@ -0,0 +1,30 @@ +//! Simple test to verify basic functionality + +use moonc_wasm::cross_platform::CrossPlatformMetadataExt; +use std::fs; +use tempfile::TempDir; + +#[test] +fn test_basic_functionality() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("test.txt"); + + fs::write(&file_path, "test content").expect("Failed to write file"); + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + let size = metadata.cross_size(); + assert_eq!(size, 12); // "test content" is 12 bytes + + println!("✅ Basic cross-platform test passed - Size: {}", size); +} + +#[test] +fn test_platform_constants() { + use moonc_wasm::cross_platform::platform_constants; + + // Just verify constants are defined + let _nonblock = platform_constants::O_NONBLOCK; + let _noctty = platform_constants::O_NOCTTY; + + println!("✅ Platform constants are accessible"); +} diff --git a/moonc_wasm/tests/unit_tests.rs b/moonc_wasm/tests/unit_tests.rs new file mode 100644 index 0000000..72f1008 --- /dev/null +++ b/moonc_wasm/tests/unit_tests.rs @@ -0,0 +1,281 @@ +//! Unit tests for cross-platform module components +//! +//! These tests focus on individual components and internal functionality + +use std::fs::{self, File}; +use tempfile::TempDir; +use rstest::rstest; + +use moonc_wasm::cross_platform::{ + MetadataExtractor, PermissionsBuilder, CrossPlatformMetadataExt, platform_constants +}; + +#[cfg(test)] +mod metadata_extractor_tests { + use super::*; + + /// Test MetadataExtractor with real file metadata + #[rstest] + fn test_metadata_extractor_basic() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("test.txt"); + + // Create test file with known content + let content = "Hello, cross-platform world!"; + fs::write(&file_path, content).expect("Failed to write file"); + + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Test all extractor methods + let mode = MetadataExtractor::mode(&metadata); + let _dev = MetadataExtractor::dev(&metadata); + let _ino = MetadataExtractor::ino(&metadata); + let nlink = MetadataExtractor::nlink(&metadata); + let uid = MetadataExtractor::uid(&metadata); + let gid = MetadataExtractor::gid(&metadata); + let _rdev = MetadataExtractor::rdev(&metadata); + let size = MetadataExtractor::size(&metadata); + let atime = MetadataExtractor::atime(&metadata); + let mtime = MetadataExtractor::mtime(&metadata); + let ctime = MetadataExtractor::ctime(&metadata); + + // Verify reasonable values + assert!(mode > 0, "Mode should be positive"); + assert_eq!(size as usize, content.len(), "Size should match content length"); + assert!(nlink >= 1, "Should have at least one link"); + assert!(mtime > 0, "Modification time should be positive"); + + // Platform-specific checks + cfg_if::cfg_if! { + if #[cfg(unix)] { + assert!(uid >= 0, "UID should be non-negative on Unix"); + assert!(gid >= 0, "GID should be non-negative on Unix"); + assert!(dev > 0, "Device ID should be positive on Unix"); + assert!(ino > 0, "Inode should be positive on Unix"); + } else if #[cfg(windows)] { + assert_eq!(uid, 0, "UID should be 0 on Windows (simulated)"); + assert_eq!(gid, 0, "GID should be 0 on Windows (simulated)"); + } + } + + println!("✅ MetadataExtractor - Mode: 0o{:o}, Size: {}, Links: {}", mode, size, nlink); + } + + /// Test metadata extraction with different file types + #[rstest] + #[case::regular_file("regular.txt", "Regular file content")] + #[case::empty_file("empty.txt", "")] + #[case::binary_file("binary.bin", "\x00\x01\x02\x03\x7F")] + fn test_metadata_extractor_file_types( + #[case] filename: &str, + #[case] content: &str, + ) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join(filename); + + fs::write(&file_path, content).expect("Failed to write file"); + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Test size extraction + let size = MetadataExtractor::size(&metadata); + assert_eq!(size as usize, content.len(), "Size should match for {}", filename); + + // Test timestamps + let atime = MetadataExtractor::atime(&metadata); + let mtime = MetadataExtractor::mtime(&metadata); + let ctime = MetadataExtractor::ctime(&metadata); + + assert!(mtime > 0 || content.is_empty(), "MTime should be positive for non-empty files"); + assert!(ctime >= 0, "CTime should be non-negative"); + + println!("✅ File type {} - Size: {}, MTime: {}", filename, size, mtime); + } +} + +#[cfg(test)] +mod permissions_builder_tests { + use super::*; + + /// Test PermissionsBuilder with standard Unix permission values + #[rstest] + #[case::owner_read_write(0o600, "owner read-write")] + #[case::standard_file(0o644, "standard file permissions")] + #[case::executable(0o755, "executable permissions")] + #[case::world_writable(0o666, "world writable")] + #[case::no_permissions(0o000, "no permissions")] + #[case::all_permissions(0o777, "all permissions")] + fn test_permissions_builder_modes(#[case] mode: u32, #[case] description: &str) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("perm_test.txt"); + + // Create test file + File::create(&file_path).expect("Failed to create file"); + + // Test permissions builder + let permissions = PermissionsBuilder::from_mode(mode); + let result = fs::set_permissions(&file_path, permissions); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + assert!(result.is_ok(), "Setting {} (0o{:o}) should succeed on Unix", description, mode); + + // Verify permissions were applied + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + let actual_mode = metadata.cross_mode() & 0o777; + + // Check readonly flag for write permissions + if mode & 0o200 == 0 { + assert!(metadata.permissions().readonly() || (actual_mode & 0o200) == 0, + "File should be readonly when write bit is cleared"); + } + } else if #[cfg(windows)] { + // On Windows, just verify the builder doesn't panic + println!("Windows permissions test for {}: {:?}", description, result); + } + } + + println!("✅ {} (0o{:o}) - Builder completed", description, mode); + } + + /// Test edge cases for permissions + #[rstest] + #[case::setuid_bit(0o4755, "setuid bit")] + #[case::setgid_bit(0o2755, "setgid bit")] + #[case::sticky_bit(0o1755, "sticky bit")] + #[case::all_special_bits(0o7777, "all special bits")] + fn test_permissions_special_bits(#[case] mode: u32, #[case] description: &str) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("special_perm_test.txt"); + + File::create(&file_path).expect("Failed to create file"); + + // Test with special permission bits + let permissions = PermissionsBuilder::from_mode(mode); + let result = fs::set_permissions(&file_path, permissions); + + // Special bits may not be supported on all filesystems + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, this might succeed or fail depending on filesystem support + println!("Unix special permissions test for {} (0o{:o}): {:?}", description, mode, result); + } else if #[cfg(windows)] { + // Windows doesn't support Unix special bits + println!("Windows special permissions test for {} (0o{:o}): {:?}", description, mode, result); + } + } + + println!("✅ {} (0o{:o}) - Special bits test completed", description, mode); + } +} + +#[cfg(test)] +mod platform_constants_tests { + use super::*; + + /// Test that platform constants are properly defined + #[rstest] + fn test_platform_constants_defined() { + // Test that all constants are defined and have reasonable values + let constants = [ + ("O_NONBLOCK", platform_constants::O_NONBLOCK), + ("O_NOCTTY", platform_constants::O_NOCTTY), + ("O_DSYNC", platform_constants::O_DSYNC), + ("O_SYNC", platform_constants::O_SYNC), + ]; + + for (name, value) in constants { + assert!(value >= 0, "{} should be non-negative", name); + + cfg_if::cfg_if! { + if #[cfg(unix)] { + // On Unix, some constants should have specific values + match name { + "O_NONBLOCK" => assert!(value > 0, "O_NONBLOCK should be positive on Unix"), + "O_NOCTTY" => assert!(value >= 0, "O_NOCTTY should be non-negative on Unix"), + _ => {} + } + } else if #[cfg(windows)] { + // On Windows, some constants might be 0 (unsupported) + println!("Windows constant {} = 0x{:x}", name, value); + } + } + } + + println!("✅ All platform constants are properly defined"); + } + + /// Test constants don't conflict with each other + #[rstest] + fn test_constants_uniqueness() { + let constants = vec![ + platform_constants::O_NONBLOCK, + platform_constants::O_NOCTTY, + platform_constants::O_DSYNC, + platform_constants::O_SYNC, + ]; + + // Remove zeros (unsupported flags on some platforms) + let non_zero_constants: Vec<_> = constants.into_iter().filter(|&x| x != 0).collect(); + + // Check for duplicates among non-zero constants + for (i, &const1) in non_zero_constants.iter().enumerate() { + for &const2 in non_zero_constants.iter().skip(i + 1) { + assert_ne!(const1, const2, "Constants should not have duplicate values"); + } + } + + println!("✅ Platform constants are unique (excluding zeros)"); + } +} + +#[cfg(test)] +mod trait_extension_tests { + use super::*; + + /// Test CrossPlatformMetadataExt trait methods + #[rstest] + fn test_trait_extension_methods() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let file_path = temp_dir.path().join("trait_test.txt"); + + let content = "Testing trait extension methods"; + fs::write(&file_path, content).expect("Failed to write file"); + + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + + // Test all trait methods + let dev = metadata.cross_dev(); + let ino = metadata.cross_ino(); + let mode = metadata.cross_mode(); + let nlink = metadata.cross_nlink(); + let uid = metadata.cross_uid(); + let gid = metadata.cross_gid(); + let rdev = metadata.cross_rdev(); + let size = metadata.cross_size(); + let atime = metadata.cross_atime(); + let mtime = metadata.cross_mtime(); + let ctime = metadata.cross_ctime(); + + // Verify all methods return reasonable values + assert!(mode > 0, "cross_mode should return positive value"); + assert_eq!(size as usize, content.len(), "cross_size should match content length"); + assert!(nlink >= 1, "cross_nlink should be at least 1"); + assert!(mtime > 0, "cross_mtime should be positive"); + assert!(ctime >= 0, "cross_ctime should be non-negative"); + + // Test that trait methods match direct extractor calls + assert_eq!(dev, MetadataExtractor::dev(&metadata), "cross_dev should match extractor"); + assert_eq!(ino, MetadataExtractor::ino(&metadata), "cross_ino should match extractor"); + assert_eq!(mode, MetadataExtractor::mode(&metadata), "cross_mode should match extractor"); + assert_eq!(nlink, MetadataExtractor::nlink(&metadata), "cross_nlink should match extractor"); + assert_eq!(uid, MetadataExtractor::uid(&metadata), "cross_uid should match extractor"); + assert_eq!(gid, MetadataExtractor::gid(&metadata), "cross_gid should match extractor"); + assert_eq!(rdev, MetadataExtractor::rdev(&metadata), "cross_rdev should match extractor"); + assert_eq!(size, MetadataExtractor::size(&metadata), "cross_size should match extractor"); + assert_eq!(atime, MetadataExtractor::atime(&metadata), "cross_atime should match extractor"); + assert_eq!(mtime, MetadataExtractor::mtime(&metadata), "cross_mtime should match extractor"); + assert_eq!(ctime, MetadataExtractor::ctime(&metadata), "cross_ctime should match extractor"); + + println!("✅ All trait extension methods work correctly and match extractors"); + } +} From f95a150af1bfcbc12f822bc0bd878c7882aba945 Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Tue, 19 Aug 2025 14:19:51 -0700 Subject: [PATCH 3/9] Clippy fixes --- .cargo/config.toml | 2 + .github/workflows/ci.yaml | 24 +-- Cargo.lock | 211 +++++++++++++++++++++++ Cargo.toml | 1 + moonc_wasm/build.rs | 8 +- moonc_wasm/src/cross_platform.rs | 103 ++++++----- moonc_wasm/src/lib.rs | 2 +- moonc_wasm/src/wasmoo_extern.rs | 62 ++++--- moonc_wasm/tests/cross_platform_tests.rs | 136 ++++++++------- moonc_wasm/tests/integration_tests.rs | 161 +++++++++-------- moonc_wasm/tests/simple_test.rs | 10 +- moonc_wasm/tests/unit_tests.rs | 182 ++++++++++++------- 12 files changed, 611 insertions(+), 291 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..1c92de1 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +jobs = 8 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9721b2b..507438c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,4 @@ name: CI - on: push: tags: @@ -7,14 +6,11 @@ on: branches: - main pull_request: - permissions: checks: write pull-requests: write - env: CARGO_TERM_COLOR: always - jobs: build: strategy: @@ -31,10 +27,9 @@ jobs: shared-key: debug cache-all-crates: true - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true + components: rustfmt, clippy - name: Install moonbit (Unix) if: runner.os != 'Windows' run: | @@ -44,8 +39,8 @@ jobs: if: runner.os == 'Windows' shell: powershell run: | - Invoke-WebRequest -Uri "https://cli.moonbitlang.com/binaries/moonbit-win32_x64-0.6.19.zip" -OutFile "moonbit.zip" - Expand-Archive -Path "moonbit.zip" -DestinationPath "$env:USERPROFILE\.moon" + $env:MOONBIT_INSTALL_VERSION = "0.6.19" + irm https://cli.moonbitlang.com/install/powershell.ps1 | iex echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm @@ -56,7 +51,6 @@ jobs: run: cargo clippy -- -Dwarnings - name: Build run: cargo build --all-features --all-targets - test: needs: [build] strategy: @@ -73,10 +67,9 @@ jobs: shared-key: debug cache-all-crates: false - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true + components: rustfmt, clippy - uses: cargo-bins/cargo-binstall@main - name: Install wasmtime-cli run: cargo binstall --force --locked wasmtime-cli@33.0.0 @@ -89,8 +82,8 @@ jobs: if: runner.os == 'Windows' shell: powershell run: | - Invoke-WebRequest -Uri "https://cli.moonbitlang.com/binaries/moonbit-win32_x64-0.6.19.zip" -OutFile "moonbit.zip" - Expand-Archive -Path "moonbit.zip" -DestinationPath "$env:USERPROFILE\.moon" + $env:MOONBIT_INSTALL_VERSION = "0.6.19" + irm https://cli.moonbitlang.com/install/powershell.ps1 | iex echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm @@ -104,7 +97,6 @@ jobs: report_paths: "**/target/report-*.xml" detailed_summary: true include_passed: true - publish: needs: [test] if: "startsWith(github.ref, 'refs/tags/v')" diff --git a/Cargo.lock b/Cargo.lock index 87f3b00..c73b3e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,6 +478,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -809,6 +815,20 @@ dependencies = [ "wit-parser", ] +[[package]] +name = "moonc_wasm" +version = "0.1.0" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "rstest", + "tempfile", + "v8", + "windows", + "windows-sys 0.60.2", +] + [[package]] name = "nom" version = "7.1.3" @@ -904,6 +924,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1010,6 +1039,42 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.25" @@ -1022,6 +1087,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1247,6 +1321,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "topologic" version = "1.1.0" @@ -1485,6 +1576,108 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1544,6 +1737,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1640,6 +1842,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/Cargo.toml b/Cargo.toml index bb80ea2..88be567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/golemcloud/moonbit-component-generator" description = "Library wrapping the MoonBit compiler and other WASM Component Model libraries for generating and compiling WASM components" [workspace] +members = ["moonc_wasm"] [lib] harness = false diff --git a/moonc_wasm/build.rs b/moonc_wasm/build.rs index 27fb705..14e0a1c 100644 --- a/moonc_wasm/build.rs +++ b/moonc_wasm/build.rs @@ -2,9 +2,9 @@ fn main() { #[cfg(target_os = "windows")] { // Link Windows system libraries required by V8 - println!("cargo:rustc-link-lib=advapi32"); // For registry functions - println!("cargo:rustc-link-lib=tdh"); // For ETW (Event Tracing for Windows) - println!("cargo:rustc-link-lib=user32"); // For additional Windows APIs - println!("cargo:rustc-link-lib=kernel32"); // For kernel functions + println!("cargo:rustc-link-lib=advapi32"); // For registry functions + println!("cargo:rustc-link-lib=tdh"); // For ETW (Event Tracing for Windows) + println!("cargo:rustc-link-lib=user32"); // For additional Windows APIs + println!("cargo:rustc-link-lib=kernel32"); // For kernel functions } } diff --git a/moonc_wasm/src/cross_platform.rs b/moonc_wasm/src/cross_platform.rs index de5c585..3cb07d2 100644 --- a/moonc_wasm/src/cross_platform.rs +++ b/moonc_wasm/src/cross_platform.rs @@ -3,8 +3,7 @@ #![allow(dead_code)] -use std::fs::{self, File, Metadata, Permissions, FileType}; -use std::path::Path; +use std::fs::{self, File, FileType, Metadata, Permissions}; use std::io; use std::io::IsTerminal; @@ -14,16 +13,10 @@ cfg_if! { if #[cfg(windows)] { use std::os::windows::fs::MetadataExt; use std::os::windows::io::{AsRawHandle, RawHandle}; - use windows::Win32::Foundation::{HANDLE, FILETIME, INVALID_HANDLE_VALUE}; - use windows::Win32::Storage::FileSystem::{ - CreateFileW, - FILE_ATTRIBUTE_NORMAL, - FILE_FLAG_OVERLAPPED, - FILE_GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING, - SetFileTime, GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, - }; + use windows::Win32::Foundation::{HANDLE, FILETIME}; + use windows::Win32::System::Console::{GetConsoleMode, CONSOLE_MODE}; - use windows::core::{PCWSTR, Error as WindowsError}; + pub type RawFd = RawHandle; } else { use std::os::unix::fs::{MetadataExt, PermissionsExt, OpenOptionsExt, FileTypeExt}; @@ -216,9 +209,10 @@ impl MetadataExtractor { // Windows FILETIME: 100-nanosecond intervals since January 1, 1601 UTC // Unix timestamp: seconds since January 1, 1970 UTC const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; // seconds between epochs - const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second + const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second - let unix_seconds = (filetime / FILETIME_UNITS_PER_SECOND).saturating_sub(WINDOWS_TO_UNIX_OFFSET); + let unix_seconds = + (filetime / FILETIME_UNITS_PER_SECOND).saturating_sub(WINDOWS_TO_UNIX_OFFSET); unix_seconds as i64 } } @@ -255,7 +249,7 @@ impl PermissionsBuilder { .create(true) .write(true) .truncate(true) - .open(&temp_path) + .open(&temp_path) { let mut perms = fs::metadata(&temp_path).unwrap().permissions(); perms.set_readonly(read_only); @@ -306,7 +300,7 @@ pub fn host_isatty(fd: RawFd) -> i32 { if fd == std::io::stderr().as_raw_handle() { // stderr return if std::io::stderr().is_terminal() { 1 } else { 0 }; } - + // For other handles, use Win32 API unsafe { let handle = HANDLE(fd); @@ -341,17 +335,39 @@ pub trait CrossPlatformMetadataExt { } impl CrossPlatformMetadataExt for Metadata { - fn cross_dev(&self) -> u64 { MetadataExtractor::dev(self) } - fn cross_ino(&self) -> u64 { MetadataExtractor::ino(self) } - fn cross_mode(&self) -> u32 { MetadataExtractor::mode(self) } - fn cross_nlink(&self) -> u64 { MetadataExtractor::nlink(self) } - fn cross_uid(&self) -> u32 { MetadataExtractor::uid(self) } - fn cross_gid(&self) -> u32 { MetadataExtractor::gid(self) } - fn cross_rdev(&self) -> u64 { MetadataExtractor::rdev(self) } - fn cross_size(&self) -> u64 { MetadataExtractor::size(self) } - fn cross_atime(&self) -> i64 { MetadataExtractor::atime(self) } - fn cross_mtime(&self) -> i64 { MetadataExtractor::mtime(self) } - fn cross_ctime(&self) -> i64 { MetadataExtractor::ctime(self) } + fn cross_dev(&self) -> u64 { + MetadataExtractor::dev(self) + } + fn cross_ino(&self) -> u64 { + MetadataExtractor::ino(self) + } + fn cross_mode(&self) -> u32 { + MetadataExtractor::mode(self) + } + fn cross_nlink(&self) -> u64 { + MetadataExtractor::nlink(self) + } + fn cross_uid(&self) -> u32 { + MetadataExtractor::uid(self) + } + fn cross_gid(&self) -> u32 { + MetadataExtractor::gid(self) + } + fn cross_rdev(&self) -> u64 { + MetadataExtractor::rdev(self) + } + fn cross_size(&self) -> u64 { + MetadataExtractor::size(self) + } + fn cross_atime(&self) -> i64 { + MetadataExtractor::atime(self) + } + fn cross_mtime(&self) -> i64 { + MetadataExtractor::mtime(self) + } + fn cross_ctime(&self) -> i64 { + MetadataExtractor::ctime(self) + } } /// Cross-platform FileType extensions @@ -414,18 +430,18 @@ pub fn cross_utimes(path: &str, atime: f64, mtime: f64) -> io::Result<()> { use libc::{timeval, suseconds_t, utimes as libc_utimes}; let c_path = std::ffi::CString::new(path) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid path"))?; - + let atime_tv = timeval { tv_sec: atime as i64, tv_usec: (atime.fract() * 1_000_000.0) as suseconds_t, }; let mtime_tv = timeval { - tv_sec: mtime as i64, + tv_sec: mtime as i64, tv_usec: (mtime.fract() * 1_000_000.0) as suseconds_t, }; - + let times = [atime_tv, mtime_tv]; - + unsafe { if libc_utimes(c_path.as_ptr(), times.as_ptr()) == 0 { Ok(()) @@ -437,14 +453,14 @@ pub fn cross_utimes(path: &str, atime: f64, mtime: f64) -> io::Result<()> { // FIXED Windows implementation use windows::core::HSTRING; use windows::Win32::Storage::FileSystem::{ - CreateFileW, FILE_GENERIC_WRITE, FILE_SHARE_READ, + CreateFileW, FILE_GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, SetFileTime }; - use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, FILETIME}; + use windows::Win32::Foundation::{INVALID_HANDLE_VALUE, FILETIME}; use windows::core::PCWSTR; - + let wide_path = HSTRING::from(path); - + unsafe { // FIX ERROR 3: Correct CreateFileW parameter types let handle = CreateFileW( @@ -456,27 +472,27 @@ pub fn cross_utimes(path: &str, atime: f64, mtime: f64) -> io::Result<()> { FILE_ATTRIBUTE_NORMAL, // Remove .0 ✓ None, // Option ✓ )?; - + if handle == INVALID_HANDLE_VALUE { return Err(io::Error::last_os_error()); } - + // Convert Unix timestamps to Windows FILETIME let unix_to_filetime = |timestamp: f64| -> FILETIME { const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; - + let windows_time = ((timestamp as u64) + WINDOWS_TO_UNIX_OFFSET) * FILETIME_UNITS_PER_SECOND; - + FILETIME { dwLowDateTime: (windows_time & 0xFFFFFFFF) as u32, dwHighDateTime: (windows_time >> 32) as u32, } }; - + let atime_ft = unix_to_filetime(atime); let mtime_ft = unix_to_filetime(mtime); - + // FIX ERROR 4: SetFileTime returns Result, not Option SetFileTime( handle, @@ -491,15 +507,14 @@ pub fn cross_utimes(path: &str, atime: f64, mtime: f64) -> io::Result<()> { } } - /// Convert Unix timestamp to Windows FILETIME #[cfg(windows)] fn unix_to_filetime(timestamp: f64) -> FILETIME { const WINDOWS_TO_UNIX_OFFSET: u64 = 11_644_473_600; // seconds between epochs - const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second - + const FILETIME_UNITS_PER_SECOND: u64 = 10_000_000; // 100ns units per second + let windows_time = ((timestamp as u64) + WINDOWS_TO_UNIX_OFFSET) * FILETIME_UNITS_PER_SECOND; - + FILETIME { dwLowDateTime: (windows_time & 0xFFFFFFFF) as u32, dwHighDateTime: (windows_time >> 32) as u32, diff --git a/moonc_wasm/src/lib.rs b/moonc_wasm/src/lib.rs index 5b1f19b..001a4e0 100644 --- a/moonc_wasm/src/lib.rs +++ b/moonc_wasm/src/lib.rs @@ -1,5 +1,5 @@ -mod wasmoo_extern; pub mod cross_platform; +mod wasmoo_extern; pub fn run_wasmoo(argv: Vec) -> anyhow::Result<()> { let isolate = &mut v8::Isolate::new(Default::default()); diff --git a/moonc_wasm/src/wasmoo_extern.rs b/moonc_wasm/src/wasmoo_extern.rs index e8bf18d..a76cdf6 100644 --- a/moonc_wasm/src/wasmoo_extern.rs +++ b/moonc_wasm/src/wasmoo_extern.rs @@ -1,15 +1,14 @@ use std::{ collections::HashMap, - ffi::CString, - fs::{self, File, OpenOptions, Permissions, metadata}, + fs::{self, File, OpenOptions, metadata}, io::{IsTerminal, Read, Write}, path::Path, process::{Command, Stdio}, }; use crate::cross_platform::{ - MetadataExtractor, CrossPlatformMetadataExt, CrossPlatformFileTypeExt, PermissionsBuilder, RawFdExt, - platform_constants, host_isatty, cross_utimes + CrossPlatformFileTypeExt, CrossPlatformMetadataExt, MetadataExtractor, PermissionsBuilder, + RawFdExt, cross_utimes, host_isatty, platform_constants, }; cfg_if::cfg_if! { @@ -326,13 +325,13 @@ fn open( use std::os::windows::fs::OpenOptionsExt; use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; use windows::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; - + opts.attributes(FILE_ATTRIBUTE_NORMAL.0); // If you want nonblocking async I/O: opts.custom_flags(FILE_FLAG_OVERLAPPED.0); } } - + match opts.open(path) { Err(err) => { let message = v8::String::new(scope, &err.to_string()).unwrap(); @@ -398,31 +397,31 @@ fn access( let path = Path::new(&path); let mode = args.get(1); let mode = mode.to_number(scope).unwrap().value() as i32; - if mode & F_OK != 0 { - if let Err(err) = metadata(path) { - let message = v8::String::new(scope, &err.to_string()).unwrap(); - let exn = v8::Exception::error(scope, message); - scope.throw_exception(exn); - return; - } + if mode & F_OK != 0 + && let Err(err) = metadata(path) + { + let message = v8::String::new(scope, &err.to_string()).unwrap(); + let exn = v8::Exception::error(scope, message); + scope.throw_exception(exn); + return; } - if mode & R_OK != 0 { - if let Err(err) = File::open(path) { - let message = v8::String::new(scope, &err.to_string()).unwrap(); - let exn = v8::Exception::error(scope, message); - scope.throw_exception(exn); - return; - } + if mode & R_OK != 0 + && let Err(err) = File::open(path) + { + let message = v8::String::new(scope, &err.to_string()).unwrap(); + let exn = v8::Exception::error(scope, message); + scope.throw_exception(exn); + return; } - if mode & W_OK != 0 { - if let Err(err) = OpenOptions::new().write(true).open(path) { - let message = v8::String::new(scope, &err.to_string()).unwrap(); - let exn = v8::Exception::error(scope, message); - scope.throw_exception(exn); - return; - } + if mode & W_OK != 0 + && let Err(err) = OpenOptions::new().write(true).open(path) + { + let message = v8::String::new(scope, &err.to_string()).unwrap(); + let exn = v8::Exception::error(scope, message); + scope.throw_exception(exn); + return; } if mode & X_OK != 0 { @@ -638,17 +637,17 @@ fn timeval_from_f64(t: f64) -> std::io::Result { "Time value must be finite", )); } - + let total_usec = (t * 1_000_000.0).round() as i64; let sec = total_usec.div_euclid(1_000_000); let usec = total_usec.rem_euclid(1_000_000); - + // Build timeval with platform-specific field types cfg_if::cfg_if! { if #[cfg(unix)] { Ok(libc::timeval { tv_sec: sec as libc::time_t, // Fixed: use 'sec' not 'sec_i64' - tv_usec: usec as libc::suseconds_t, // Fixed: use 'usec' not 'usec_i64' + tv_usec: usec as libc::suseconds_t, // Fixed: use 'usec' not 'usec_i64' }) } else if #[cfg(windows)] { Ok(libc::timeval { @@ -666,13 +665,10 @@ fn timeval_from_f64(t: f64) -> std::io::Result { } } - fn __utimes(path: String, atime: f64, mtime: f64) -> std::io::Result<()> { cross_utimes(&path, atime, mtime) } - - // utimes: JSString as Path, F64 as AccessTime, F64 as ModifyTime -> undefined fn utimes( scope: &mut v8::HandleScope, diff --git a/moonc_wasm/tests/cross_platform_tests.rs b/moonc_wasm/tests/cross_platform_tests.rs index 3e5b4b2..8c26011 100644 --- a/moonc_wasm/tests/cross_platform_tests.rs +++ b/moonc_wasm/tests/cross_platform_tests.rs @@ -1,16 +1,16 @@ //! Integration tests for cross-platform file system operations -//! +//! //! This module contains comprehensive parameterized tests using rstest //! to verify cross-platform compatibility of metadata operations. +use rstest::rstest; use std::fs::{self, File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use tempfile::TempDir; -use rstest::rstest; use moonc_wasm::cross_platform::{ - CrossPlatformMetadataExt, PermissionsBuilder, MetadataExtractor, platform_constants + CrossPlatformMetadataExt, MetadataExtractor, PermissionsBuilder, platform_constants, }; /// Test fixture that creates a temporary directory for each test @@ -27,13 +27,9 @@ fn temp_dir() -> TempDir { #[case::full_permissions(0o777, "full permissions")] #[case::no_write(0o555, "no write permissions")] #[case::owner_only(0o600, "owner only permissions")] -fn test_metadata_permissions( - temp_dir: TempDir, - #[case] mode: u32, - #[case] description: &str, -) { +fn test_metadata_permissions(temp_dir: TempDir, #[case] mode: u32, #[case] description: &str) { let file_path = temp_dir.path().join("test_file.txt"); - + // Create test file with content let mut file = File::create(&file_path).expect("Failed to create test file"); writeln!(file, "Test content for {}", description).expect("Failed to write to file"); @@ -45,18 +41,18 @@ fn test_metadata_permissions( // Test metadata extraction let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Verify cross-platform methods work let extracted_mode = metadata.cross_mode(); let size = metadata.cross_size(); let uid = metadata.cross_uid(); let gid = metadata.cross_gid(); let nlink = metadata.cross_nlink(); - + // Basic assertions assert!(size > 0, "File size should be greater than 0"); assert!(nlink >= 1, "Number of links should be at least 1"); - + // Platform-specific assertions cfg_if::cfg_if! { if #[cfg(unix)] { @@ -67,9 +63,11 @@ fn test_metadata_permissions( assert!(extracted_mode > 0, "Windows file attributes should be present"); } } - - println!("✅ {} - Mode: 0o{:o}, Size: {}, UID: {}, GID: {}, Links: {}", - description, extracted_mode, size, uid, gid, nlink); + + println!( + "✅ {} - Mode: 0o{:o}, Size: {}, UID: {}, GID: {}, Links: {}", + description, extracted_mode, size, uid, gid, nlink + ); } /// Test metadata extraction for different file types and edge cases @@ -84,13 +82,13 @@ fn test_metadata_file_sizes( #[case] description: &str, ) { let file_path = temp_dir.path().join("size_test.txt"); - + // Create file with specific content size let content = "x".repeat(content_size); fs::write(&file_path, &content).expect("Failed to write file"); - + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Test all cross-platform metadata methods let size = metadata.cross_size(); let dev = metadata.cross_dev(); @@ -98,19 +96,27 @@ fn test_metadata_file_sizes( let atime = metadata.cross_atime(); let mtime = metadata.cross_mtime(); let ctime = metadata.cross_ctime(); - + // Verify size matches expected - assert_eq!(size as usize, content_size, "File size should match written content"); - + assert_eq!( + size as usize, content_size, + "File size should match written content" + ); + // Verify other metadata fields are reasonable assert!(dev >= 0, "Device ID should be non-negative"); assert!(ino >= 0, "Inode should be non-negative"); - assert!(atime > 0 || atime == 0, "Access time should be valid timestamp or 0"); + assert!( + atime > 0 || atime == 0, + "Access time should be valid timestamp or 0" + ); assert!(mtime > 0, "Modification time should be positive timestamp"); assert!(ctime >= 0, "Creation/change time should be non-negative"); - - println!("✅ {} - Size: {}, Dev: {}, Ino: {}, ATime: {}, MTime: {}, CTime: {}", - description, size, dev, ino, atime, mtime, ctime); + + println!( + "✅ {} - Size: {}, Dev: {}, Ino: {}, ATime: {}, MTime: {}, CTime: {}", + description, size, dev, ino, atime, mtime, ctime + ); } /// Test platform constants compatibility @@ -130,7 +136,7 @@ fn test_platform_constants(#[case] constant: i32, #[case] name: &str) { assert!(constant >= 0, "{} should be non-negative on Windows", name); } } - + println!("✅ {} = 0x{:x} ({})", name, constant, constant); } @@ -152,29 +158,29 @@ fn test_platform_constants(#[case] constant: i32, #[case] name: &str) { #[case::public_read(0o644)] fn test_permissions_builder(temp_dir: TempDir, #[case] mode: u32) { let file_path = temp_dir.path().join("perm_test.txt"); - + // Create test file File::create(&file_path).expect("Failed to create test file"); - + // Test permissions builder let permissions = PermissionsBuilder::from_mode(mode); - + // Apply permissions let result = fs::set_permissions(&file_path, permissions); - + cfg_if::cfg_if! { if #[cfg(unix)] { // On Unix, setting permissions should generally succeed assert!(result.is_ok(), "Setting permissions 0o{:o} should succeed on Unix", mode); - + // Verify the permissions were set correctly let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); let actual_mode = metadata.cross_mode() & 0o777; - + // Account for umask and filesystem limitations if mode & 0o200 == 0 { // If we tried to remove write permission, verify it's readonly - assert!(metadata.permissions().readonly() || (actual_mode & 0o200) == 0, + assert!(metadata.permissions().readonly() || (actual_mode & 0o200) == 0, "File should be readonly when write bit is cleared"); } } else if #[cfg(windows)] { @@ -183,7 +189,7 @@ fn test_permissions_builder(temp_dir: TempDir, #[case] mode: u32) { println!("Windows permissions builder test for mode 0o{:o}: {:?}", mode, result); } } - + println!("✅ Permissions 0o{:o} - Builder succeeded", mode); } @@ -193,10 +199,10 @@ fn test_permissions_builder(temp_dir: TempDir, #[case] mode: u32) { #[case::empty_filename("")] fn test_metadata_edge_cases(temp_dir: TempDir, #[case] filename: &str) { let file_path = temp_dir.path().join(filename); - + // Attempt to get metadata for nonexistent file let result = fs::metadata(&file_path); - + match filename { "" => { // Empty filename should fail @@ -208,7 +214,7 @@ fn test_metadata_edge_cases(temp_dir: TempDir, #[case] filename: &str) { } _ => {} } - + println!("✅ Edge case '{}' handled correctly", filename); } @@ -225,32 +231,39 @@ fn test_file_operations_cross_platform( #[case] append: bool, ) { let file_path = temp_dir.path().join("ops_test.txt"); - + // Create initial file fs::write(&file_path, "initial content").expect("Failed to create initial file"); - + // Test OpenOptions with different combinations let mut opts = OpenOptions::new(); opts.read(read).write(write).append(append); - + let result = opts.open(&file_path); - + if read || write { - assert!(result.is_ok(), "File should open with read={}, write={}, append={}", - read, write, append); - + assert!( + result.is_ok(), + "File should open with read={}, write={}, append={}", + read, + write, + append + ); + if let Ok(file) = result { let metadata = file.metadata().expect("Failed to get file metadata"); - + // Test metadata extraction on open file let size = metadata.cross_size(); let mode = metadata.cross_mode(); - + assert!(size > 0, "File should have content"); assert!(mode > 0, "File should have valid mode"); - - println!("✅ File operations (r={}, w={}, a={}) - Size: {}, Mode: 0o{:o}", - read, write, append, size, mode); + + println!( + "✅ File operations (r={}, w={}, a={}) - Size: {}, Mode: 0o{:o}", + read, write, append, size, mode + ); } } } @@ -259,17 +272,17 @@ fn test_file_operations_cross_platform( #[rstest] fn test_metadata_performance(temp_dir: TempDir) { let file_path = temp_dir.path().join("perf_test.txt"); - + // Create test file let content = "x".repeat(1024); fs::write(&file_path, &content).expect("Failed to create test file"); - + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Perform multiple metadata extractions to test performance let iterations = 1000; let start = std::time::Instant::now(); - + for _ in 0..iterations { let _mode = metadata.cross_mode(); let _size = metadata.cross_size(); @@ -283,13 +296,18 @@ fn test_metadata_performance(temp_dir: TempDir) { let _mtime = metadata.cross_mtime(); let _ctime = metadata.cross_ctime(); } - + let duration = start.elapsed(); let avg_time = duration.as_nanos() / iterations as u128; - + // Performance should be reasonable (less than 1ms per operation set) - assert!(avg_time < 1_000_000, "Average metadata extraction should be under 1ms"); - - println!("✅ Performance test - {} iterations in {:?} (avg: {}ns per iteration)", - iterations, duration, avg_time); + assert!( + avg_time < 1_000_000, + "Average metadata extraction should be under 1ms" + ); + + println!( + "✅ Performance test - {} iterations in {:?} (avg: {}ns per iteration)", + iterations, duration, avg_time + ); } diff --git a/moonc_wasm/tests/integration_tests.rs b/moonc_wasm/tests/integration_tests.rs index 342fe18..8056303 100644 --- a/moonc_wasm/tests/integration_tests.rs +++ b/moonc_wasm/tests/integration_tests.rs @@ -1,12 +1,14 @@ //! Integration tests for wasmoo_extern functionality -//! +//! //! Tests the complete integration of cross-platform operations with the v8 JavaScript engine +use rstest::rstest; use std::fs::{self, File}; use tempfile::TempDir; -use rstest::rstest; -use moonc_wasm::cross_platform::{CrossPlatformMetadataExt, PermissionsBuilder, RawFdExt, host_isatty}; +use moonc_wasm::cross_platform::{ + CrossPlatformMetadataExt, PermissionsBuilder, RawFdExt, host_isatty, +}; #[cfg(test)] mod file_operations_integration { @@ -17,33 +19,33 @@ mod file_operations_integration { fn test_file_descriptor_operations() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("fd_test.txt"); - + // Create and open file let content = "File descriptor test content"; fs::write(&file_path, content).expect("Failed to write file"); - + let file = File::open(&file_path).expect("Failed to open file"); - + // Test raw file descriptor extraction let raw_fd = file.as_raw_fd(); - + cfg_if::cfg_if! { if #[cfg(unix)] { assert!(raw_fd >= 0, "Unix file descriptor should be non-negative"); - + // Test host_isatty function let tty_result = host_isatty(raw_fd); assert_eq!(tty_result, 0, "Regular file should not be a TTY"); } else if #[cfg(windows)] { // On Windows, raw_fd is a HANDLE println!("Windows file handle: {:?}", raw_fd); - + // Test host_isatty function (should return 0 for regular files) let tty_result = host_isatty(raw_fd); assert_eq!(tty_result, 0, "Regular file should not be a TTY on Windows"); } } - + println!("✅ File descriptor operations work across platforms"); } @@ -55,11 +57,14 @@ mod file_operations_integration { fn test_standard_io_descriptors(#[case] fd: i32, #[case] name: &str) { // Test host_isatty on standard file descriptors let tty_result = host_isatty(fd as _); - + // Standard streams might or might not be TTYs depending on environment - assert!(tty_result == 0 || tty_result == 1, - "{} host_isatty should return 0 or 1", name); - + assert!( + tty_result == 0 || tty_result == 1, + "{} host_isatty should return 0 or 1", + name + ); + println!("✅ {} (fd={}) host_isatty result: {}", name, fd, tty_result); } } @@ -72,25 +77,22 @@ mod metadata_integration_tests { #[rstest] #[case::regular_file("regular.txt", "Regular file content")] #[case::executable_script("script.sh", "#!/bin/bash\necho 'Hello World'")] - fn test_complete_metadata_workflow( - #[case] filename: &str, - #[case] content: &str, - ) { + fn test_complete_metadata_workflow(#[case] filename: &str, #[case] content: &str) { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join(filename); - + // Create file with specific content fs::write(&file_path, content).expect("Failed to write file"); - + // Set executable permissions for script files if filename.ends_with(".sh") { let permissions = PermissionsBuilder::from_mode(0o755); fs::set_permissions(&file_path, permissions).expect("Failed to set permissions"); } - + // Get metadata and extract all fields (as done in wasmoo_extern stat functions) let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Extract all metadata fields using cross-platform methods let kind = if metadata.is_file() { 0 } else { 1 }; // File type classification let dev = metadata.cross_dev(); @@ -104,20 +106,23 @@ mod metadata_integration_tests { let atime = metadata.cross_atime(); let mtime = metadata.cross_mtime(); let ctime = metadata.cross_ctime(); - + // Verify all fields have reasonable values assert_eq!(kind, 0, "Should be classified as regular file"); assert_eq!(size as usize, content.len(), "Size should match content"); assert!(nlink >= 1, "Should have at least one link"); assert!(mtime > 0, "Modification time should be positive"); - + // Simulate v8 object creation (as done in wasmoo_extern) let stat_object = format!( "{{ kind: {}, dev: {}, ino: {}, mode: {}, nlink: {}, uid: {}, gid: {}, rdev: {}, size: {}, atime: {}, mtime: {}, ctime: {} }}", kind, dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime ); - - println!("✅ Complete metadata workflow for {} - {}", filename, stat_object); + + println!( + "✅ Complete metadata workflow for {} - {}", + filename, stat_object + ); } /// Test metadata operations on different file types @@ -125,23 +130,23 @@ mod metadata_integration_tests { fn test_directory_metadata() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let dir_path = temp_dir.path().join("test_subdir"); - + // Create subdirectory fs::create_dir(&dir_path).expect("Failed to create directory"); - + // Get directory metadata let metadata = fs::metadata(&dir_path).expect("Failed to get directory metadata"); - + // Test directory-specific metadata assert!(metadata.is_dir(), "Should be identified as directory"); - + let kind = if metadata.is_dir() { 1 } else { 0 }; let mode = metadata.cross_mode(); let nlink = metadata.cross_nlink(); - + assert_eq!(kind, 1, "Directory should have kind = 1"); assert!(mode > 0, "Directory should have valid mode"); - + cfg_if::cfg_if! { if #[cfg(unix)] { // On Unix, directories typically have nlink >= 2 (. and ..) @@ -151,8 +156,11 @@ mod metadata_integration_tests { assert!(nlink >= 1, "Windows directory should have nlink >= 1"); } } - - println!("✅ Directory metadata - Kind: {}, Mode: 0o{:o}, Links: {}", kind, mode, nlink); + + println!( + "✅ Directory metadata - Kind: {}, Mode: 0o{:o}, Links: {}", + kind, mode, nlink + ); } } @@ -165,15 +173,19 @@ mod error_handling_integration { fn test_nonexistent_file_handling() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let nonexistent_path = temp_dir.path().join("does_not_exist.txt"); - + // Attempt to get metadata for nonexistent file let result = fs::metadata(&nonexistent_path); assert!(result.is_err(), "Should fail for nonexistent file"); - + // Verify error type let error = result.unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "Should be NotFound error"); - + assert_eq!( + error.kind(), + std::io::ErrorKind::NotFound, + "Should be NotFound error" + ); + println!("✅ Nonexistent file error handling works correctly"); } @@ -182,19 +194,19 @@ mod error_handling_integration { fn test_permission_scenarios() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("restricted.txt"); - + // Create file fs::write(&file_path, "restricted content").expect("Failed to create file"); - + // Try to set very restrictive permissions let permissions = PermissionsBuilder::from_mode(0o000); let perm_result = fs::set_permissions(&file_path, permissions); - + cfg_if::cfg_if! { if #[cfg(unix)] { // On Unix, this should succeed assert!(perm_result.is_ok(), "Setting restrictive permissions should succeed on Unix"); - + // Verify the file is now readonly let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); assert!(metadata.permissions().readonly(), "File should be readonly"); @@ -203,7 +215,7 @@ mod error_handling_integration { println!("Windows restrictive permissions result: {:?}", perm_result); } } - + println!("✅ Permission scenarios handled appropriately"); } } @@ -216,24 +228,24 @@ mod performance_integration { #[rstest] fn test_metadata_performance_integration() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); - + // Create multiple test files let file_count = 100; let mut file_paths = Vec::new(); - + for i in 0..file_count { let file_path = temp_dir.path().join(format!("perf_test_{}.txt", i)); let content = format!("Performance test file {} content", i); fs::write(&file_path, &content).expect("Failed to create test file"); file_paths.push(file_path); } - + // Measure time for complete metadata extraction workflow let start = std::time::Instant::now(); - + for file_path in &file_paths { let metadata = fs::metadata(file_path).expect("Failed to get metadata"); - + // Extract all metadata fields (simulating wasmoo_extern usage) let _dev = metadata.cross_dev(); let _ino = metadata.cross_ino(); @@ -247,48 +259,55 @@ mod performance_integration { let _mtime = metadata.cross_mtime(); let _ctime = metadata.cross_ctime(); } - + let duration = start.elapsed(); let avg_time = duration.as_micros() / file_count as u128; - + // Performance should be reasonable (less than 1ms per file) - assert!(avg_time < 1000, "Average metadata extraction should be under 1ms per file"); - - println!("✅ Performance integration - {} files in {:?} (avg: {}μs per file)", - file_count, duration, avg_time); + assert!( + avg_time < 1000, + "Average metadata extraction should be under 1ms per file" + ); + + println!( + "✅ Performance integration - {} files in {:?} (avg: {}μs per file)", + file_count, duration, avg_time + ); } /// Test concurrent metadata operations #[rstest] fn test_concurrent_metadata_operations() { - use std::thread; use std::sync::Arc; - + use std::thread; + let temp_dir = Arc::new(TempDir::new().expect("Failed to create temp dir")); let file_path = temp_dir.path().join("concurrent_test.txt"); - + // Create test file fs::write(&file_path, "Concurrent access test").expect("Failed to create file"); - + // Spawn multiple threads to access metadata concurrently - let handles: Vec<_> = (0..10).map(|i| { - let file_path = file_path.clone(); - thread::spawn(move || { - for _ in 0..10 { - let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - let _size = metadata.cross_size(); - let _mode = metadata.cross_mode(); - let _mtime = metadata.cross_mtime(); - } - i + let handles: Vec<_> = (0..10) + .map(|i| { + let file_path = file_path.clone(); + thread::spawn(move || { + for _ in 0..10 { + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); + let _size = metadata.cross_size(); + let _mode = metadata.cross_mode(); + let _mtime = metadata.cross_mtime(); + } + i + }) }) - }).collect(); - + .collect(); + // Wait for all threads to complete for handle in handles { handle.join().expect("Thread should complete successfully"); } - + println!("✅ Concurrent metadata operations completed successfully"); } } diff --git a/moonc_wasm/tests/simple_test.rs b/moonc_wasm/tests/simple_test.rs index ea72dc7..79844f6 100644 --- a/moonc_wasm/tests/simple_test.rs +++ b/moonc_wasm/tests/simple_test.rs @@ -8,23 +8,23 @@ use tempfile::TempDir; fn test_basic_functionality() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("test.txt"); - + fs::write(&file_path, "test content").expect("Failed to write file"); let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + let size = metadata.cross_size(); assert_eq!(size, 12); // "test content" is 12 bytes - + println!("✅ Basic cross-platform test passed - Size: {}", size); } #[test] fn test_platform_constants() { use moonc_wasm::cross_platform::platform_constants; - + // Just verify constants are defined let _nonblock = platform_constants::O_NONBLOCK; let _noctty = platform_constants::O_NOCTTY; - + println!("✅ Platform constants are accessible"); } diff --git a/moonc_wasm/tests/unit_tests.rs b/moonc_wasm/tests/unit_tests.rs index 72f1008..d918c25 100644 --- a/moonc_wasm/tests/unit_tests.rs +++ b/moonc_wasm/tests/unit_tests.rs @@ -1,13 +1,13 @@ //! Unit tests for cross-platform module components -//! +//! //! These tests focus on individual components and internal functionality +use rstest::rstest; use std::fs::{self, File}; use tempfile::TempDir; -use rstest::rstest; use moonc_wasm::cross_platform::{ - MetadataExtractor, PermissionsBuilder, CrossPlatformMetadataExt, platform_constants + CrossPlatformMetadataExt, MetadataExtractor, PermissionsBuilder, platform_constants, }; #[cfg(test)] @@ -19,13 +19,13 @@ mod metadata_extractor_tests { fn test_metadata_extractor_basic() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("test.txt"); - + // Create test file with known content let content = "Hello, cross-platform world!"; fs::write(&file_path, content).expect("Failed to write file"); - + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Test all extractor methods let mode = MetadataExtractor::mode(&metadata); let _dev = MetadataExtractor::dev(&metadata); @@ -38,13 +38,17 @@ mod metadata_extractor_tests { let atime = MetadataExtractor::atime(&metadata); let mtime = MetadataExtractor::mtime(&metadata); let ctime = MetadataExtractor::ctime(&metadata); - + // Verify reasonable values assert!(mode > 0, "Mode should be positive"); - assert_eq!(size as usize, content.len(), "Size should match content length"); + assert_eq!( + size as usize, + content.len(), + "Size should match content length" + ); assert!(nlink >= 1, "Should have at least one link"); assert!(mtime > 0, "Modification time should be positive"); - + // Platform-specific checks cfg_if::cfg_if! { if #[cfg(unix)] { @@ -57,8 +61,11 @@ mod metadata_extractor_tests { assert_eq!(gid, 0, "GID should be 0 on Windows (simulated)"); } } - - println!("✅ MetadataExtractor - Mode: 0o{:o}, Size: {}, Links: {}", mode, size, nlink); + + println!( + "✅ MetadataExtractor - Mode: 0o{:o}, Size: {}, Links: {}", + mode, size, nlink + ); } /// Test metadata extraction with different file types @@ -66,29 +73,37 @@ mod metadata_extractor_tests { #[case::regular_file("regular.txt", "Regular file content")] #[case::empty_file("empty.txt", "")] #[case::binary_file("binary.bin", "\x00\x01\x02\x03\x7F")] - fn test_metadata_extractor_file_types( - #[case] filename: &str, - #[case] content: &str, - ) { + fn test_metadata_extractor_file_types(#[case] filename: &str, #[case] content: &str) { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join(filename); - + fs::write(&file_path, content).expect("Failed to write file"); let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Test size extraction let size = MetadataExtractor::size(&metadata); - assert_eq!(size as usize, content.len(), "Size should match for {}", filename); - + assert_eq!( + size as usize, + content.len(), + "Size should match for {}", + filename + ); + // Test timestamps let atime = MetadataExtractor::atime(&metadata); let mtime = MetadataExtractor::mtime(&metadata); let ctime = MetadataExtractor::ctime(&metadata); - - assert!(mtime > 0 || content.is_empty(), "MTime should be positive for non-empty files"); + + assert!( + mtime > 0 || content.is_empty(), + "MTime should be positive for non-empty files" + ); assert!(ctime >= 0, "CTime should be non-negative"); - - println!("✅ File type {} - Size: {}, MTime: {}", filename, size, mtime); + + println!( + "✅ File type {} - Size: {}, MTime: {}", + filename, size, mtime + ); } } @@ -107,22 +122,22 @@ mod permissions_builder_tests { fn test_permissions_builder_modes(#[case] mode: u32, #[case] description: &str) { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("perm_test.txt"); - + // Create test file File::create(&file_path).expect("Failed to create file"); - + // Test permissions builder let permissions = PermissionsBuilder::from_mode(mode); let result = fs::set_permissions(&file_path, permissions); - + cfg_if::cfg_if! { if #[cfg(unix)] { assert!(result.is_ok(), "Setting {} (0o{:o}) should succeed on Unix", description, mode); - + // Verify permissions were applied let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); let actual_mode = metadata.cross_mode() & 0o777; - + // Check readonly flag for write permissions if mode & 0o200 == 0 { assert!(metadata.permissions().readonly() || (actual_mode & 0o200) == 0, @@ -133,7 +148,7 @@ mod permissions_builder_tests { println!("Windows permissions test for {}: {:?}", description, result); } } - + println!("✅ {} (0o{:o}) - Builder completed", description, mode); } @@ -146,13 +161,13 @@ mod permissions_builder_tests { fn test_permissions_special_bits(#[case] mode: u32, #[case] description: &str) { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("special_perm_test.txt"); - + File::create(&file_path).expect("Failed to create file"); - + // Test with special permission bits let permissions = PermissionsBuilder::from_mode(mode); let result = fs::set_permissions(&file_path, permissions); - + // Special bits may not be supported on all filesystems cfg_if::cfg_if! { if #[cfg(unix)] { @@ -163,8 +178,11 @@ mod permissions_builder_tests { println!("Windows special permissions test for {} (0o{:o}): {:?}", description, mode, result); } } - - println!("✅ {} (0o{:o}) - Special bits test completed", description, mode); + + println!( + "✅ {} (0o{:o}) - Special bits test completed", + description, mode + ); } } @@ -182,10 +200,10 @@ mod platform_constants_tests { ("O_DSYNC", platform_constants::O_DSYNC), ("O_SYNC", platform_constants::O_SYNC), ]; - + for (name, value) in constants { assert!(value >= 0, "{} should be non-negative", name); - + cfg_if::cfg_if! { if #[cfg(unix)] { // On Unix, some constants should have specific values @@ -200,7 +218,7 @@ mod platform_constants_tests { } } } - + println!("✅ All platform constants are properly defined"); } @@ -213,17 +231,17 @@ mod platform_constants_tests { platform_constants::O_DSYNC, platform_constants::O_SYNC, ]; - + // Remove zeros (unsupported flags on some platforms) let non_zero_constants: Vec<_> = constants.into_iter().filter(|&x| x != 0).collect(); - + // Check for duplicates among non-zero constants for (i, &const1) in non_zero_constants.iter().enumerate() { for &const2 in non_zero_constants.iter().skip(i + 1) { assert_ne!(const1, const2, "Constants should not have duplicate values"); } } - + println!("✅ Platform constants are unique (excluding zeros)"); } } @@ -237,12 +255,12 @@ mod trait_extension_tests { fn test_trait_extension_methods() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let file_path = temp_dir.path().join("trait_test.txt"); - + let content = "Testing trait extension methods"; fs::write(&file_path, content).expect("Failed to write file"); - + let metadata = fs::metadata(&file_path).expect("Failed to get metadata"); - + // Test all trait methods let dev = metadata.cross_dev(); let ino = metadata.cross_ino(); @@ -255,27 +273,75 @@ mod trait_extension_tests { let atime = metadata.cross_atime(); let mtime = metadata.cross_mtime(); let ctime = metadata.cross_ctime(); - + // Verify all methods return reasonable values assert!(mode > 0, "cross_mode should return positive value"); - assert_eq!(size as usize, content.len(), "cross_size should match content length"); + assert_eq!( + size as usize, + content.len(), + "cross_size should match content length" + ); assert!(nlink >= 1, "cross_nlink should be at least 1"); assert!(mtime > 0, "cross_mtime should be positive"); assert!(ctime >= 0, "cross_ctime should be non-negative"); - + // Test that trait methods match direct extractor calls - assert_eq!(dev, MetadataExtractor::dev(&metadata), "cross_dev should match extractor"); - assert_eq!(ino, MetadataExtractor::ino(&metadata), "cross_ino should match extractor"); - assert_eq!(mode, MetadataExtractor::mode(&metadata), "cross_mode should match extractor"); - assert_eq!(nlink, MetadataExtractor::nlink(&metadata), "cross_nlink should match extractor"); - assert_eq!(uid, MetadataExtractor::uid(&metadata), "cross_uid should match extractor"); - assert_eq!(gid, MetadataExtractor::gid(&metadata), "cross_gid should match extractor"); - assert_eq!(rdev, MetadataExtractor::rdev(&metadata), "cross_rdev should match extractor"); - assert_eq!(size, MetadataExtractor::size(&metadata), "cross_size should match extractor"); - assert_eq!(atime, MetadataExtractor::atime(&metadata), "cross_atime should match extractor"); - assert_eq!(mtime, MetadataExtractor::mtime(&metadata), "cross_mtime should match extractor"); - assert_eq!(ctime, MetadataExtractor::ctime(&metadata), "cross_ctime should match extractor"); - + assert_eq!( + dev, + MetadataExtractor::dev(&metadata), + "cross_dev should match extractor" + ); + assert_eq!( + ino, + MetadataExtractor::ino(&metadata), + "cross_ino should match extractor" + ); + assert_eq!( + mode, + MetadataExtractor::mode(&metadata), + "cross_mode should match extractor" + ); + assert_eq!( + nlink, + MetadataExtractor::nlink(&metadata), + "cross_nlink should match extractor" + ); + assert_eq!( + uid, + MetadataExtractor::uid(&metadata), + "cross_uid should match extractor" + ); + assert_eq!( + gid, + MetadataExtractor::gid(&metadata), + "cross_gid should match extractor" + ); + assert_eq!( + rdev, + MetadataExtractor::rdev(&metadata), + "cross_rdev should match extractor" + ); + assert_eq!( + size, + MetadataExtractor::size(&metadata), + "cross_size should match extractor" + ); + assert_eq!( + atime, + MetadataExtractor::atime(&metadata), + "cross_atime should match extractor" + ); + assert_eq!( + mtime, + MetadataExtractor::mtime(&metadata), + "cross_mtime should match extractor" + ); + assert_eq!( + ctime, + MetadataExtractor::ctime(&metadata), + "cross_ctime should match extractor" + ); + println!("✅ All trait extension methods work correctly and match extractors"); } } From 062c0ee0bf5feadd81580756e6847151c92d0ff4 Mon Sep 17 00:00:00 2001 From: chethanuk Date: Wed, 20 Aug 2025 04:29:04 +0700 Subject: [PATCH 4/9] Update ci.yaml --- .github/workflows/ci.yaml | 49 ++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 300a4f9..507438c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,4 @@ name: CI - on: push: tags: @@ -7,34 +6,42 @@ on: branches: - main pull_request: - permissions: checks: write pull-requests: write env: CARGO_TERM_COLOR: always - jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: Swatinem/rust-cache@v2 with: - prefix-key: v1-rust + prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: true - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true - - name: Install moonbit + components: rustfmt, clippy + - name: Install moonbit (Unix) + if: runner.os != 'Windows' run: | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s -- 0.6.19 echo "$HOME/.moon/bin" >> $GITHUB_PATH + - name: Install moonbit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + $env:MOONBIT_INSTALL_VERSION = "0.6.19" + irm https://cli.moonbitlang.com/install/powershell.ps1 | iex + echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm working-directory: core @@ -46,28 +53,38 @@ jobs: run: cargo build --all-features --all-targets test: needs: [build] - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: Swatinem/rust-cache@v2 with: - prefix-key: v1-rust + prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: false - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true + components: rustfmt, clippy - uses: cargo-bins/cargo-binstall@main - name: Install wasmtime-cli run: cargo binstall --force --locked wasmtime-cli@33.0.0 - - name: Install moonbit + - name: Install moonbit (Unix) + if: runner.os != 'Windows' run: | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s -- 0.6.19 echo "$HOME/.moon/bin" >> $GITHUB_PATH + - name: Install moonbit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + $env:MOONBIT_INSTALL_VERSION = "0.6.19" + irm https://cli.moonbitlang.com/install/powershell.ps1 | iex + echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm working-directory: core @@ -83,7 +100,7 @@ jobs: publish: needs: [test] if: "startsWith(github.ref, 'refs/tags/v')" - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Publish on Ubuntu only steps: - name: Checkout uses: actions/checkout@v4 From 7591dba425569a38fa122209841a654ca8a5c15d Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Wed, 20 Aug 2025 00:01:26 -0700 Subject: [PATCH 5/9] Fix the tests and update moonc --- moonc_wasm/src/cross_platform.rs | 23 ++++++++++----- moonc_wasm/src/wasmoo_extern.rs | 19 ++++++------ moonc_wasm/tests/cross_platform_tests.rs | 37 +++++++++++++++++++----- moonc_wasm/tests/unit_tests.rs | 10 +++---- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/moonc_wasm/src/cross_platform.rs b/moonc_wasm/src/cross_platform.rs index 3cb07d2..dec9a08 100644 --- a/moonc_wasm/src/cross_platform.rs +++ b/moonc_wasm/src/cross_platform.rs @@ -94,7 +94,7 @@ impl MetadataExtractor { } /// Extract number of hard links - pub fn nlink(metadata: &Metadata) -> u64 { + pub fn nlink(_metadata: &Metadata) -> u64 { cfg_if::cfg_if! { if #[cfg(windows)] { // Windows files typically have 1 link @@ -108,7 +108,7 @@ impl MetadataExtractor { } /// Extract user ID (Windows: simulate with 0, Unix: actual UID) - pub fn uid(metadata: &Metadata) -> u32 { + pub fn uid(_metadata: &Metadata) -> u32 { cfg_if::cfg_if! { if #[cfg(windows)] { // Windows uses SIDs, not UIDs. For compatibility, return 0 @@ -123,7 +123,7 @@ impl MetadataExtractor { } /// Extract group ID (Windows: simulate with 0, Unix: actual GID) - pub fn gid(metadata: &Metadata) -> u32 { + pub fn gid(_metadata: &Metadata) -> u32 { cfg_if::cfg_if! { if #[cfg(windows)] { // Windows uses SIDs, not GIDs. For compatibility, return 0 @@ -251,13 +251,20 @@ impl PermissionsBuilder { .truncate(true) .open(&temp_path) { - let mut perms = fs::metadata(&temp_path).unwrap().permissions(); - perms.set_readonly(read_only); + if let Ok(metadata) = fs::metadata(&temp_path) { + let mut perms = metadata.permissions(); + perms.set_readonly(read_only); - // Clean up - let _ = fs::remove_file(temp_path); + // Clean up + let _ = fs::remove_file(temp_path); - perms + perms + } else { + // If we can't get metadata, return default permissions + fs::metadata(".") + .unwrap_or_else(|_| fs::metadata("C:\\").unwrap()) + .permissions() + } } else { // Fallback to current directory permissions fs::metadata(".").unwrap().permissions() diff --git a/moonc_wasm/src/wasmoo_extern.rs b/moonc_wasm/src/wasmoo_extern.rs index a76cdf6..9b99da8 100644 --- a/moonc_wasm/src/wasmoo_extern.rs +++ b/moonc_wasm/src/wasmoo_extern.rs @@ -274,7 +274,7 @@ fn open( let flags = args.get(1); let flags = flags.to_number(scope).unwrap().value() as i32; let mode = args.get(2); - let mode = mode.to_number(scope).unwrap().value() as i32; + let _mode = mode.to_number(scope).unwrap().value() as i32; let access_mode = flags & (O_RDONLY | O_WRONLY | O_RDWR); let (read, write) = match access_mode { @@ -303,23 +303,23 @@ fn open( } else if has_creat { opts.create(true); } - let mut custom_flags = 0; + let mut _custom_flags = 0; if (flags & O_NONBLOCK) != 0 { - custom_flags |= platform_constants::O_NONBLOCK; + _custom_flags |= platform_constants::O_NONBLOCK; } if (flags & O_NOCTTY) != 0 { - custom_flags |= platform_constants::O_NOCTTY; + _custom_flags |= platform_constants::O_NOCTTY; } if (flags & O_DSYNC) != 0 { - custom_flags |= platform_constants::O_DSYNC; + _custom_flags |= platform_constants::O_DSYNC; } if (flags & O_SYNC) != 0 { - custom_flags |= platform_constants::O_SYNC; + _custom_flags |= platform_constants::O_SYNC; } cfg_if::cfg_if! { if #[cfg(unix)] { - opts.custom_flags(custom_flags as u32); - opts.mode((mode & 0o777) as u32); + opts.custom_flags(_custom_flags as u32); + opts.mode((_mode & 0o777) as u32); } else { // Windows: set the common FILE_ATTRIBUTE_NORMAL and optionally overlapped I/O use std::os::windows::fs::OpenOptionsExt; @@ -433,7 +433,7 @@ fn access( return; } Ok(metadata) => { - let mode_bits = MetadataExtractor::mode(&metadata); + let _mode_bits = MetadataExtractor::mode(&metadata); if mode & 0o111 == 0 { let message = v8::String::new(scope, "execute permission denied").unwrap(); let exn = v8::Exception::error(scope, message); @@ -630,6 +630,7 @@ fn file_size( ret.set(size.into()); } +#[allow(dead_code)] fn timeval_from_f64(t: f64) -> std::io::Result { if !t.is_finite() { return Err(std::io::Error::new( diff --git a/moonc_wasm/tests/cross_platform_tests.rs b/moonc_wasm/tests/cross_platform_tests.rs index 8c26011..97e67c5 100644 --- a/moonc_wasm/tests/cross_platform_tests.rs +++ b/moonc_wasm/tests/cross_platform_tests.rs @@ -3,18 +3,17 @@ //! This module contains comprehensive parameterized tests using rstest //! to verify cross-platform compatibility of metadata operations. -use rstest::rstest; +use rstest::{fixture, rstest}; use std::fs::{self, File, OpenOptions}; use std::io::Write; -use std::path::PathBuf; use tempfile::TempDir; use moonc_wasm::cross_platform::{ - CrossPlatformMetadataExt, MetadataExtractor, PermissionsBuilder, platform_constants, + CrossPlatformMetadataExt, PermissionsBuilder, platform_constants, }; /// Test fixture that creates a temporary directory for each test -#[rstest] +#[fixture] fn temp_dir() -> TempDir { TempDir::new().expect("Failed to create temporary directory") } @@ -104,8 +103,8 @@ fn test_metadata_file_sizes( ); // Verify other metadata fields are reasonable - assert!(dev >= 0, "Device ID should be non-negative"); - assert!(ino >= 0, "Inode should be non-negative"); + assert!(dev > 0 || dev == 0, "Device ID should be valid"); + assert!(ino > 0 || ino == 0, "Inode should be valid"); assert!( atime > 0 || atime == 0, "Access time should be valid timestamp or 0" @@ -205,8 +204,30 @@ fn test_metadata_edge_cases(temp_dir: TempDir, #[case] filename: &str) { match filename { "" => { - // Empty filename should fail - assert!(result.is_err(), "Empty filename should fail"); + // On most platforms, joining empty string to a path returns the directory itself + // which exists (temp_dir), so metadata will succeed + #[cfg(unix)] + { + // On Unix, this might fail depending on the implementation + if result.is_err() { + println!("Empty filename resulted in error (expected on some Unix systems)"); + } else { + // The path likely resolved to the temp directory itself + assert!( + result.unwrap().is_dir(), + "Empty path should resolve to directory" + ); + } + } + #[cfg(windows)] + { + // On Windows, joining empty string returns the directory path + assert!( + result.is_ok(), + "Empty filename should resolve to temp directory on Windows" + ); + assert!(result.unwrap().is_dir(), "Should be a directory"); + } } "nonexistent.txt" => { // Nonexistent file should fail diff --git a/moonc_wasm/tests/unit_tests.rs b/moonc_wasm/tests/unit_tests.rs index d918c25..ee7edad 100644 --- a/moonc_wasm/tests/unit_tests.rs +++ b/moonc_wasm/tests/unit_tests.rs @@ -35,9 +35,9 @@ mod metadata_extractor_tests { let gid = MetadataExtractor::gid(&metadata); let _rdev = MetadataExtractor::rdev(&metadata); let size = MetadataExtractor::size(&metadata); - let atime = MetadataExtractor::atime(&metadata); + let _atime = MetadataExtractor::atime(&metadata); let mtime = MetadataExtractor::mtime(&metadata); - let ctime = MetadataExtractor::ctime(&metadata); + let _ctime = MetadataExtractor::ctime(&metadata); // Verify reasonable values assert!(mode > 0, "Mode should be positive"); @@ -90,15 +90,15 @@ mod metadata_extractor_tests { ); // Test timestamps - let atime = MetadataExtractor::atime(&metadata); + let _atime = MetadataExtractor::atime(&metadata); let mtime = MetadataExtractor::mtime(&metadata); - let ctime = MetadataExtractor::ctime(&metadata); + let _ctime = MetadataExtractor::ctime(&metadata); assert!( mtime > 0 || content.is_empty(), "MTime should be positive for non-empty files" ); - assert!(ctime >= 0, "CTime should be non-negative"); + assert!(_ctime >= 0, "CTime should be non-negative"); println!( "✅ File type {} - Size: {}, MTime: {}", From 6686c22e791c61b43524d7782cce3512eb0c1f18 Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Wed, 20 Aug 2025 00:10:06 -0700 Subject: [PATCH 6/9] Fix the tests and update moonc --- .github/workflows/ci.yaml | 34 ++++++++++++++++++++++++++++++-- Cargo.lock | 1 + Cargo.toml | 1 + moonc_wasm/src/cross_platform.rs | 3 ++- src/lib.rs | 33 +++++++++++++++++++++++++++++-- src/moonc_wasm/mod.rs | 3 ++- 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 507438c..bc48ba5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,9 +27,16 @@ jobs: shared-key: debug cache-all-crates: true - name: Setup Rust + if: runner.os != 'Windows' uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy + - name: Setup Rust + if: runner.os == 'Windows' + run: | + rustup toolchain install stable --profile default --no-self-update + rustup default stable + rustup target add wasm32-wasip1 - name: Install moonbit (Unix) if: runner.os != 'Windows' run: | @@ -40,7 +47,15 @@ jobs: shell: powershell run: | $env:MOONBIT_INSTALL_VERSION = "0.6.19" - irm https://cli.moonbitlang.com/install/powershell.ps1 | iex + Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force + $ProgressPreference = 'SilentlyContinue' + try { + $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 90 + Invoke-Expression $script + } catch { + Write-Error "Failed to download or execute MoonBit installer: $_" + exit 1 + } echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm @@ -67,9 +82,16 @@ jobs: shared-key: debug cache-all-crates: false - name: Setup Rust + if: runner.os != 'Windows' uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy + - name: Setup Rust + if: runner.os == 'Windows' + run: | + rustup toolchain install stable --profile default --no-self-update + rustup default stable + rustup target add wasm32-wasip1 - uses: cargo-bins/cargo-binstall@main - name: Install wasmtime-cli run: cargo binstall --force --locked wasmtime-cli@33.0.0 @@ -83,7 +105,15 @@ jobs: shell: powershell run: | $env:MOONBIT_INSTALL_VERSION = "0.6.19" - irm https://cli.moonbitlang.com/install/powershell.ps1 | iex + Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force + $ProgressPreference = 'SilentlyContinue' + try { + $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 60 + Invoke-Expression $script + } catch { + Write-Error "Failed to download or execute MoonBit installer: $_" + exit 1 + } echo "$env:USERPROFILE\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Bundle core MoonBit library run: moon bundle --target wasm diff --git a/Cargo.lock b/Cargo.lock index c73b3e3..c8d9148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,6 +803,7 @@ dependencies = [ "lazy_static", "libc", "log", + "moonc_wasm", "pretty_env_logger", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 88be567..a7ff439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ indoc = "2.0.6" lazy_static = "1.5.0" libc = "0.2.172" log = "0.4.27" +moonc_wasm = { path = "moonc_wasm" } serde = { version = "1", features = ["derive"] } serde_json = "1" topologic = "1.1.0" diff --git a/moonc_wasm/src/cross_platform.rs b/moonc_wasm/src/cross_platform.rs index dec9a08..401501e 100644 --- a/moonc_wasm/src/cross_platform.rs +++ b/moonc_wasm/src/cross_platform.rs @@ -245,11 +245,12 @@ impl PermissionsBuilder { // Create a temporary file to get permissions, then modify let temp_path = std::env::temp_dir().join("temp_permissions"); - if let Ok(_) = std::fs::OpenOptions::new() + if std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(&temp_path) + .is_ok() { if let Ok(metadata) = fs::metadata(&temp_path) { let mut perms = metadata.permissions(); diff --git a/src/lib.rs b/src/lib.rs index 51a9fce..812c264 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,21 @@ struct MoonC { } impl MoonC { - pub fn run(&self, mut args: Vec) -> anyhow::Result<()> { + pub fn run(&self, args: Vec) -> anyhow::Result<()> { self.ensure_initialized()?; + + // Normalize paths for Windows compatibility + #[cfg(windows)] + let args: Vec = args.into_iter().map(|arg| { + // Convert backslashes to forward slashes for better cross-platform compatibility + arg.replace('\\', "/") + }).collect(); + + #[cfg(not(windows))] + let args = args; + debug!("Running the MoonBit compiler with args: {}", args.join(" ")); + let mut args = args; args.insert(0, "moonc".to_string()); moonc_wasm::run_wasmoo(args).context("Running the MoonBit compiler")?; Ok(()) @@ -73,7 +85,24 @@ impl MoonBitComponent { selected_world: Option<&str>, ) -> anyhow::Result { let temp_dir = Utf8TempDir::new().context("Creating temporary directory")?; - let dir = temp_dir.path().to_path_buf(); + + // Get the canonical path to avoid Windows short names + let dir = { + let path = temp_dir.path(); + // Try to get the canonical path, fall back to original if it fails + if let Ok(canonical) = std::fs::canonicalize(path) { + // Convert to string and remove Windows UNC prefix if present + let canonical_str = canonical.to_string_lossy(); + let clean_path = if canonical_str.starts_with(r"\\?\") { + &canonical_str[4..] + } else { + &canonical_str + }; + Utf8PathBuf::from(clean_path) + } else { + path.to_path_buf() + } + }; info!("Creating MoonBit component in temporary directory: {dir}"); diff --git a/src/moonc_wasm/mod.rs b/src/moonc_wasm/mod.rs index cf851d8..2376fd3 120000 --- a/src/moonc_wasm/mod.rs +++ b/src/moonc_wasm/mod.rs @@ -1 +1,2 @@ -../../moonc_wasm/src/lib.rs \ No newline at end of file +// Re-export the moonc_wasm crate's public items +pub use ::moonc_wasm::*; \ No newline at end of file From 985a207ce0236b75ab6af2290f9a778766c0a18a Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Wed, 20 Aug 2025 00:47:31 -0700 Subject: [PATCH 7/9] Fix clippy: --- mise.toml | 2 + moon.mod.json | 4 ++ moonc_wasm/moon.mod.json | 5 ++ src/lib.rs | 117 +++++++++++++++++---------------------- src/moonc_wasm/mod.rs | 2 +- 5 files changed, 64 insertions(+), 66 deletions(-) create mode 100644 mise.toml create mode 100644 moon.mod.json create mode 100644 moonc_wasm/moon.mod.json diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..95d23b8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +rust = "latest" diff --git a/moon.mod.json b/moon.mod.json new file mode 100644 index 0000000..9037b65 --- /dev/null +++ b/moon.mod.json @@ -0,0 +1,4 @@ +{ + "name": "moonbit-component-generator", + "version": "0.1.0" +} \ No newline at end of file diff --git a/moonc_wasm/moon.mod.json b/moonc_wasm/moon.mod.json new file mode 100644 index 0000000..ebb089a --- /dev/null +++ b/moonc_wasm/moon.mod.json @@ -0,0 +1,5 @@ +{ + "name": "moonc_wasm", + "version": "0.1.0", + "deps": {} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 812c264..6e3278e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,17 +35,20 @@ struct MoonC { impl MoonC { pub fn run(&self, args: Vec) -> anyhow::Result<()> { self.ensure_initialized()?; - + // Normalize paths for Windows compatibility #[cfg(windows)] - let args: Vec = args.into_iter().map(|arg| { - // Convert backslashes to forward slashes for better cross-platform compatibility - arg.replace('\\', "/") - }).collect(); - + let args: Vec = args + .into_iter() + .map(|arg| { + // Convert backslashes to forward slashes for better cross-platform compatibility + arg.replace('\\', "/") + }) + .collect(); + #[cfg(not(windows))] let args = args; - + debug!("Running the MoonBit compiler with args: {}", args.join(" ")); let mut args = args; args.insert(0, "moonc".to_string()); @@ -85,7 +88,7 @@ impl MoonBitComponent { selected_world: Option<&str>, ) -> anyhow::Result { let temp_dir = Utf8TempDir::new().context("Creating temporary directory")?; - + // Get the canonical path to avoid Windows short names let dir = { let path = temp_dir.path(); @@ -93,11 +96,9 @@ impl MoonBitComponent { if let Ok(canonical) = std::fs::canonicalize(path) { // Convert to string and remove Windows UNC prefix if present let canonical_str = canonical.to_string_lossy(); - let clean_path = if canonical_str.starts_with(r"\\?\") { - &canonical_str[4..] - } else { - &canonical_str - }; + let clean_path = canonical_str + .strip_prefix(r"\\?\") + .unwrap_or(&canonical_str); Utf8PathBuf::from(clean_path) } else { path.to_path_buf() @@ -839,33 +840,26 @@ impl MoonBitComponent { .ok_or_else(|| anyhow::anyhow!("Could not find world"))?; let mut imported_interfaces = Vec::new(); for (_, item) in &world.imports { - if let wit_parser::WorldItem::Interface { id, .. } = item { - if let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) { - if let Some(interface_name) = interface.name.as_ref() { - let owner_package = interface.package.ok_or_else(|| { - anyhow::anyhow!( - "Interface '{}' does not have a package", - interface_name - ) + if let wit_parser::WorldItem::Interface { id, .. } = item + && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) + { + if let Some(interface_name) = interface.name.as_ref() { + let owner_package = interface.package.ok_or_else(|| { + anyhow::anyhow!("Interface '{}' does not have a package", interface_name) + })?; + let package = self + .resolve + .as_ref() + .and_then(|r| r.packages.get(owner_package)) + .ok_or_else(|| { + anyhow::anyhow!("Package for interface '{}' not found", interface_name) })?; - let package = self - .resolve - .as_ref() - .and_then(|r| r.packages.get(owner_package)) - .ok_or_else(|| { - anyhow::anyhow!( - "Package for interface '{}' not found", - interface_name - ) - })?; - - imported_interfaces - .push((package.name.clone(), interface_name.to_string())); - } else { - return Err(anyhow::anyhow!( - "Anonymous imported interfaces are not supported" - )); - } + + imported_interfaces.push((package.name.clone(), interface_name.to_string())); + } else { + return Err(anyhow::anyhow!( + "Anonymous imported interfaces are not supported" + )); } } } @@ -880,33 +874,26 @@ impl MoonBitComponent { .ok_or_else(|| anyhow::anyhow!("Could not find world"))?; let mut exported_interfaces = Vec::new(); for (_, item) in &world.exports { - if let wit_parser::WorldItem::Interface { id, .. } = item { - if let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) { - if let Some(interface_name) = interface.name.as_ref() { - let owner_package = interface.package.ok_or_else(|| { - anyhow::anyhow!( - "Interface '{}' does not have a package", - interface_name - ) + if let wit_parser::WorldItem::Interface { id, .. } = item + && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) + { + if let Some(interface_name) = interface.name.as_ref() { + let owner_package = interface.package.ok_or_else(|| { + anyhow::anyhow!("Interface '{}' does not have a package", interface_name) + })?; + let package = self + .resolve + .as_ref() + .and_then(|r| r.packages.get(owner_package)) + .ok_or_else(|| { + anyhow::anyhow!("Package for interface '{}' not found", interface_name) })?; - let package = self - .resolve - .as_ref() - .and_then(|r| r.packages.get(owner_package)) - .ok_or_else(|| { - anyhow::anyhow!( - "Package for interface '{}' not found", - interface_name - ) - })?; - - exported_interfaces - .push((package.name.clone(), interface_name.to_string())); - } else { - return Err(anyhow::anyhow!( - "Anonymous exported interfaces are not supported" - )); - } + + exported_interfaces.push((package.name.clone(), interface_name.to_string())); + } else { + return Err(anyhow::anyhow!( + "Anonymous exported interfaces are not supported" + )); } } } diff --git a/src/moonc_wasm/mod.rs b/src/moonc_wasm/mod.rs index 2376fd3..88f6bd8 120000 --- a/src/moonc_wasm/mod.rs +++ b/src/moonc_wasm/mod.rs @@ -1,2 +1,2 @@ // Re-export the moonc_wasm crate's public items -pub use ::moonc_wasm::*; \ No newline at end of file +pub use ::moonc_wasm::*; From e0743a40d099fbe722ed60e0c500b662a2a92f9a Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Wed, 20 Aug 2025 01:10:39 -0700 Subject: [PATCH 8/9] Fix clippy locally --- .github/workflows/ci.yaml | 6 ++++-- moonc_wasm/.mise.toml | 17 +++++++++++++++-- src/moonc_wasm/mod.rs | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bc48ba5..cf78614 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,6 +26,7 @@ jobs: prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: true + timeout-minutes: 10 - name: Setup Rust if: runner.os != 'Windows' uses: dtolnay/rust-toolchain@stable @@ -50,7 +51,7 @@ jobs: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force $ProgressPreference = 'SilentlyContinue' try { - $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 90 + $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 120 Invoke-Expression $script } catch { Write-Error "Failed to download or execute MoonBit installer: $_" @@ -81,6 +82,7 @@ jobs: prefix-key: v1-rust-${{ matrix.os }} shared-key: debug cache-all-crates: false + timeout-minutes: 10 - name: Setup Rust if: runner.os != 'Windows' uses: dtolnay/rust-toolchain@stable @@ -108,7 +110,7 @@ jobs: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force $ProgressPreference = 'SilentlyContinue' try { - $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 60 + $script = Invoke-RestMethod -Uri "https://cli.moonbitlang.com/install/powershell.ps1" -TimeoutSec 120 Invoke-Expression $script } catch { Write-Error "Failed to download or execute MoonBit installer: $_" diff --git a/moonc_wasm/.mise.toml b/moonc_wasm/.mise.toml index 773da5d..e1db791 100644 --- a/moonc_wasm/.mise.toml +++ b/moonc_wasm/.mise.toml @@ -10,7 +10,12 @@ run = "cargo build --release --target aarch64-pc-windows-msvc" [tasks.test] description = "Run all tests with output" -run = "cargo test --all -- --nocapture" +run = [ + "cargo test --test cross_platform_tests -- --nocapture", + "cargo test --test integration_tests -- --nocapture", + "cargo test --test simple_test -- --nocapture", + "cargo test --lib -- --nocapture" +] [tasks.test-unit] description = "Run unit tests only" @@ -18,7 +23,7 @@ run = "cargo test --lib -- --nocapture" [tasks.test-integration] description = "Run integration tests only" -run = "cargo test --test '*' -- --nocapture" +run = "cargo test --test integration_tests -- --nocapture" [tasks.test-quiet] description = "Run all tests quietly" @@ -28,5 +33,13 @@ run = "cargo test --all" description = "Run all tests with verbose output" run = "cargo test --all --verbose -- --nocapture" +[tasks.test-cross-platform] +description = "Run cross-platform tests only" +run = "cargo test --test cross_platform_tests -- --nocapture" + +[tasks.test-simple] +description = "Run simple tests only" +run = "cargo test --test simple_test -- --nocapture" + [tasks.release_windows] depends = ["build_windows"] diff --git a/src/moonc_wasm/mod.rs b/src/moonc_wasm/mod.rs index 88f6bd8..2376fd3 120000 --- a/src/moonc_wasm/mod.rs +++ b/src/moonc_wasm/mod.rs @@ -1,2 +1,2 @@ // Re-export the moonc_wasm crate's public items -pub use ::moonc_wasm::*; +pub use ::moonc_wasm::*; \ No newline at end of file From 8dedc8a5ecd0fa44a533253f247944119a6e374c Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Wed, 20 Aug 2025 01:54:48 -0700 Subject: [PATCH 9/9] FIx moonc for windows os error 87 - draft --- .github/workflows/ci.yaml | 4 +- mise.toml | 2 +- src/lib.rs | 154 +++++++++++++++++++++++++++++++++----- src/moonc_wasm/mod.rs | 2 +- 4 files changed, 139 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cf78614..31cde7e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,8 @@ jobs: run: | rustup toolchain install stable --profile default --no-self-update rustup default stable - rustup target add wasm32-wasip1 + rustup component add rustfmt clippy + rustup target add wasm32-wasip1 - name: Install moonbit (Unix) if: runner.os != 'Windows' run: | @@ -93,6 +94,7 @@ jobs: run: | rustup toolchain install stable --profile default --no-self-update rustup default stable + rustup component add rustfmt clippy rustup target add wasm32-wasip1 - uses: cargo-bins/cargo-binstall@main - name: Install wasmtime-cli diff --git a/mise.toml b/mise.toml index 95d23b8..3156a04 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -rust = "latest" +rust = "stable" diff --git a/src/lib.rs b/src/lib.rs index 6e3278e..1ad0dd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,17 @@ impl MoonC { let args: Vec = args .into_iter() .map(|arg| { - // Convert backslashes to forward slashes for better cross-platform compatibility + // Special handling for package source arguments (format: package:path) + if arg.contains(":") && !arg.starts_with("-") { + // Check if this looks like a package:path argument + if let Some((pkg, path)) = arg.split_once(':') { + // Only convert the path part if it looks like a Windows path + if path.contains('\\') || (path.len() > 2 && &path[1..2] == ":") { + return format!("{}:{}", pkg, path.replace('\\', "/")); + } + } + } + // Convert all backslashes to forward slashes for other arguments arg.replace('\\', "/") }) .collect(); @@ -50,6 +60,13 @@ impl MoonC { let args = args; debug!("Running the MoonBit compiler with args: {}", args.join(" ")); + + // Additional debug logging for Windows + #[cfg(windows)] + for (i, arg) in args.iter().enumerate() { + debug!(" Arg[{}]: {:?}", i, arg); + } + let mut args = args; args.insert(0, "moonc".to_string()); moonc_wasm::run_wasmoo(args).context("Running the MoonBit compiler")?; @@ -80,6 +97,11 @@ pub struct MoonBitComponent { } impl MoonBitComponent { + /// Normalizes paths to use forward slashes for cross-platform compatibility + fn normalize_path(&self, path: &Utf8Path) -> String { + path.as_str().replace('\\', "/") + } + /// Initializes a new MoonBit component that implements the given WIT interface. /// /// This step will create a temporary directory and generate MoonBit WIT bindings in it. @@ -92,16 +114,32 @@ impl MoonBitComponent { // Get the canonical path to avoid Windows short names let dir = { let path = temp_dir.path(); - // Try to get the canonical path, fall back to original if it fails - if let Ok(canonical) = std::fs::canonicalize(path) { - // Convert to string and remove Windows UNC prefix if present - let canonical_str = canonical.to_string_lossy(); - let clean_path = canonical_str - .strip_prefix(r"\\?\") - .unwrap_or(&canonical_str); - Utf8PathBuf::from(clean_path) - } else { - path.to_path_buf() + // Try to get the canonical UTF-8 path, fall back to original if it fails + match path.canonicalize_utf8() { + Ok(mut canonical) => { + // On Windows, strip the UNC prefix if present + #[cfg(target_os = "windows")] + { + let canonical_str = canonical.as_str(); + if let Some(clean_path) = canonical_str.strip_prefix(r"\\?\") { + canonical = Utf8PathBuf::from(clean_path); + } + } + + // Normalize separators to forward slashes for cross-platform consistency + #[cfg(target_os = "windows")] + { + let normalized = canonical.as_str().replace('\\', "/"); + Utf8PathBuf::from(normalized) + } + + #[cfg(not(target_os = "windows"))] + canonical + } + Err(_) => { + // Fall back to converting the original path to Utf8PathBuf + path.to_path_buf() + } } }; @@ -595,22 +633,27 @@ impl MoonBitComponent { let mut args = vec!["build-package".to_string()]; for file in mbt_files { let full_path = self.dir.join(file); - args.push(full_path.to_string()); + // Verify file exists before passing to moonc + if !full_path.exists() { + anyhow::bail!("Source file does not exist: {}", full_path); + } + // Ensure forward slashes for moonc compatibility + args.push(self.normalize_path(&full_path)); } for w in warning_control { args.push("-w".to_string()); args.push(w.to_string()); } args.push("-o".to_string()); - args.push(self.dir.join(output).to_string()); + args.push(self.normalize_path(&self.dir.join(output))); args.push("-pkg".to_string()); args.push(package.to_string()); args.push("-std-path".to_string()); - args.push(self.core_bundle_dir().to_string()); + args.push(self.normalize_path(&self.core_bundle_dir())); for (dep_path, dep_name) in dependencies { args.push("-i".to_string()); let full_path = self.dir.join(dep_path); - args.push(format!("{full_path}:{dep_name}")); + args.push(format!("{}:{dep_name}", self.normalize_path(&full_path))); } self.add_package_sources(&mut args, package_sources); args.push("-target".to_string()); @@ -636,17 +679,17 @@ impl MoonBitComponent { for file in core_files { let full_path = self.dir.join(file); - args.push(full_path.to_string()); + args.push(self.normalize_path(&full_path)); } args.push("-main".to_string()); args.push(main_package_name.to_string()); args.push("-o".to_string()); - args.push(self.module_wasm().to_string()); + args.push(self.normalize_path(&self.module_wasm())); args.push("-pkg-config-path".to_string()); - args.push(self.dir.join(main_package_json).to_string()); + args.push(self.normalize_path(&self.dir.join(main_package_json))); self.add_package_sources(&mut args, package_sources); args.push("-pkg-sources".to_string()); - args.push(format!("moonbitlang/core:{}", self.core_dir())); + args.push(format!("moonbitlang/core:{}", self.normalize_path(&self.core_dir()))); args.push("-target".to_string()); args.push("wasm".to_string()); args.push(format!( @@ -670,7 +713,7 @@ impl MoonBitComponent { for (source_name, source_path) in package_sources { args.push("-pkg-sources".to_string()); let full_path = self.dir.join(source_path); - args.push(format!("{source_name}:{full_path}")); + args.push(format!("{source_name}:{}", self.normalize_path(&full_path))); } } @@ -973,3 +1016,74 @@ fn initialize_trace() -> Trace { .init(); Trace } + +#[cfg(test)] +mod path_tests { + use super::Trace; + use camino::Utf8PathBuf; + use camino_tempfile::Utf8TempDir; + + test_r::inherit_test_dep!(Trace); + + #[test_r::test] + fn test_canonicalization_preserves_utf8_and_normalizes_paths(_trace: &Trace) { + // Create a temporary directory + let temp_dir = Utf8TempDir::new().expect("Failed to create temp dir"); + + // Apply our canonicalization logic + let dir = { + let path = temp_dir.path(); + match path.canonicalize_utf8() { + Ok(mut canonical) => { + // On Windows, strip the UNC prefix if present + #[cfg(target_os = "windows")] + { + let canonical_str = canonical.as_str(); + if let Some(clean_path) = canonical_str.strip_prefix(r"\\?\") { + canonical = Utf8PathBuf::from(clean_path); + } + } + + // Normalize separators to forward slashes for cross-platform consistency + #[cfg(target_os = "windows")] + { + let normalized = canonical.as_str().replace('\\', "/"); + Utf8PathBuf::from(normalized) + } + + #[cfg(not(target_os = "windows"))] + canonical + } + Err(_) => path.to_path_buf(), + } + }; + + // Verify the path doesn't contain UNC prefix + assert!( + !dir.as_str().starts_with(r"\\?\"), + "Path should not start with UNC prefix" + ); + + // On Windows, verify no backslashes remain + #[cfg(target_os = "windows")] + assert!( + !dir.as_str().contains('\\'), + "Path should not contain backslashes on Windows" + ); + + // Verify path operations work correctly + let subdir = dir.join("test"); + // The joined path should end with our test directory + assert!( + subdir.as_str().ends_with("/test") || subdir.as_str().ends_with("\\test"), + "Expected path to end with /test or \\test, got: {}", + subdir + ); + + // Verify parent path calculation works + assert!( + dir.parent().is_some(), + "Should be able to get parent directory" + ); + } +} diff --git a/src/moonc_wasm/mod.rs b/src/moonc_wasm/mod.rs index 2376fd3..88f6bd8 120000 --- a/src/moonc_wasm/mod.rs +++ b/src/moonc_wasm/mod.rs @@ -1,2 +1,2 @@ // Re-export the moonc_wasm crate's public items -pub use ::moonc_wasm::*; \ No newline at end of file +pub use ::moonc_wasm::*;