Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions crates/devkit-cli/tests/path_normalization.rs
Original file line number Diff line number Diff line change
@@ -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");
}
68 changes: 68 additions & 0 deletions crates/devkit-core/tests/cross_platform.rs
Original file line number Diff line number Diff line change
@@ -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<Component<'_>> = forward.components().collect();
let native_components: Vec<Component<'_>> = 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<_>>(),
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::<Vec<_>>(),
native.components().collect::<Vec<_>>()
);
}

#[test]
fn parsers_ignore_platform_line_endings_and_surrounding_whitespace() {
assert_eq!(
"json\r\n".parse::<OutputFormat>().unwrap(),
OutputFormat::Json
);
assert_eq!("testnet\r\n".parse::<Network>().unwrap(), Network::Testnet);
assert_eq!(
" JSON ".parse::<OutputFormat>().unwrap(),
OutputFormat::Json
);
}
47 changes: 47 additions & 0 deletions crates/devkit-xdr/tests/endianness.rs
Original file line number Diff line number Diff line change
@@ -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);
}
18 changes: 18 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading