diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7ad3811 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + CARGO_TERM_COLOR: always + +jobs: + default-build: + name: Test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + # os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v5 + with: + lfs: true + - run: rustup component add rustfmt clippy + + # dependencies for wxWidgets (see installation instructions and github + # workflow of https://github.com/allendang/wxdragon) + - name: Install deps + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libpng-dev libjpeg-dev libgl1-mesa-dev libglu1-mesa-dev libxkbcommon-dev libexpat1-dev libtiff-dev libwebkit2gtk-4.1-dev libxtst-dev + + - name: Cargo cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: build + run: cargo build + + - name: clippy + run: cargo clippy --all-features -- -D warnings + + - name: check formatting + run: cargo fmt --all -- --check + + - name: headless tests + if: runner.os == 'Linux' + uses: coactions/setup-xvfb@v1 + with: + # skip main doc test because it opens a window + # run: cargo test --all-features --verbose + run: cargo test --all-features --verbose --lib --tests + + - name: headless tests + if: runner.os == 'Windows' + # skip main doc test because it opens a window + # run: cargo test --all-features --verbose + run: cargo test --all-features --verbose --lib --tests + + - name: upload test outputs + uses: actions/upload-artifact@v4 + if: always() + with: + name: tests-outputs + path: | + tests/outputs/ + retention-days: 7 + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + # dependencies for wxWidgets (see installation instructions and github + # workflow of https://github.com/allendang/wxdragon) + - name: Install deps + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libpng-dev libjpeg-dev libgl1-mesa-dev libglu1-mesa-dev libxkbcommon-dev libexpat1-dev libtiff-dev libwebkit2gtk-4.1-dev libxtst-dev + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build documentation + run: cargo doc --all-features --no-deps diff --git a/.gitignore b/.gitignore index 3072d78..423bc6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .aider* target +/tests/outputs diff --git a/src/lib.rs b/src/lib.rs index ef18c16..cac052f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,22 +98,20 @@ //! } //! } //! -//! fn main() { -//! let _ = wxdragon::main(|_| { -//! let frame = wx::Frame::builder() -//! .with_title("Getting started") -//! // with this, wx produces a canvas of size 800 x 600 -//! .with_size(wx::Size::new(852, 689)) -//! .build(); +//! let _ = wxdragon::main(|_| { +//! let frame = wx::Frame::builder() +//! .with_title("Getting started") +//! // with this, wx produces a canvas of size 800 x 600 +//! .with_size(wx::Size::new(852, 689)) +//! .build(); //! -//! let drawing_panel = DrawingPanel::new(&frame); +//! let drawing_panel = DrawingPanel::new(&frame); //! -//! // Initial paint -//! drawing_panel.refresh(true, None); +//! // Initial paint +//! drawing_panel.refresh(true, None); //! -//! frame.show(true); -//! }); -//! } +//! frame.show(true); +//! }); //! ``` //! //! You can find more details in the examples for how to integrate a plot in diff --git a/tests/3d_plot.rs b/tests/3d_plot.rs index 15c7547..1e7aa35 100644 --- a/tests/3d_plot.rs +++ b/tests/3d_plot.rs @@ -10,7 +10,7 @@ use test_utils::run_plotters_image_test; #[test] fn test_3d_plot() -> anyhow::Result<()> { - run_plotters_image_test(1024, 768, "tests/3d_plot", draw_3d_plot) + run_plotters_image_test(1024, 768, "tests/3d_plot.png", draw_3d_plot) } fn draw_3d_plot(backend: WxBackend) -> anyhow::Result<()> { diff --git a/tests/chart.rs b/tests/chart.rs index d14e61c..d36eb45 100644 --- a/tests/chart.rs +++ b/tests/chart.rs @@ -12,7 +12,7 @@ use test_utils::run_plotters_image_test; #[test] fn test_chart() -> anyhow::Result<()> { - run_plotters_image_test(1024, 768, "tests/chart", draw_chart) + run_plotters_image_test(1024, 768, "tests/chart.png", draw_chart) } fn draw_chart(backend: WxBackend) -> anyhow::Result<()> { diff --git a/tests/full_palette.rs b/tests/full_palette.rs index c1d04b7..4b8a4a9 100644 --- a/tests/full_palette.rs +++ b/tests/full_palette.rs @@ -12,7 +12,12 @@ use test_utils::run_plotters_image_test; #[test] fn test_full_palette() -> anyhow::Result<()> { - run_plotters_image_test(2000, 850, "tests/full_palette", draw_full_palette) + run_plotters_image_test( + 2000, + 850, + "tests/full_palette.png", + draw_full_palette, + ) } fn draw_full_palette( backend: WxBackend, diff --git a/tests/mandelbrot.rs b/tests/mandelbrot.rs index 9314494..30febae 100644 --- a/tests/mandelbrot.rs +++ b/tests/mandelbrot.rs @@ -15,7 +15,7 @@ use test_utils::run_plotters_image_test; #[test] fn test_mandelbrot() -> Result<()> { - run_plotters_image_test(800, 600, "tests/mandelbrot", draw_mandelbrot) + run_plotters_image_test(800, 600, "tests/mandelbrot.png", draw_mandelbrot) } fn draw_mandelbrot(backend: WxBackend) -> Result<()> { diff --git a/tests/test_utils.rs b/tests/test_utils.rs index 5959089..821620a 100644 --- a/tests/test_utils.rs +++ b/tests/test_utils.rs @@ -2,6 +2,8 @@ use std::fs; use std::io; +use std::path::Path; +use std::path::PathBuf; use std::process; use anyhow::{Context, Result}; @@ -14,15 +16,18 @@ use wxdragon::{self as wx}; /// /// This function sets up a wxWidgets `MemoryDC`, draws on it using the /// provided `draw_fn`, then compares the resulting bitmap with a reference -/// image loaded from `{path_root}.png`. If the images do not match, the test +/// image loaded from `{reference_png}`. If the images do not match, the test /// will fail. /// +/// In case of image mismatch, the actual producted image i saved in +/// subdirectory `/outputs/` of the directory containing `{reference_png}`. +/// /// # Arguments /// /// * `width`: width of the drawing area. /// * `height`: height of the drawing area. -/// * `path_root`: used to build the path `"{path_root}.png"` to the reference -/// PNG image for non-regression comparison. +/// * `reference_png`: path to the reference PNG image for non-regression +/// comparison. /// * `draw_fn`: closure that performs the drawing operations. /// /// # Returns @@ -33,14 +38,26 @@ use wxdragon::{self as wx}; pub fn run_plotters_image_test( width: u32, height: u32, - path_root: &str, + reference_png: impl Into, draw_fn: F, ) -> Result<()> where F: FnOnce(WxBackend) -> Result<()> + Send + 'static, { - let reference_png = format!("{path_root}.png"); - let actual_png = format!("{path_root}_actual.png"); // saved if mismatch + let reference_png = reference_png.into(); + let (output_dir, actual_png) = { + let output_dir = reference_png + .parent() + .unwrap_or_else(|| Path::new("")) + .join("outputs"); + let actual_png = output_dir.join( + reference_png + .file_name() + .context("Invalid reference_png path: missing filename")?, + ); + (output_dir, actual_png) + }; + let _ = wx::main(move |_| { let result = (|| -> Result<()> { // setup the backend with an empty bitmap @@ -69,25 +86,36 @@ where let expected = image::load( io::BufReader::new( fs::File::open(&reference_png).with_context(|| { - format!("failed to open {reference_png}") + format!("failed to open {}", reference_png.display()) })?, ), image::ImageFormat::Png, ) - .with_context(|| "failed to load {reference_png}")?; + .with_context(|| { + format!("failed to load {}", reference_png.display()) + })?; + + // save actual image for later comparison in case of failure + fs::create_dir_all(&output_dir).context(format!( + "failed to create directory {}", + output_dir.display() + ))?; + image + .save(&actual_png) + .context(format!("failed to save {}", actual_png.display()))?; + if expected == image::DynamicImage::ImageRgba8(image.clone()) { Ok(()) } else { - image - .save(&actual_png) - .context("failed to save {actual_png}")?; let message = format!( "ERROR: image mismatch. Compare the following two files manually, then \ update the reference image if needed. - reference image: {reference_png} - actual image : {actual_png} -" + reference image: {} + actual image : {} +", + reference_png.display(), + actual_png.display() ); anyhow::bail!(message) }