rb-sys-test-helpers

The rb-sys-test-helpers crate provides utilities for testing Ruby extensions from Rust. It makes it easy to run tests with a valid Ruby VM.

Usage

Add this to your Cargo.toml:

[dev-dependencies]
rb-sys-env = { version = "0.1" }
rb-sys-test-helpers = { version = "0.2" }

Then, in your crate's build.rs:

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = rb_sys_env::activate()?;

    Ok(())
}

Then, you can use the ruby_test attribute macro in your tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use rb_sys_test_helpers::ruby_test;
    use rb_sys::{rb_num2fix, rb_int2big, FIXNUM_P};

    #[ruby_test]
    fn test_something() {
        // Your test code here will have a valid Ruby VM (hint: this works with
        // the `magnus` crate, too!)
        //
        // ...

        let int = unsafe { rb_num2fix(1) };
        let big = unsafe { rb_int2big(9999999) };

        assert!(FIXNUM_P(int));
        assert!(!FIXNUM_P(big));
    }
}
}

How It Works

The ruby_test macro sets up a Ruby VM before running your test and tears it down afterward. This allows you to interact with Ruby from your Rust code during tests without having to set up the VM yourself.

The test helpers are compatible with both rb-sys for low-level C API access and magnus for higher-level Ruby interactions.

Common Testing Patterns

Testing Value Conversions

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_value_conversion() {
    use rb_sys::{rb_cObject, rb_funcall, rb_str_new_cstr, rb_iv_set};
    use std::ffi::CString;

    unsafe {
        let obj = rb_cObject;
        let name = CString::new("test").unwrap();
        let value = rb_str_new_cstr(name.as_ptr());

        rb_iv_set(obj, b"@name\0".as_ptr() as *const _, value);

        let result = rb_funcall(obj, b"instance_variable_get\0".as_ptr() as *const _, 1,
                                rb_str_new_cstr(b"@name\0".as_ptr() as *const _));

        assert_eq!(value, result);
    }
}
}

Testing with Magnus

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_with_magnus() {
    use magnus::{Ruby, RString, Value};

    let ruby = unsafe { Ruby::get().unwrap() };
    let string = RString::new(ruby, "Hello, world!").unwrap();

    assert_eq!(string.to_string().unwrap(), "Hello, world!");
}
}

Testing Multiple Ruby Versions

To test against multiple Ruby versions, you can use environment variables and CI configuration:

# .github/workflows/test.yml
jobs:
  test:
    strategy:
      matrix:
        ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
    steps:
      - uses: actions/checkout@v4
      - uses: oxidize-rb/actions/setup-ruby-and-rust@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - run: cargo test

Your tests will run against each Ruby version in the matrix, helping you ensure compatibility.

Integration with Other Test Frameworks

The ruby_test attribute works with common Rust test frameworks like proptest and quickcheck:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_with_proptest() {
    use proptest::prelude::*;

    proptest!(|(s in "[a-zA-Z0-9]*")| {
        let ruby = unsafe { Ruby::get().unwrap() };
        let ruby_string = RString::new(ruby, &s).unwrap();
        assert_eq!(ruby_string.to_string().unwrap(), s);
    });
}
}