From 92ecadace71d75243704acd551f96099f3ac2ac4 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 23 Jan 2026 10:10:48 -0600 Subject: [PATCH] test: add Rust unit tests + CI Adds Rust unit tests for src-tauri and runs them in CI (and in pre-commit when relevant files are staged). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .githooks/pre-commit | 15 ++++++ .github/workflows/rust-tests.yml | 58 ++++++++++++++++++++ frontend/src-tauri/src/pdf_extractor.rs | 71 +++++++++++++++++++++++++ frontend/src-tauri/src/tts.rs | 38 +++++++++++++ setup-hooks.sh | 1 + 5 files changed, 183 insertions(+) create mode 100644 .github/workflows/rust-tests.yml diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 0f4dddda..3108f527 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -34,5 +34,20 @@ if ! bun run test; then exit 1 fi +# Run Rust unit tests only when relevant Rust/Tauri files are staged +STAGED_FILES=$(git diff --cached --name-only) +if echo "$STAGED_FILES" | grep -Eq '^frontend/src-tauri/.*\.(rs|toml|lock)$'; then + echo "Running Rust unit tests..." + cd "$REPO_ROOT/frontend/src-tauri" || exit 1 + if ! cargo test --all-targets; then + echo "" + echo "Error: Rust tests failed! Please fix the test failures before committing." + echo "Run 'cd frontend/src-tauri && cargo test --all-targets' to see the errors." + exit 1 + fi +else + echo "No staged Rust changes detected; skipping Rust unit tests." +fi + echo "All checks passed! Proceeding with commit..." exit 0 diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml new file mode 100644 index 00000000..7aa218b3 --- /dev/null +++ b/.github/workflows/rust-tests.yml @@ -0,0 +1,58 @@ +name: Rust Unit Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + rust-tests: + runs-on: ubuntu-latest-8-cores + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install sccache + run: | + SCCACHE_VERSION=0.8.2 + SCCACHE_URL="https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" + curl -L "$SCCACHE_URL" | tar xz + sudo mv sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache /usr/local/bin/ + chmod +x /usr/local/bin/sccache + sccache --version + + - name: Cache sccache + uses: actions/cache@v4 + with: + path: ~/.cache/sccache + key: ${{ runner.os }}-sccache-rust-tests-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-sccache-rust-tests- + ${{ runner.os }}-sccache- + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libssl-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + pkg-config + + - name: Configure sccache + run: | + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "SCCACHE_DIR=$HOME/.cache/sccache" >> $GITHUB_ENV + echo "SCCACHE_CACHE_SIZE=2G" >> $GITHUB_ENV + + - name: Run unit tests + working-directory: ./frontend/src-tauri + run: cargo test --all-targets + + - name: Show sccache stats + run: sccache --show-stats diff --git a/frontend/src-tauri/src/pdf_extractor.rs b/frontend/src-tauri/src/pdf_extractor.rs index d4158c9c..2b652a24 100644 --- a/frontend/src-tauri/src/pdf_extractor.rs +++ b/frontend/src-tauri/src/pdf_extractor.rs @@ -48,3 +48,74 @@ pub async fn extract_document_content( status: "completed".to_string(), }) } + +#[cfg(test)] +mod tests { + use super::extract_document_content; + use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; + + #[tokio::test] + async fn extract_document_content_text_plain_success() { + let file_base64 = BASE64.encode(b"Hello, Maple!"); + + let resp = extract_document_content( + file_base64, + "hello.txt".to_string(), + "text/plain".to_string(), + ) + .await + .expect("expected text/plain extraction to succeed"); + + assert_eq!(resp.status, "completed"); + assert_eq!(resp.document.filename, "hello.txt"); + assert_eq!(resp.document.text_content, "Hello, Maple!"); + } + + #[tokio::test] + async fn extract_document_content_rejects_unsupported_file_type() { + let file_base64 = BASE64.encode(b"whatever"); + + let err = extract_document_content( + file_base64, + "file.bin".to_string(), + "application/octet-stream".to_string(), + ) + .await + .expect_err("expected unsupported file type to error"); + + assert!( + err.contains("Unsupported file type"), + "unexpected error: {err}" + ); + } + + #[tokio::test] + async fn extract_document_content_rejects_invalid_base64() { + let err = extract_document_content( + "not base64".to_string(), + "file.txt".to_string(), + "txt".to_string(), + ) + .await + .expect_err("expected invalid base64 to error"); + + assert!( + err.contains("Failed to decode base64 file"), + "unexpected error: {err}" + ); + } + + #[tokio::test] + async fn extract_document_content_rejects_invalid_utf8_for_text_files() { + let file_base64 = BASE64.encode([0xff, 0xfe, 0xfd]); + + let err = extract_document_content(file_base64, "bad.txt".to_string(), "txt".to_string()) + .await + .expect_err("expected invalid utf-8 to error"); + + assert!( + err.contains("Failed to decode text file"), + "unexpected error: {err}" + ); + } +} diff --git a/frontend/src-tauri/src/tts.rs b/frontend/src-tauri/src/tts.rs index e8b78e9b..fbd38771 100644 --- a/frontend/src-tauri/src/tts.rs +++ b/frontend/src-tauri/src/tts.rs @@ -1002,3 +1002,41 @@ pub async fn tts_delete_models(state: tauri::State<'_, Mutex>) -> Resu log::info!("TTS models deleted"); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bytes_to_hex_is_lowercase_and_zero_padded() { + assert_eq!(bytes_to_hex(&[0x00, 0xab, 0xff]), "00abff"); + } + + #[test] + fn preprocess_text_strips_markdown_and_emoji_and_adds_period() { + assert_eq!(preprocess_text("**Hello** _world_ 😊"), "Hello world."); + } + + #[test] + fn preprocess_text_does_not_add_punctuation_if_already_present() { + assert_eq!(preprocess_text("Hi!"), "Hi!"); + } + + #[test] + fn chunk_text_splits_long_sentence_by_words_when_needed() { + let chunks = chunk_text("Hello world. Bye.", 10); + assert_eq!( + chunks, + vec![ + "Hello".to_string(), + "world.".to_string(), + "Bye.".to_string() + ] + ); + } + + #[test] + fn chunk_text_returns_single_empty_chunk_for_empty_input() { + assert_eq!(chunk_text(" ", 10), vec![String::new()]); + } +} diff --git a/setup-hooks.sh b/setup-hooks.sh index 51752b64..5ccd75e9 100755 --- a/setup-hooks.sh +++ b/setup-hooks.sh @@ -18,3 +18,4 @@ echo "The pre-commit hook will now:" echo " 1. Check code formatting with 'bun run format:check'" echo " 2. Run 'bun run build' to ensure the project builds" echo " 3. Run 'bun run test' to ensure unit tests pass" +echo " 4. Run Rust unit tests with 'cargo test --all-targets' when Rust/Tauri files are staged"