Repository: extendr/extendr
Pull request #373 (2022-02-05 04:17:05)

"graphics" feature gate
This pull request move graphics-related features behind a feature gate because graphics is not what every user needs. I was trying to create a separate crate for graphics, but I found it's a bit difficult to untie the code that are tightly related each other.

Ilia-Kosenkov: It is time for `extendr` to get a separate `Features` section in Readme, it is hard to track all the options we have right now.
Ilia-Kosenkov: My preference: a separate PR summarizing features as a list in `extendr` README and somewhere on the web site, perhaps also on the front page.
Ilia-Kosenkov: I stumbled upon this crate [serial_test](https://crates.io/crates/serial_test). Have you ever tried it out? I wonder if decorating your graphics tests with `#[serial(graphics)]` can help with memory issues while still running tests multithreaded?
(Note that crate guarantees that marked test will run in serial, but other tests will run arbitrary, including in parallel to the `#[serial()]` ones).
yutannihilation: Yes, I already tried it, but it didn't save me from #370. I guess this part matters

> but other tests will run arbitrary
yutannihilation: But I didn't investigate much, so it might be worth looking it again at some point....
yutannihilation: Thanks! The label is really useful.
yutannihilation: @andy-thomason 
I'm going to merge this in a week. If you have any concern on moving graphics-related code into a feature gate, please comment here (if not, please just ignore).
yutannihilation: Merging this and will address https://github.com/extendr/extendr/issues/378

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 772b7f477..fa8ca7d09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,8 @@
 
 - Removed `TryFrom` conversions between `Robj` and `HashMap` for consistency. `List::into_hashmap()` and `List::from_hashmap()` should be used instead.
 
+- Added support for the graphics-related API (`graphics` feature flag, disabled by default).
+
 ## extendr 0.2.0
 
 - Added contributing guidelines and code of conduct.
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index 8e5a61fe4..0b1634544 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -23,16 +23,19 @@ lazy_static = "1.4"
 paste = "1.0.5"
 serde = { version = "1.0", features = ["derive"], optional = true }
 num-complex = { version = "0.4", optional = true }
-
-# TODO: libc is only needed to allocate the memory for DevDesc. Remove this when we
-# separate the code into extendr-graphics crate.
-libc = "0.2"
+libc = { version = "0.2", optional = true }
 
 [features]
 default = []
 
+# libc is needed to allocate a DevDesc (c.f., https://bugs.r-project.org/show_bug.cgi?id=18292)
+graphics = ["libc"]
+
 # All features to test
-tests-all = ["ndarray", "libR-sys/use-bindgen", "serde", "num-complex"]
+tests-all = ["ndarray", "libR-sys/use-bindgen", "serde", "num-complex", "graphics"]
 
 # The minimal set of features without all optional ones
 tests-minimal = ["libR-sys/use-bindgen"]
+
+[package.metadata.docs.rs]
+features = ["graphics"]
diff --git a/extendr-api/README.md b/extendr-api/README.md
index 29885433e..f47afa455 100644
--- a/extendr-api/README.md
+++ b/extendr-api/README.md
@@ -220,4 +220,15 @@ test! {
 }
 ```
 
-License: MIT
+## Feature gates
+
+extendr-api has some optional features behind these feature gates:
+
+- `ndarray`: provides the conversion between R's matrices and [ndarray](https://docs.rs/ndarray/latest/ndarray/).
+- `num-complex`: provides the conversion between R's complex numbers and [num-complex](https://docs.rs/num-complex/latest/num_complex/).
+- `serde`: provides the [Serde](https://serde.rs/) support.
+- `graphics`: provides the functionality to control or implement graphics devices.
+
+## License
+
+MIT
diff --git a/extendr-api/src/graphics/device_descriptor.rs b/extendr-api/src/graphics/device_descriptor.rs
index cb3d39c4a..548d4f167 100644
--- a/extendr-api/src/graphics/device_descriptor.rs
+++ b/extendr-api/src/graphics/device_descriptor.rs
@@ -74,7 +74,7 @@ pub(crate) enum DevCapLocator {
     Yes = 2,
 }
 
-/// A builder of `DevDesc`.
+/// A builder of [libR_sys::DevDesc].
 ///
 // # Design notes (which feels a bit too internal to be exposed as an official document)
 //
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index df8c0c5d2..4b1b622e6 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -218,6 +218,14 @@
 //! }
 //! ```
 //!
+//! ## Feature gates
+//!
+//! extendr-api has some optional features behind these feature gates:
+//!
+//! - `ndarray`: provides the conversion between R's matrices and [ndarray](https://docs.rs/ndarray/latest/ndarray/).
+//! - `num-complex`: provides the conversion between R's complex numbers and [num-complex](https://docs.rs/num-complex/latest/num_complex/).
+//! - `serde`: provides the [Serde](https://serde.rs/) support.
+//! - `graphics`: provides the functionality to control or implement graphics devices.
 
 #![doc(
     html_logo_url = "https://raw.githubusercontent.com/extendr/extendr/master/extendr-logo-256.png"
@@ -239,7 +247,9 @@ pub mod serializer;
 #[cfg(feature = "serde")]
 pub mod deserializer;
 
+#[cfg(feature = "graphics")]
 pub mod graphics;
+
 pub mod robj;
 pub mod scalar;
 pub mod thread_safety;


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 89334694b..41fed61c2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -414,11 +414,10 @@ jobs:
 
             ci-cargo +$toolchain test --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture -ActionName "Test extendr-engine \w tests-all for $target target"
 
-            # TODO: remove --test-threads=1 when extendr-graphics            
+            # TODO: --test-threads=1 is needed because graphics-related tests need to run in serial
             ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture --test-threads=1 -ActionName "Test extendr-api \w tests-all for $target target"
 
-            # TODO: remove --test-threads=1 when extendr-graphics            
-            ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture --test-threads=1 -ActionName "Test extendr-api \w tests-minimal for $target target"
+            ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture -ActionName "Test extendr-api \w tests-minimal for $target target"
                       
             ci-cargo +$toolchain test --manifest-path extendr-macros/Cargo.toml  $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture -ActionName "Test extendr-macros for $target target"
             
diff --git a/extendr-api/tests/graphics_tests.rs b/extendr-api/tests/graphics_tests.rs
index 9035aa0a7..37449fbd9 100644
--- a/extendr-api/tests/graphics_tests.rs
+++ b/extendr-api/tests/graphics_tests.rs
@@ -1,353 +1,356 @@
-use std::fmt::Write;
-
-use extendr_api::graphics::color::predefined::{antiquewhite, black, darkkhaki, deepskyblue};
-use extendr_api::graphics::color::Color;
-use extendr_api::graphics::{
-    Context, DevDesc, Device, DeviceDescriptor, DeviceDriver, FontFace, R_GE_gcontext, Raster,
-    TextMetric, Unit,
-};
-use extendr_api::prelude::*;
-
-#[test]
-fn graphics_test() {
-    // Set this to false to render with your local graphics device.
-    let use_postscript = true;
-    let dir = std::env::temp_dir();
-    let path = dir.join("test.ps");
-    let path_str = path.to_string_lossy().to_string();
-    test! {
-        if use_postscript {
-            R!("postscript({{path_str}})")?;
-        }
-        let dev = Device::current()?;
-        let mut gc = Context::from_device(&dev, Unit::Inches);
-
-        // Start a new page.
-        // Use CSS "antiquewhite" https://www.w3.org/TR/2018/REC-css-color-3-20180619/
-        gc.fill(antiquewhite());
-        dev.new_page(&gc);
-
-        dev.mode_on()?;
-
-        // Graphics commands.
-        gc.color(darkkhaki());
-        gc.line_width(0.1);
-
-        // Draw a line.
-        dev.line((2.0, 2.0), (3.0, 3.0), &gc);
-
-        // Draw a circle using `polygon()`.
-        let scale = std::f64::consts::PI*2.0/10.0;
-        gc.fill(deepskyblue());
-        dev.polygon(
-            (0..10).map(|i| (
-                ((i as f64) * scale).cos() + 4.0,
-                ((i as f64) * scale).sin() + 2.0
-            )), &gc);
-
-        // Draw a circle using `circle()`.
-        gc.fill(Color::rgb(0x20, 0x20, 0xc0));
-        dev.circle((1.0, 1.0), 0.5, &gc);
-
-        gc.color(black());
-        gc.point_size(36.0);
-        gc.font_face(FontFace::Plain);
-        gc.font_family("Helvetica");
-
-        // Draw Hello -- World with the two dashes almost touching.
-        let w = dev.text_width("Hello -", &gc);
-        dev.text((1.0, 3.0), "Hello -", (0.0, 0.0), 0.0, &gc);
-        dev.text((1.0 + w, 3.0), "- World", (0.0, 0.0), 0.0, &gc);
-
-        gc.line_width(0.01);
-        for i in 0..10 {
-            dev.symbol((1.0 + i as f64 * 0.3, 4.0), i, 0.25, &gc);
-        }
-
-        println!("{:?}", dev.char_metric('a', &gc));
-        println!("{:?}", dev.char_metric('g', &gc));
-        println!("{:?}", dev.char_metric('ê', &gc));
-
-        println!("{:?}", dev.text_metric("a", &gc));
-        println!("{:?}", dev.text_metric("g", &gc));
-        println!("{:?}", dev.text_metric("ê", &gc));
-
-        dev.mode_off()?;
+#[cfg(feature = "graphics")]
+mod tests {
+    use std::fmt::Write;
+
+    use extendr_api::graphics::color::predefined::{antiquewhite, black, darkkhaki, deepskyblue};
+    use extendr_api::graphics::color::Color;
+    use extendr_api::graphics::{
+        Context, DevDesc, Device, DeviceDescriptor, DeviceDriver, FontFace, R_GE_gcontext, Raster,
+        TextMetric, Unit,
+    };
+    use extendr_api::prelude::*;
+
+    #[test]
+    fn graphics_test() {
+        // Set this to false to render with your local graphics device.
+        let use_postscript = true;
+        let dir = std::env::temp_dir();
+        let path = dir.join("test.ps");
+        let path_str = path.to_string_lossy().to_string();
+        test! {
+            if use_postscript {
+                R!("postscript({{path_str}})")?;
+            }
+            let dev = Device::current()?;
+            let mut gc = Context::from_device(&dev, Unit::Inches);
+
+            // Start a new page.
+            // Use CSS "antiquewhite" https://www.w3.org/TR/2018/REC-css-color-3-20180619/
+            gc.fill(antiquewhite());
+            dev.new_page(&gc);
+
+            dev.mode_on()?;
+
+            // Graphics commands.
+            gc.color(darkkhaki());
+            gc.line_width(0.1);
+
+            // Draw a line.
+            dev.line((2.0, 2.0), (3.0, 3.0), &gc);
+
+            // Draw a circle using `polygon()`.
+            let scale = std::f64::consts::PI*2.0/10.0;
+            gc.fill(deepskyblue());
+            dev.polygon(
+                (0..10).map(|i| (
+                    ((i as f64) * scale).cos() + 4.0,
+                    ((i as f64) * scale).sin() + 2.0
+                )), &gc);
+
+            // Draw a circle using `circle()`.
+            gc.fill(Color::rgb(0x20, 0x20, 0xc0));
+            dev.circle((1.0, 1.0), 0.5, &gc);
+
+            gc.color(black());
+            gc.point_size(36.0);
+            gc.font_face(FontFace::Plain);
+            gc.font_family("Helvetica");
+
+            // Draw Hello -- World with the two dashes almost touching.
+            let w = dev.text_width("Hello -", &gc);
+            dev.text((1.0, 3.0), "Hello -", (0.0, 0.0), 0.0, &gc);
+            dev.text((1.0 + w, 3.0), "- World", (0.0, 0.0), 0.0, &gc);
+
+            gc.line_width(0.01);
+            for i in 0..10 {
+                dev.symbol((1.0 + i as f64 * 0.3, 4.0), i, 0.25, &gc);
+            }
 
-        if use_postscript {
-            // Flush the file and kill the device.
-            R!("dev.off()")?;
-            let ps = std::fs::read_to_string(path).expect("PS file not written.");
-            if let Some(split) = ps.split_once("%%EndProlog") {
-                let epilogue = split.1;
-                println!("epilogue:\n{}", epilogue);
-
-                // Graphics commands.
-                // Note windows version uses \r\n, hence the newline at the start.
-                // assert!(epilogue.contains("\no"));
-                // assert!(epilogue.contains("\ncp p3"));
-                // assert!(epilogue.contains("\nc p3"));
+            println!("{:?}", dev.char_metric('a', &gc));
+            println!("{:?}", dev.char_metric('g', &gc));
+            println!("{:?}", dev.char_metric('ê', &gc));
+
+            println!("{:?}", dev.text_metric("a", &gc));
+            println!("{:?}", dev.text_metric("g", &gc));
+            println!("{:?}", dev.text_metric("ê", &gc));
+
+            dev.mode_off()?;
+
+            if use_postscript {
+                // Flush the file and kill the device.
+                R!("dev.off()")?;
+                let ps = std::fs::read_to_string(path).expect("PS file not written.");
+                if let Some(split) = ps.split_once("%%EndProlog") {
+                    let epilogue = split.1;
+                    println!("epilogue:\n{}", epilogue);
+
+                    // Graphics commands.
+                    // Note windows version uses \r\n, hence the newline at the start.
+                    // assert!(epilogue.contains("\no"));
+                    // assert!(epilogue.contains("\ncp p3"));
+                    // assert!(epilogue.contains("\nc p3"));
+                } else {
+                    println!("ps:\n{}", ps);
+                    assert!(("epilogue not found", false).1);
+                }
             } else {
-                println!("ps:\n{}", ps);
-                assert!(("epilogue not found", false).1);
+                std::thread::sleep(std::time::Duration::from_millis(2000));
+                assert!(false); // did we check in the flag as "false?"
             }
-        } else {
-            std::thread::sleep(std::time::Duration::from_millis(2000));
-            assert!(false); // did we check in the flag as "false?"
         }
     }
-}
-
-// Taking the mutable references so that we can peek at the values from
-// outside, which is (only?) useful for testing.
-struct TestDevice<'a> {
-    last_mode: &'a mut i32,
-    value: &'a mut f64,
-    canvas: &'a mut String,
-    closed: &'a mut bool,
-}
 
-impl<'a> DeviceDriver for TestDevice<'a> {
-    fn activate(&mut self, _: DevDesc) {
-        *self.value = 100.0;
+    // Taking the mutable references so that we can peek at the values from
+    // outside, which is (only?) useful for testing.
+    struct TestDevice<'a> {
+        last_mode: &'a mut i32,
+        value: &'a mut f64,
+        canvas: &'a mut String,
+        closed: &'a mut bool,
     }
 
-    fn mode(&mut self, mode: i32, _: DevDesc) {
-        *self.last_mode = mode;
-        *self.value += 1.0;
-    }
+    impl<'a> DeviceDriver for TestDevice<'a> {
+        fn activate(&mut self, _: DevDesc) {
+            *self.value = 100.0;
+        }
 
-    fn close(&mut self, _: DevDesc) {
-        *self.closed = true;
-    }
+        fn mode(&mut self, mode: i32, _: DevDesc) {
+            *self.last_mode = mode;
+            *self.value += 1.0;
+        }
 
-    fn new_page(&mut self, _: R_GE_gcontext, _: DevDesc) {
-        self.canvas.clear();
-    }
+        fn close(&mut self, _: DevDesc) {
+            *self.closed = true;
+        }
 
-    fn clip(&mut self, from: (f64, f64), to: (f64, f64), _dd: DevDesc) {
-        let (f_x, f_y) = from;
-        let (t_x, t_y) = to;
-        writeln!(
-            *self.canvas,
-            "clip from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
-        )
-        .unwrap();
-    }
+        fn new_page(&mut self, _: R_GE_gcontext, _: DevDesc) {
+            self.canvas.clear();
+        }
 
-    fn circle(&mut self, center: (f64, f64), r: f64, _: R_GE_gcontext, _: DevDesc) {
-        let (x, y) = center;
-        writeln!(*self.canvas, "circle center=({x:.1}, {y:.1}) r={r:.1}").unwrap();
-    }
+        fn clip(&mut self, from: (f64, f64), to: (f64, f64), _dd: DevDesc) {
+            let (f_x, f_y) = from;
+            let (t_x, t_y) = to;
+            writeln!(
+                *self.canvas,
+                "clip from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
+            )
+            .unwrap();
+        }
 
-    fn line(&mut self, from: (f64, f64), to: (f64, f64), _: R_GE_gcontext, _: DevDesc) {
-        let (f_x, f_y) = from;
-        let (t_x, t_y) = to;
-        writeln!(
-            *self.canvas,
-            "line from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
-        )
-        .unwrap();
-    }
+        fn circle(&mut self, center: (f64, f64), r: f64, _: R_GE_gcontext, _: DevDesc) {
+            let (x, y) = center;
+            writeln!(*self.canvas, "circle center=({x:.1}, {y:.1}) r={r:.1}").unwrap();
+        }
 
-    fn rect(&mut self, from: (f64, f64), to: (f64, f64), _: R_GE_gcontext, _: DevDesc) {
-        let (f_x, f_y) = from;
-        let (t_x, t_y) = to;
-        writeln!(
-            *self.canvas,
-            "rect from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
-        )
-        .unwrap();
-    }
+        fn line(&mut self, from: (f64, f64), to: (f64, f64), _: R_GE_gcontext, _: DevDesc) {
+            let (f_x, f_y) = from;
+            let (t_x, t_y) = to;
+            writeln!(
+                *self.canvas,
+                "line from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
+            )
+            .unwrap();
+        }
 
-    fn polyline<T: IntoIterator<Item = (f64, f64)>>(
-        &mut self,
-        coords: T,
-        _: R_GE_gcontext,
-        _: DevDesc,
-    ) {
-        let coords = coords
-            .into_iter()
-            .map(|(x, y)| format!("({x:.1}, {y:.1})"))
-            .collect::<Vec<String>>()
-            .join(" ");
-        writeln!(*self.canvas, "polyline coords=[{coords}]").unwrap();
-    }
+        fn rect(&mut self, from: (f64, f64), to: (f64, f64), _: R_GE_gcontext, _: DevDesc) {
+            let (f_x, f_y) = from;
+            let (t_x, t_y) = to;
+            writeln!(
+                *self.canvas,
+                "rect from=({f_x:.1}, {f_y:.1}) to=({t_x:.1}, {t_y:.1})"
+            )
+            .unwrap();
+        }
 
-    fn polygon<T: IntoIterator<Item = (f64, f64)>>(
-        &mut self,
-        coords: T,
-        _: R_GE_gcontext,
-        _: DevDesc,
-    ) {
-        let coords = coords
-            .into_iter()
-            .map(|(x, y)| format!("({x:.1}, {y:.1})"))
-            .collect::<Vec<String>>()
-            .join(" ");
-        writeln!(*self.canvas, "polygon coords=[{coords}]").unwrap();
-    }
+        fn polyline<T: IntoIterator<Item = (f64, f64)>>(
+            &mut self,
+            coords: T,
+            _: R_GE_gcontext,
+            _: DevDesc,
+        ) {
+            let coords = coords
+                .into_iter()
+                .map(|(x, y)| format!("({x:.1}, {y:.1})"))
+                .collect::<Vec<String>>()
+                .join(" ");
+            writeln!(*self.canvas, "polyline coords=[{coords}]").unwrap();
+        }
 
-    fn path<T: IntoIterator<Item = impl IntoIterator<Item = (f64, f64)>>>(
-        &mut self,
-        coords: T,
-        winding: bool,
-        _: R_GE_gcontext,
-        _: DevDesc,
-    ) {
-        let coords = coords
-            .into_iter()
-            .map(|i| {
-                let xy = i
-                    .into_iter()
-                    .map(|(x, y)| format!("({x:.1}, {y:.1})"))
-                    .collect::<Vec<String>>()
-                    .join(" ");
-                format!("({xy})")
-            })
-            .collect::<Vec<String>>()
-            .join(", ");
-
-        writeln!(*self.canvas, "path coords=[{coords}] winding={winding}").unwrap();
-    }
+        fn polygon<T: IntoIterator<Item = (f64, f64)>>(
+            &mut self,
+            coords: T,
+            _: R_GE_gcontext,
+            _: DevDesc,
+        ) {
+            let coords = coords
+                .into_iter()
+                .map(|(x, y)| format!("({x:.1}, {y:.1})"))
+                .collect::<Vec<String>>()
+                .join(" ");
+            writeln!(*self.canvas, "polygon coords=[{coords}]").unwrap();
+        }
 
-    fn text(
-        &mut self,
-        pos: (f64, f64),
-        str: &str,
-        rot: f64,
-        hadj: f64,
-        _: R_GE_gcontext,
-        _: DevDesc,
-    ) {
-        let (x, y) = pos;
-        writeln!(
-            *self.canvas,
-            "text pos=({x:.1}, {y:.1}) str='{str}' rot={rot:.1} hadj={hadj:.1}"
-        )
-        .unwrap();
-    }
+        fn path<T: IntoIterator<Item = impl IntoIterator<Item = (f64, f64)>>>(
+            &mut self,
+            coords: T,
+            winding: bool,
+            _: R_GE_gcontext,
+            _: DevDesc,
+        ) {
+            let coords = coords
+                .into_iter()
+                .map(|i| {
+                    let xy = i
+                        .into_iter()
+                        .map(|(x, y)| format!("({x:.1}, {y:.1})"))
+                        .collect::<Vec<String>>()
+                        .join(" ");
+                    format!("({xy})")
+                })
+                .collect::<Vec<String>>()
+                .join(", ");
+
+            writeln!(*self.canvas, "path coords=[{coords}] winding={winding}").unwrap();
+        }
 
-    // returns the char code in width so that we can check it
-    fn char_metric(&mut self, c: char, _: R_GE_gcontext, _: DevDesc) -> TextMetric {
-        TextMetric {
-            ascent: 0.0,
-            descent: 0.0,
-            width: c as i32 as _,
+        fn text(
+            &mut self,
+            pos: (f64, f64),
+            str: &str,
+            rot: f64,
+            hadj: f64,
+            _: R_GE_gcontext,
+            _: DevDesc,
+        ) {
+            let (x, y) = pos;
+            writeln!(
+                *self.canvas,
+                "text pos=({x:.1}, {y:.1}) str='{str}' rot={rot:.1} hadj={hadj:.1}"
+            )
+            .unwrap();
         }
-    }
 
-    fn raster<T: AsRef<[u32]>>(
-        &mut self,
-        raster: Raster<T>,
-        pos: (f64, f64),
-        size: (f64, f64),
-        rot: f64,
-        interpolate: bool,
-        _: R_GE_gcontext,
-        _: DevDesc,
-    ) {
-        let (x, y) = pos;
-        let (width, height) = size;
-        let Raster { pixels, width: w } = raster;
-        let pixels_str = pixels
-            .as_ref()
-            .iter()
-            .map(|&p| format!("{p}"))
-            .collect::<Vec<String>>()
-            .join("|");
-
-        writeln!(
+        // returns the char code in width so that we can check it
+        fn char_metric(&mut self, c: char, _: R_GE_gcontext, _: DevDesc) -> TextMetric {
+            TextMetric {
+                ascent: 0.0,
+                descent: 0.0,
+                width: c as i32 as _,
+            }
+        }
+
+        fn raster<T: AsRef<[u32]>>(
+            &mut self,
+            raster: Raster<T>,
+            pos: (f64, f64),
+            size: (f64, f64),
+            rot: f64,
+            interpolate: bool,
+            _: R_GE_gcontext,
+            _: DevDesc,
+        ) {
+            let (x, y) = pos;
+            let (width, height) = size;
+            let Raster { pixels, width: w } = raster;
+            let pixels_str = pixels
+                .as_ref()
+                .iter()
+                .map(|&p| format!("{p}"))
+                .collect::<Vec<String>>()
+                .join("|");
+
+            writeln!(
                 *self.canvas,
                 "raster {pixels_str} w={w} pos=({x:.1}, {y:.1}) size=({width:.1}, {height:.1}) rot={rot:.1} interpolate={interpolate}"
             )
             .unwrap();
+        }
     }
-}
 
-#[test]
-fn device_driver_test() {
-    test! {
-        let mut value = 0.0;
-        let mut last_mode = 0;
-        let mut closed = false;
-        let mut canvas = String::new();
-
-        let device_driver = TestDevice {
-            last_mode: &mut last_mode,
-            value: &mut value,
-            canvas: &mut canvas,
-            closed: &mut closed,
-        };
-
-        let device_descriptor = DeviceDescriptor::new();
-        let device = device_driver.create_device::<TestDevice>(device_descriptor, "test device");
-
-        let gc = Context::from_device(&device, Unit::Device);
-
-        // if activate() is invoked, value should be 100.0
-        assert_eq!(value, 100.0);
-        assert!(!closed);
-
-        device.mode_on().unwrap();
-
-        // if mode() is invoked, value and last_mode should be updated
-        assert_eq!(last_mode, 1);
-        assert_eq!(value, 101.0);
-
-        device.mode_off().unwrap();
-
-        // if mode() is invoked, value and last_mode should be updated
-        assert_eq!(last_mode, 0);
-        assert_eq!(value, 102.0);
-
-        // ASCII char
-        let c1 = 'c';
-        assert_eq!(device.char_metric(c1, &gc).width, c1 as i32 as f64);
-        // non-ASCII char
-        let c2 = 'ú';
-        let c3 = '鬼';
-        assert_eq!(device.char_metric(c2, &gc).width, c2 as i32 as f64);
-        assert_eq!(device.char_metric(c3, &gc).width, c3 as i32 as f64);
-        // text
-        let t1 = "ab";
-        assert_eq!(device.text_width(t1, &gc), ('a' as i32 + 'b' as i32) as f64);
-
-        device.clip((1.1, 2.2), (3.3, 4.4), &gc);
-        device.circle((1.1, 2.2), 3.3, &gc);
-        device.line((1.1, 2.2), (3.3, 4.4), &gc);
-        device.rect((1.1, 2.2), (3.3, 4.4), &gc);
-
-        // // TODO: uncomment the following lines when https://github.com/extendr/extendr/issues/370 is solved.
-        // device.polyline([(0.0, 0.0), (0.0, 2.0)], &gc);
-        // device.polygon([(0.0, 0.0), (1.0, 2.0), (2.0, 0.0)], &gc);
-        // device.path([[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.3, 0.0), (0.3, 0.3), (0.7, 0.3), (0.3, 0.7)]], true, &gc);
-
-        // x element of `center` is `hadj`, a horizontal adjustment
-        // I'm yet to figure out how the `y` element is used. Let's leave it as 0 for now.
-        device.text((1.1, 2.2), "foo", (0.5, 0.0), 5.5, &gc);
-
-        let r = Raster {
-            pixels: &[1, 2, 3, 4, 5, 6],
-            width: 3,
-        };
-        device.raster(r, (1.1, 2.2), (3.3, 4.4), 5.5, false, &gc);
-
-        assert_eq!(canvas, "clip from=(1.1, 2.2) to=(3.3, 4.4)\n\
-                                circle center=(1.1, 2.2) r=3.3\n\
-                                line from=(1.1, 2.2) to=(3.3, 4.4)\n\
-                                rect from=(1.1, 2.2) to=(3.3, 4.4)\n\
-                                text pos=(1.1, 2.2) str='foo' rot=5.5 hadj=0.5\n\
-                                raster 1|2|3|4|5|6 w=3 pos=(1.1, 2.2) size=(3.3, 4.4) rot=5.5 interpolate=false\n\
-                                ");
-
-        // Clearing canvas.
-        device.new_page(&gc);
-        assert_eq!(canvas, "");
-
-        // check if the R doesn't crash on closing the device.
-        R!("dev.off()")?;
-
-        assert!(closed);
+    #[test]
+    fn device_driver_test() {
+        test! {
+            let mut value = 0.0;
+            let mut last_mode = 0;
+            let mut closed = false;
+            let mut canvas = String::new();
+
+            let device_driver = TestDevice {
+                last_mode: &mut last_mode,
+                value: &mut value,
+                canvas: &mut canvas,
+                closed: &mut closed,
+            };
+
+            let device_descriptor = DeviceDescriptor::new();
+            let device = device_driver.create_device::<TestDevice>(device_descriptor, "test device");
+
+            let gc = Context::from_device(&device, Unit::Device);
+
+            // if activate() is invoked, value should be 100.0
+            assert_eq!(value, 100.0);
+            assert!(!closed);
+
+            device.mode_on().unwrap();
+
+            // if mode() is invoked, value and last_mode should be updated
+            assert_eq!(last_mode, 1);
+            assert_eq!(value, 101.0);
+
+            device.mode_off().unwrap();
+
+            // if mode() is invoked, value and last_mode should be updated
+            assert_eq!(last_mode, 0);
+            assert_eq!(value, 102.0);
+
+            // ASCII char
+            let c1 = 'c';
+            assert_eq!(device.char_metric(c1, &gc).width, c1 as i32 as f64);
+            // non-ASCII char
+            let c2 = 'ú';
+            let c3 = '鬼';
+            assert_eq!(device.char_metric(c2, &gc).width, c2 as i32 as f64);
+            assert_eq!(device.char_metric(c3, &gc).width, c3 as i32 as f64);
+            // text
+            let t1 = "ab";
+            assert_eq!(device.text_width(t1, &gc), ('a' as i32 + 'b' as i32) as f64);
+
+            device.clip((1.1, 2.2), (3.3, 4.4), &gc);
+            device.circle((1.1, 2.2), 3.3, &gc);
+            device.line((1.1, 2.2), (3.3, 4.4), &gc);
+            device.rect((1.1, 2.2), (3.3, 4.4), &gc);
+
+            // // TODO: uncomment the following lines when https://github.com/extendr/extendr/issues/370 is solved.
+            // device.polyline([(0.0, 0.0), (0.0, 2.0)], &gc);
+            // device.polygon([(0.0, 0.0), (1.0, 2.0), (2.0, 0.0)], &gc);
+            // device.path([[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.3, 0.0), (0.3, 0.3), (0.7, 0.3), (0.3, 0.7)]], true, &gc);
+
+            // x element of `center` is `hadj`, a horizontal adjustment
+            // I'm yet to figure out how the `y` element is used. Let's leave it as 0 for now.
+            device.text((1.1, 2.2), "foo", (0.5, 0.0), 5.5, &gc);
+
+            let r = Raster {
+                pixels: &[1, 2, 3, 4, 5, 6],
+                width: 3,
+            };
+            device.raster(r, (1.1, 2.2), (3.3, 4.4), 5.5, false, &gc);
+
+            assert_eq!(canvas, "clip from=(1.1, 2.2) to=(3.3, 4.4)\n\
+                                    circle center=(1.1, 2.2) r=3.3\n\
+                                    line from=(1.1, 2.2) to=(3.3, 4.4)\n\
+                                    rect from=(1.1, 2.2) to=(3.3, 4.4)\n\
+                                    text pos=(1.1, 2.2) str='foo' rot=5.5 hadj=0.5\n\
+                                    raster 1|2|3|4|5|6 w=3 pos=(1.1, 2.2) size=(3.3, 4.4) rot=5.5 interpolate=false\n\
+                                    ");
+
+            // Clearing canvas.
+            device.new_page(&gc);
+            assert_eq!(canvas, "");
+
+            // check if the R doesn't crash on closing the device.
+            R!("dev.off()")?;
+
+            assert!(closed);
+        }
     }
 }
diff --git a/tests/extendrtests/src/rust/Cargo.toml b/tests/extendrtests/src/rust/Cargo.toml
index fa59a5447..39f4c85e7 100644
--- a/tests/extendrtests/src/rust/Cargo.toml
+++ b/tests/extendrtests/src/rust/Cargo.toml
@@ -13,13 +13,13 @@ edition = "2018"
 crate-type = ["staticlib"]
 
 [dependencies]
-extendr-api = "*"
+extendr-api = { version = "*", features = ["graphics"] }
 
 [patch.crates-io]
 ## This is configured to work with RStudio features.
 ## Replace by absolute path to simplify testing.
 ## CI overrides this path.
-extendr-api = { path = "../../../../../extendr/extendr-api" }
+extendr-api = { path = "../../../../../../../extendr/extendr-api" }
 ## This allows to run `rcmdcheck` from `./tests/extendrtests/`
 # extendr-api = { path = "../../../../../../../../../extendr/extendr-api"}