diff --git a/CHANGELOG.md b/CHANGELOG.md index bcdc5c6..08438ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 default), and return a clear `DevkitError::RateLimited` when the cap would be exceeded. +## [Unreleased] + +### Added + +- Cross-platform tests for XDR big-endian encoding, path separator handling, + `..` normalization, and CRLF input across the CI operating systems. See + `docs/TESTING.md`. + ## [0.1.0] - 2026-10-28 First tagged release. Prebuilt binaries for Linux x86_64, macOS arm64, and diff --git a/crates/devkit-cli/tests/path_normalization.rs b/crates/devkit-cli/tests/path_normalization.rs new file mode 100644 index 0000000..7d32da7 --- /dev/null +++ b/crates/devkit-cli/tests/path_normalization.rs @@ -0,0 +1,56 @@ +//! Cross-platform path handling for file-based commands. + +use std::fs; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn unique_dir() -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "stellar-foundry-paths-{}-{suffix}", + std::process::id() + )) +} + +fn inspect(path: &str) -> Output { + Command::new(env!("CARGO_BIN_EXE_stellar-foundry")) + .args(["--format", "json", "inspect-wasm", path]) + .output() + .expect("run inspect-wasm") +} + +#[test] +fn native_and_forward_slash_paths_produce_identical_output() { + let dir = unique_dir(); + let nested = dir.join("nested"); + fs::create_dir_all(&nested).expect("create fixture directory"); + let wasm = nested.join("minimal.wasm"); + fs::write(&wasm, [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]).expect("write minimal wasm"); + + let native = wasm.to_string_lossy().to_string(); + let forward = native.replace('\\', "/"); + let with_parent = nested + .join("..") + .join("nested") + .join("minimal.wasm") + .to_string_lossy() + .to_string(); + + let native_output = inspect(&native); + let forward_output = inspect(&forward); + let parent_output = inspect(&with_parent); + + assert!( + native_output.status.success(), + "native path failed: {}", + String::from_utf8_lossy(&native_output.stderr) + ); + assert_eq!(native_output.stdout, forward_output.stdout); + assert_eq!(native_output.stdout, parent_output.stdout); + + fs::remove_dir_all(&dir).expect("remove fixture directory"); +} diff --git a/crates/devkit-core/tests/cross_platform.rs b/crates/devkit-core/tests/cross_platform.rs new file mode 100644 index 0000000..26645eb --- /dev/null +++ b/crates/devkit-core/tests/cross_platform.rs @@ -0,0 +1,68 @@ +//! Cross-platform behaviour: path separators and line endings. +//! +//! CI runs these tests on Linux, macOS, and Windows. They assert host-independent +//! values rather than comparing against a platform-specific string. + +use std::path::{Component, Path, PathBuf}; + +use devkit_core::{Network, OutputFormat}; + +#[test] +fn native_and_forward_slash_paths_have_identical_components() { + let forward = Path::new("fixtures/malformed/scval-empty.txt"); + let native: PathBuf = ["fixtures", "malformed", "scval-empty.txt"] + .iter() + .collect(); + + let forward_components: Vec> = forward.components().collect(); + let native_components: Vec> = native.components().collect(); + + assert_eq!(forward_components, native_components); + assert_eq!( + forward_components + .iter() + .filter_map(|component| match component { + Component::Normal(part) => Some(*part), + _ => None, + }) + .collect::>(), + vec![ + std::ffi::OsStr::new("fixtures"), + std::ffi::OsStr::new("malformed"), + std::ffi::OsStr::new("scval-empty.txt"), + ] + ); +} + +#[test] +fn current_directory_components_are_removed_from_both_separator_styles() { + let forward = Path::new("fixtures/./malformed/scval-empty.txt"); + let native = Path::new("fixtures") + .join(".") + .join("malformed") + .join("scval-empty.txt"); + + let has_current_dir = |path: &Path| { + path.components() + .any(|component| component == Component::CurDir) + }; + assert!(!has_current_dir(forward)); + assert!(!has_current_dir(&native)); + assert_eq!( + forward.components().collect::>(), + native.components().collect::>() + ); +} + +#[test] +fn parsers_ignore_platform_line_endings_and_surrounding_whitespace() { + assert_eq!( + "json\r\n".parse::().unwrap(), + OutputFormat::Json + ); + assert_eq!("testnet\r\n".parse::().unwrap(), Network::Testnet); + assert_eq!( + " JSON ".parse::().unwrap(), + OutputFormat::Json + ); +} diff --git a/crates/devkit-xdr/tests/endianness.rs b/crates/devkit-xdr/tests/endianness.rs new file mode 100644 index 0000000..71c12dc --- /dev/null +++ b/crates/devkit-xdr/tests/endianness.rs @@ -0,0 +1,47 @@ +//! Byte-order and cross-platform stability tests. +//! +//! XDR is big-endian on every host. These tests use explicit byte sequences so +//! they do not depend on the host's native endianness, and they assert the +//! stable JSON produced by the decoder. + +use devkit_xdr::inspect_scval_base64; +use stellar_xdr::{Limits, ReadXdr, ScVal, WriteXdr}; + +#[test] +fn u32_uses_big_endian_discriminant_and_payload() { + let value = ScVal::U32(0x0102_0304); + let encoded = value.to_xdr(Limits::none()).expect("encode scval"); + + // SCVal discriminant 3 (U32), then the value in network byte order. + assert_eq!(encoded, vec![0, 0, 0, 3, 1, 2, 3, 4]); + + let decoded = ScVal::from_xdr(&encoded, Limits::none()).expect("decode scval"); + assert_eq!(decoded, value); +} + +#[test] +fn i32_keeps_twos_complement_in_big_endian_order() { + let bytes = [0, 0, 0, 4, 0xff, 0xff, 0xff, 0xfe]; + let value = ScVal::from_xdr(bytes, Limits::none()).expect("decode i32"); + assert_eq!(value, ScVal::I32(-2)); + assert_eq!(value.to_xdr(Limits::none()).unwrap(), bytes); +} + +#[test] +fn i64_uses_big_endian_discriminant_and_payload() { + let value = ScVal::I64(0x0102_0304_0506_0708); + let encoded = value.to_xdr(Limits::none()).expect("encode scval"); + + // SCVal discriminant 6 (I64), then eight big-endian bytes. + assert_eq!(encoded, vec![0, 0, 0, 6, 1, 2, 3, 4, 5, 6, 7, 8]); + + let decoded = ScVal::from_xdr(&encoded, Limits::none()).expect("decode scval"); + assert_eq!(decoded, value); +} + +#[test] +fn base64_and_json_output_are_identical_on_every_host() { + // `00 00 00 03 01 02 03 04` is ScVal::U32(0x01020304). + let json = inspect_scval_base64("AAAAAwECAwQ=").expect("decode scval document"); + assert_eq!(json["u32"], 0x0102_0304u32); +} diff --git a/docs/TESTING.md b/docs/TESTING.md index d00cff9..d531cae 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -13,6 +13,24 @@ cargo test --all Unit and integration tests live next to the code (`#[cfg(test)]` modules) and in `tests/`. Decoding changes must include malformed-input tests. The shared `fixtures/malformed/` corpus contains small, named samples for truncated XDR, invalid discriminants, empty input, and an overlong length prefix. `crates/devkit-xdr/tests/malformed_corpus.rs` runs each fixture through its intended decoder and requires a structured `DevkitError::Decode`; an unexpected successful decode or panic fails the test. +## Cross-platform and endianness tests + +CI runs the suite on Linux, macOS, and Windows. The following tests pin behavior +that otherwise differs between hosts: + +- `crates/devkit-xdr/tests/endianness.rs` asserts XDR's big-endian wire format + with explicit byte sequences (`ScVal::U32`, `ScVal::I32`, and `ScVal::I64`), + round-trips them, and checks that base64/JSON output is host-independent. +- `crates/devkit-core/tests/cross_platform.rs` asserts that native and + forward-slash paths produce the same components, that `.` components are + removed, and that parsers accept CRLF-terminated input. +- `crates/devkit-cli/tests/path_normalization.rs` runs `inspect-wasm` with a + native path, a forward-slash path, and a path containing `..`, and requires + byte-identical JSON output. + +Use explicit byte arrays rather than values derived from the host when testing +encodings, and compare path *components* rather than rendered separator strings. + ## Fuzzing Fuzz targets live in the standalone `fuzz/` crate (it is not part of the main