diff --git a/.github/workflows/occt-ffi-unit-test.yml b/.github/workflows/occt-ffi-unit-test.yml new file mode 100644 index 0000000..0c1cc61 --- /dev/null +++ b/.github/workflows/occt-ffi-unit-test.yml @@ -0,0 +1,103 @@ +name: OCCT FFI Unit Tests + +on: + push: + branches: [master, dev-occt**, dev-ffi**] + paths: + - "src/occt/**" + - "src/ffi/**" + - "tests/occt/**" + - "tests/*.rs" + - "ci/occt.Dockerfile" + - "ci/scripts/occt.sh" + - "ci/ffi.Dockerfile" + - "ci/scripts/ffi.sh" + - ".github/**" + - "./CMakeLists.txt" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + pull_request: + branches: [master, dev-occt, dev-ffi] + paths: + - "src/occt/**" + - "src/ffi/**" + - "tests/occt/**" + - "tests/*.rs" + - "ci/occt.Dockerfile" + - "ci/scripts/occt.sh" + - "ci/ffi.Dockerfile" + - "ci/scripts/ffi.sh" + - ".github/**" + - "./CMakeLists.txt" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + +jobs: + build-occt: + name: OCCT + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Make scripts executable + run: chmod +x ./ci/scripts/occt.sh + + - name: Build OCCT and run tests in Docker + run: ./ci/scripts/occt.sh --unit-testing + + - name: Tag and push OCCT image + if: github.event_name != 'pull_request' + run: | + docker tag occt-build:latest ghcr.io/${{ github.repository }}/occt-build:latest + docker push ghcr.io/${{ github.repository }}/occt-build:latest + + build-ffi: + name: FFI + runs-on: ubuntu-24.04 + needs: build-occt + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull and tag OCCT image + run: | + docker pull ghcr.io/${{ github.repository }}/occt-build:latest + docker tag ghcr.io/${{ github.repository }}/occt-build:latest occt-build:latest + + - name: Make scripts executable + run: chmod +x ./ci/scripts/ffi.sh + + - name: Build and test FFI in Docker + run: ./ci/scripts/ffi.sh --tests + + - name: Tag and push FFI image + if: github.event_name != 'pull_request' + run: | + docker tag ffi-build:latest ghcr.io/${{ github.repository }}/ffi-build:latest + docker push ghcr.io/${{ github.repository }}/ffi-build:latest diff --git a/.github/workflows/occt-unit-test.yml b/.github/workflows/occt-unit-test.yml deleted file mode 100644 index 538300e..0000000 --- a/.github/workflows/occt-unit-test.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: OCCT CI -on: - push: - branches: [master, dev-occt] - paths: - - "src/occt/**" - - "src/ffi/**" - - "test/occt/**" - - "test/ffi/**" - - "ci/**" - - ".github/**" - - "./CMakeLists.txt" - pull_request: - branches: [master, dev-occt] - paths: - - "src/occt/**" - - "src/ffi/**" - - "test/occt/**" - - "test/ffi/**" - - "ci/**" - - ".github/**" - - "./CMakeLists.txt" - -jobs: - build-wrappers: - name: Build OCCT wrappers - runs-on: ubuntu-24.04 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Make script executable - run: chmod +x ./ci/scripts/occt.sh - - - name: Build OCCT and run tests - run: ./ci/scripts/occt.sh --unit-testing diff --git a/Cargo.toml b/Cargo.toml index a885705..2899a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,14 @@ [package] name = "arc-core" -version = "0.0.1" +version = "0.0.2" edition = "2024" license = "AGPL-3.0-or-later" +[lib] +name = "arc_core" +path = "src/lib.rs" + [dependencies] + +[build-dependencies] +cc = "1.0" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..749625c --- /dev/null +++ b/build.rs @@ -0,0 +1,13 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +fn main() { + println!("cargo:rustc-link-search=/usr/local/lib"); + println!("cargo:rustc-link-search=/app/build"); + + println!("cargo:rustc-link-lib=occt"); + + println!("cargo:rerun-if-changed=src/wrappers/"); +} diff --git a/ci/ffi.Dockerfile b/ci/ffi.Dockerfile new file mode 100644 index 0000000..9ca14db --- /dev/null +++ b/ci/ffi.Dockerfile @@ -0,0 +1,41 @@ +FROM occt-build:latest AS occt-local +FROM ghcr.io/erickahmed/arc-core/occt-build:latest AS occt-ghcr +FROM occt-local AS occt-selected +FROM occt-selected AS ffi-base + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y curl build-essential + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +WORKDIR /app +COPY . . + +ENV LD_LIBRARY_PATH=/usr/local/lib:/app/build +ENV RUSTFLAGS="-L /usr/local/lib -L /app/build" + +RUN cargo build + +FROM ubuntu:24.04 AS ffi-unit-testing + +RUN apt-get update && apt-get install -y \ + build-essential cmake git curl \ + libfreetype-dev \ + xvfb \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +COPY --from=occt-selected /usr/local /usr/local +COPY --from=occt-selected /app/build /app/occt-build +COPY --from=ffi-base /app /app + +WORKDIR /app + +ENV LD_LIBRARY_PATH=/usr/local/lib:/app/occt-build +ENV RUSTFLAGS="-L /usr/local/lib -L /app/occt-build" + +CMD ["cargo", "test", "--verbose", "--", "--nocapture"] diff --git a/ci/occt.Dockerfile b/ci/occt.Dockerfile index 40dff64..eca18d0 100644 --- a/ci/occt.Dockerfile +++ b/ci/occt.Dockerfile @@ -3,7 +3,7 @@ # Source: https://github.com/Open-Cascade-SAS/OCCT/tree/c5f20409c52bf8f658314d205a0e5d6f0be0969c # Base builder -FROM ubuntu:24.04 AS base +FROM ubuntu:24.04 AS occt-base ENV DEBIAN_FRONTEND=noninteractive @@ -88,10 +88,9 @@ COPY src/occt/ ./src RUN cmake -S src -B build -DCMAKE_PREFIX_PATH=/usr/local RUN cmake --build build -COPY test/occt/ ./test +COPY tests/occt/ ./test -# Unit test runner stage -FROM ubuntu:24.04 AS unit-testing +FROM ubuntu:24.04 AS occt-unit-testing ENV DEBIAN_FRONTEND=noninteractive @@ -102,12 +101,10 @@ RUN apt-get update && apt-get install -y \ xvfb \ && rm -rf /var/lib/apt/lists/* -COPY --from=base /usr/local /usr/local -COPY --from=base /app/build /app/build -COPY --from=base /app/src /app/src -COPY --from=base /app/test /app/test -COPY --from=base /usr/local/include /usr/local/include -COPY --from=base /usr/local/lib /usr/local/lib +COPY --from=occt-base /usr/local /usr/local +COPY --from=occt-base /app/build /app/build +COPY --from=occt-base /app/src /app/src +COPY --from=occt-base /app/test /app/test WORKDIR /app diff --git a/ci/scripts/ffi.sh b/ci/scripts/ffi.sh new file mode 100644 index 0000000..819db98 --- /dev/null +++ b/ci/scripts/ffi.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME="ffi-build:latest" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_PATH="$(cd "$SCRIPT_DIR/../../" && pwd)" +DOCKERFILE_PATH="$SCRIPT_DIR/../ffi.Dockerfile" + +RUN_UNIT_TESTS=false +if [[ "${1-}" == "--tests" ]]; then + RUN_UNIT_TESTS=true +fi + +# Try to understand where tf are we +if [[ -n "${GITHUB_REPOSITORY-}" ]]; then + BASE_IMAGE="ghcr.io/$GITHUB_REPOSITORY/occt-build:latest" + echo "Using GHCR image: $BASE_IMAGE" + docker pull "$BASE_IMAGE" || true +else + BASE_IMAGE="occt-build:latest" + echo "Using local image: $BASE_IMAGE" +fi + +echo "Building base image..." +docker build --target ffi-base -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" \ + --build-arg BASE_IMAGE="$BASE_IMAGE" \ + "$PROJECT_PATH" + +if [ "$RUN_UNIT_TESTS" = true ]; then + echo "Building unit-testing stage..." + docker build --target ffi-unit-testing -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" \ + --build-arg BASE_IMAGE="$BASE_IMAGE" \ + "$PROJECT_PATH" + + echo "Running tests..." + docker run --rm "$IMAGE_NAME" cargo test --verbose +fi diff --git a/ci/scripts/occt.sh b/ci/scripts/occt.sh index 17d260f..2c105dd 100644 --- a/ci/scripts/occt.sh +++ b/ci/scripts/occt.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -IMAGE_NAME="occt-builder:latest" +IMAGE_NAME="occt-build:latest" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_PATH="$(cd "$SCRIPT_DIR/../../" && pwd)" DOCKERFILE_PATH="$SCRIPT_DIR/../occt.Dockerfile" @@ -12,11 +12,11 @@ if [[ "${1-}" == "--unit-testing" ]]; then fi echo "Building base image..." -docker build --target base -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" "$PROJECT_PATH" +docker build --target occt-base -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" "$PROJECT_PATH" if [ "$RUN_UNIT_TESTS" = true ]; then echo "Building unit-testing stage..." - docker build --target unit-testing -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" "$PROJECT_PATH" + docker build --target occt-unit-testing -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" "$PROJECT_PATH" echo "Running unit tests..." docker run --rm "$IMAGE_NAME" /app/build/unit-tests/tests diff --git a/src/ffi/point.rs b/src/ffi/point.rs new file mode 100644 index 0000000..94afd7b --- /dev/null +++ b/src/ffi/point.rs @@ -0,0 +1,151 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +mod ffi_point { + //FFI + + use std::os::raw::c_double; + + #[repr(C)] + pub(crate) struct PointShape { + _private: [u8; 0], + } + + unsafe extern "C" { + fn make_point(x: c_double, y: c_double, z: c_double) -> *mut PointShape; + fn coord_point( + shape: *const PointShape, + x: *mut c_double, + y: *mut c_double, + z: *mut c_double, + ); + fn delete_point(shape: *mut PointShape); + } + + // Safe wrapper + + pub struct Point { + ptr: *mut PointShape, + } + + impl Point { + pub fn new(x: f64, y: f64, z: f64) -> Result { + unsafe { + let ptr = make_point(x, y, z); + if ptr.is_null() { + Err("Error: null pointer returned") + } else { + Ok(Point { ptr }) + } + } + } + + pub fn coordinates(&self) -> (f64, f64, f64) { + unsafe { + let mut x = 0.0; + let mut y = 0.0; + let mut z = 0.0; + coord_point(self.ptr, &mut x, &mut y, &mut z); + (x, y, z) + } + } + + pub fn delete(mut self) -> Result<(), &'static str> { + unsafe { + if self.ptr.is_null() { + return Err("Error: Attempted to delete null pointer"); + } + delete_point(self.ptr); + + self.ptr = std::ptr::null_mut(); + std::mem::forget(self); + } + Ok(()) + } + } + + impl Drop for Point { + fn drop(&mut self) { + unsafe { + delete_point(self.ptr); + } + } + } + + // TODO: check if i can mark as thread-safe + + impl std::fmt::Debug for Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (x, y, z) = self.coordinates(); + write!(f, "Point({}, {}, {})", x, y, z) + } + } +} + +pub use ffi_point::Point; + +// Basic tests +#[cfg(test)] +mod tests { + use super::*; + use std::os::raw::c_double; + + #[test] + fn point_debug_precision() { + let p = Point::new(1.123456789, 2.987654321, -3.555555555).expect("Failed to create point"); + let debug_str = format!("{:?}", p); + + let coords = p.coordinates(); + assert!(debug_str.contains(&coords.0.to_string())); + assert!(debug_str.contains(&coords.1.to_string())); + assert!(debug_str.contains(&coords.2.to_string())); + } + + #[test] + fn point_null_pointer_safety() { + let result = Point::new(1.0, 2.0, 3.0); + assert!(result.is_ok()); + + let p = result.unwrap(); + let coords = p.coordinates(); + + assert_eq!(coords, (1.0, 2.0, 3.0)); + } + + #[test] + fn point_drop_impl_safety() { + { + let p = Point::new(1.0, 2.0, 3.0).expect("Failed to create point"); + // p goes out of scope here and should be dropped + } + let p2 = Point::new(4.0, 5.0, 6.0).expect("Failed to create point after drop"); + assert_eq!(p2.coordinates(), (4.0, 5.0, 6.0)); + } + + #[test] + fn point_ffi_repr_c() { + use std::mem; + + // PointShape should be repr(C) and zero-sized in Rust + assert_eq!(mem::size_of::(), 0); + + // c_double should match f64 + assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(mem::align_of::(), mem::align_of::()); + } + + #[test] + fn point_coordinates_after_multiple_uses() { + let p = Point::new(1.1, 2.2, 3.3).expect("Failed to create point"); + + let _coords1 = p.coordinates(); + let _debug1 = format!("{:?}", p); + let _coords2 = p.coordinates(); + let _debug2 = format!("{:?}", p); + let final_coords = p.coordinates(); + + assert_eq!(final_coords, (1.1, 2.2, 3.3)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3748af9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +pub mod ffi { + pub mod point; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..545e950 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +fn main() {} diff --git a/src/occt/include/point.hpp b/src/occt/include/point.hpp index 8b82a03..2dc09f2 100644 --- a/src/occt/include/point.hpp +++ b/src/occt/include/point.hpp @@ -3,9 +3,23 @@ Copyright (C) 2025 Erick Ahmed */ +#ifndef POINT_HPP +#define POINT_HPP + #pragma once -#include +#ifdef __cplusplus +extern "C" { +#endif -TopoDS_Shape make_point(double x, double y, double z); -void coord_point(const TopoDS_Shape& shape, double* x, double* y, double* z); +typedef struct point_shape point_shape_t; + +point_shape_t* make_point(double x, double y, double z); +void coord_point(const point_shape_t* shape, double* x, double* y, double* z); +void delete_point(point_shape_t* shape); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/occt/point.cpp b/src/occt/point.cpp index 84cee5d..6b18d31 100644 --- a/src/occt/point.cpp +++ b/src/occt/point.cpp @@ -10,16 +10,29 @@ #include #include -TopoDS_Shape make_point(double x, double y, double z) { - gp_Pnt point(x, y, z); - return BRepBuilderAPI_MakeVertex(point).Vertex(); -} +struct point_shape_t { + TopoDS_Shape shape; +}; -void coord_point(const TopoDS_Shape& shape, double* x, double* y, double* z) { - TopoDS_Vertex vertex = TopoDS::Vertex(shape); - gp_Pnt point = BRep_Tool::Pnt(vertex); +extern "C" { + point_shape_t* make_point(double x, double y, double z) { + point_shape_t* result = new point_shape_t(); + gp_Pnt point(x, y, z); + result->shape = BRepBuilderAPI_MakeVertex(point).Vertex(); + return result; + } - *x = point.X(); - *y = point.Y(); - *z = point.Z(); + void coord_point(const point_shape_t* shape, double* x, double* y, double* z) { + if (!shape || !x || !y || !z) return; + + TopoDS_Vertex vertex = TopoDS::Vertex(shape->shape); + gp_Pnt point = BRep_Tool::Pnt(vertex); + *x = point.X(); + *y = point.Y(); + *z = point.Z(); + } + + void delete_point(point_shape_t* shape) { + delete shape; + } } diff --git a/test/occt/point-test.cpp b/test/occt/point-test.cpp deleted file mode 100644 index 0d14308..0000000 --- a/test/occt/point-test.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - SPDX-License-Identifier: AGPL-3.0-or-later - Copyright (C) 2025 Erick Ahmed -*/ - -#include -#include -#include -#include "point.hpp" - -TEST_CASE("make_point and coord_point behavior", "[make_point]") { - auto check_coords = [](const TopoDS_Shape& p, const double expected[3]) { - double x, y, z; - coord_point(p, &x, &y, &z); - REQUIRE(x == Catch::Approx(expected[0])); - REQUIRE(y == Catch::Approx(expected[1])); - REQUIRE(z == Catch::Approx(expected[2])); - }; - - SECTION("Accept integer coordinates") { - double coords[3] = {3, -7, 2}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - check_coords(p, coords); - } - - SECTION("Accept float coordinates") { - double coords[3] = {3.2352, 7.124662, -2.5}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - check_coords(p, coords); - } - - SECTION("XYZ Origin") { - double coords[3] = {0, 0, 0}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - check_coords(p, coords); - } - - SECTION("Large magnitude values") { - double coords[3] = {6.5186415e7, 9.48156654e8, -6.516515e6}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - check_coords(p, coords); - } - - SECTION("Multiple points") { - const double points[5][3] = { - {3, 7, 2}, - {3, 7, 2}, - {-5, 3.7, 8}, - {-3, -7, -2}, - {0, 0, 1} - }; - - for (const auto& c : points) { - TopoDS_Shape p = make_point(c[0], c[1], c[2]); - check_coords(p, c); - } - } - - SECTION("Extreme double limits") { - double coords[3] = {std::numeric_limits::max(), - std::numeric_limits::lowest(), - std::numeric_limits::min()}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - check_coords(p, coords); - } - - SECTION("NaN and Infinity") { - double coords[3] = {std::numeric_limits::quiet_NaN(), - std::numeric_limits::infinity(), - -std::numeric_limits::infinity()}; - TopoDS_Shape p = make_point(coords[0], coords[1], coords[2]); - double x, y, z; - coord_point(p, &x, &y, &z); - REQUIRE((std::isnan(x) || std::isinf(x))); - REQUIRE((std::isnan(y) || std::isinf(y))); - REQUIRE((std::isnan(z) || std::isinf(z))); - } -} diff --git a/test/occt/CMakeLists.txt b/tests/occt/CMakeLists.txt similarity index 100% rename from test/occt/CMakeLists.txt rename to tests/occt/CMakeLists.txt diff --git a/tests/occt/point-test.cpp b/tests/occt/point-test.cpp new file mode 100644 index 0000000..b74b876 --- /dev/null +++ b/tests/occt/point-test.cpp @@ -0,0 +1,171 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +#include +#include +#include +#include "point.hpp" + +TEST_CASE("Test point creation, query and deletion behaviour", "[point]") { + auto check_coords = [](const point_shape_t* p, const double expected[3]) { + double x, y, z; + coord_point(p, &x, &y, &z); + REQUIRE(x == Catch::Approx(expected[0])); + REQUIRE(y == Catch::Approx(expected[1])); + REQUIRE(z == Catch::Approx(expected[2])); + }; + + SECTION("Accept integer coordinates") { + double coords[3] = {3, -7, 2}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("Accept float coordinates") { + double coords[3] = {3.2352, 7.124662, -2.5}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("XYZ Origin") { + double coords[3] = {0, 0, 0}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("Large magnitude values") { + double coords[3] = {6.5186415e7, 9.48156654e8, -6.516515e6}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("Multiple points") { + const double points[5][3] = { + {3, 7, 2}, + {3, 7, 2}, + {-5, 3.7, 8}, + {-3, -7, -2}, + {0, 0, 1} + }; + + for (const auto& c : points) { + point_shape_t* p = make_point(c[0], c[1], c[2]); + check_coords(p, c); + delete_point(p); + } + } + + SECTION("Extreme double limits") { + double coords[3] = {std::numeric_limits::max(), + std::numeric_limits::lowest(), + std::numeric_limits::min()}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("NaN and Infinity") { + double coords[3] = {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + double x, y, z; + coord_point(p, &x, &y, &z); + + REQUIRE(std::isnan(x)); + REQUIRE(std::isinf(y)); + REQUIRE(std::isinf(z)); + REQUIRE(y > 0); + REQUIRE(z < 0); + + delete_point(p); + } + + SECTION("Denormal values") { + double coords[3] = {std::numeric_limits::denorm_min(), + -std::numeric_limits::denorm_min(), + 0.0}; + point_shape_t* p = make_point(coords[0], coords[1], coords[2]); + check_coords(p, coords); + delete_point(p); + } + + SECTION("Clear single point") { + point_shape_t* p = make_point(1.0, 2.0, 3.0); + + double x, y, z; + coord_point(p, &x, &y, &z); + REQUIRE(x == Catch::Approx(1.0)); + REQUIRE(y == Catch::Approx(2.0)); + REQUIRE(z == Catch::Approx(3.0)); + + delete_point(p); + } + + SECTION("Clear multiple points") { + const int num_points = 5; + point_shape_t* p[num_points]; + + for (int i = 0; i < num_points; ++i) { + p[i] = make_point(i * 1.0, i * 2.0, i * 3.0); + } + + for (int i = 0; i < num_points; ++i) { + delete_point(p[i]); + } + } + + SECTION("Clear already null shape") { + point_shape_t* null_p = nullptr; + REQUIRE(null_p == nullptr); + delete_point(null_p); + } + + SECTION("Memory efficiency - multiple clear calls") { + for (int i = 0; i < 100; ++i) { + point_shape_t* tmp_p = make_point(1.0, 1.0, 1.0); + delete_point(tmp_p); + } + } + + SECTION("Pointer validity after operations") { + point_shape_t* p = make_point(1.0, 2.0, 3.0); + REQUIRE(p != nullptr); + + double x, y, z; + coord_point(p, &x, &y, &z); + + delete_point(p); + } +} + +TEST_CASE("Benchmark point operations") { + SECTION("make_point performance") { + point_shape_t* p = nullptr; + BENCHMARK("Create point") { + return p = make_point(1.0, 2.0, 3.0); + }; + delete_point(p); + } + + SECTION("coord_point performance") { + point_shape_t* p = make_point(1.0, 2.0, 3.0); + double x, y, z; + + BENCHMARK("Extract coordinates") { + coord_point(p, &x, &y, &z); + }; + delete_point(p); + } + + SECTION("delete_point performance") { + BENCHMARK("Delete point") { + point_shape_t* p = make_point(1.0, 2.0, 3.0); + delete_point(p); + }; + } +} diff --git a/tests/point_test.rs b/tests/point_test.rs new file mode 100644 index 0000000..e11c9eb --- /dev/null +++ b/tests/point_test.rs @@ -0,0 +1,112 @@ +/* + SPDX-License-Identifier: AGPL-3.0-or-later + Copyright (C) 2025 Erick Ahmed +*/ + +use arc_core::ffi::point::Point; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn point_creation() { + let p = Point::new(1.0, 2.0, -3.0).expect("Failed to create point"); + let coords = p.coordinates(); + assert_eq!(coords, (1.0, 2.0, -3.0)); + } + + #[test] + fn point_query() { + let p = Point::new(5.5, 3.15, -0.001).expect("Failed to create point"); + let coords = p.coordinates(); + assert_eq!(coords, (5.5, 3.15, -0.001)); + } + + #[test] + fn point_deletion() { + let p = Point::new(1.0, 2.0, 3.0).expect("Failed to create point"); + let result = p.delete(); + assert!(result.is_ok(), "Deletion should succeed for valid point"); + } + + #[test] + fn point_creation_extreme_values() { + let cases = [ + (f64::MAX, f64::MIN, 0.0), + (0.0, 0.0, 0.0), + (-f64::MAX, -f64::MIN, f64::EPSILON), + ]; + + for (x, y, z) in cases { + let p = Point::new(x, y, z).expect("Failed to create point with extreme values"); + let coords = p.coordinates(); + assert_eq!(coords, (x, y, z)); + p.delete().expect("Failed to delete point"); + } + } + + #[test] + fn point_coordinates_immutability() { + let p = Point::new(1.5, 2.5, 3.5).expect("Failed to create point"); + + let coords1 = p.coordinates(); + let coords2 = p.coordinates(); + let coords3 = p.coordinates(); + + assert_eq!(coords1, coords2); + assert_eq!(coords2, coords3); + assert_eq!(coords1, (1.5, 2.5, 3.5)); + } + + #[test] + fn point_automatic_cleanup_on_drop() { + let coords = { + let p = Point::new(7.0, 8.0, 9.0).expect("Failed to create point"); + p.coordinates() + }; + + assert_eq!(coords, (7.0, 8.0, 9.0)); + + // Verify we can still create points after automatic cleanup + let p2 = Point::new(10.0, 11.0, 12.0).expect("Failed to create point after drop"); + assert_eq!(p2.coordinates(), (10.0, 11.0, 12.0)); + } + + #[test] + fn point_multiple_instances_independent() { + let p1 = Point::new(1.0, 1.0, 1.0).expect("Failed to create point 1"); + let p2 = Point::new(2.0, 2.0, 2.0).expect("Failed to create point 2"); + let p3 = Point::new(3.0, 3.0, 3.0).expect("Failed to create point 3"); + + assert_eq!(p1.coordinates(), (1.0, 1.0, 1.0)); + assert_eq!(p2.coordinates(), (2.0, 2.0, 2.0)); + assert_eq!(p3.coordinates(), (3.0, 3.0, 3.0)); + + p2.delete().expect("Failed to delete p2"); + p1.delete().expect("Failed to delete p1"); + p3.delete().expect("Failed to delete p3"); + } + + #[test] + fn point_debug_format() { + let p = Point::new(1.1, 2.2, 3.3).expect("Failed to create point"); + let debug_output = format!("{:?}", p); + + assert!(debug_output.contains("Point")); + assert!(debug_output.contains("1.1")); + assert!(debug_output.contains("2.2")); + assert!(debug_output.contains("3.3")); + } + + #[test] + fn point_rapid_creation_deletion_cycle_stress_test() { + for i in 0..100 { + let p = Point::new(i as f64, i as f64, i as f64) + .expect(&format!("Failed to create point in iteration {}", i)); + assert_eq!(p.coordinates(), (i as f64, i as f64, i as f64)); + p.delete() + .expect(&format!("Failed to delete point in iteration {}", i)); + } + } +}