Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/coldvox-text-injection/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ xdg_kdotool = ["kdotool"]

[build-dependencies]
cc = "1.2"
pkg-config = "0.3"
pkg-config = "0.3.30"
80 changes: 80 additions & 0 deletions crates/coldvox-text-injection/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Testing Text Injection Backends

This document outlines the process for testing the text injection backends for the `coldvox-text-injection` crate.

## Prerequisites

To run these tests, you will need a graphical environment (X11 or Wayland) and the development packages for GTK3. The required packages for some common distributions are listed below:

**Debian/Ubuntu:**
```bash
sudo apt-get install libgtk-3-dev
```

**Fedora:**
```bash
sudo dnf install gtk3-devel
```

**Arch Linux:**
```bash
sudo pacman -S gtk3
```

You will also need to have the `at-spi2-core` package installed, which is usually included by default in most desktop environments.

## Running the Tests

A test runner script, `run_tests.sh`, is provided to simplify the process of running the tests. This script must be run from the root of the repository.

To run all available backend tests, simply execute the script without any arguments:
```bash
./crates/coldvox-text-injection/run_tests.sh
```

You can also run tests for a specific backend by passing the backend name as an argument to the script. The available backends are:

- `atspi`
- `ydotool`
- `kdotool`
- `clipboard`
- `enigo`

For example, to run only the AT-SPI backend tests, you would use the following command:
```bash
./crates/coldvox-text-injection/run_tests.sh atspi
```

## Test Matrix

The following matrix can be used to track the test results across different platforms and backends. Please fill in the results as you test each combination.

| Backend | GNOME/Wayland | GNOME/X11 | KDE/Wayland | KDE/X11 | Sway | i3 |
|---|---|---|---|---|---|---|
| AT-SPI | ? | ? | ? | ? | ? | ? |
| Clipboard | ? | ? | ? | ? | ? | ? |
| ydotool | ? | N/A | ? | N/A | ? | N/A |
| kdotool | N/A | ? | N/A | ? | N/A | ? |
| Combo | ? | ? | ? | ? | ? | ? |

## Known Issues

* (Please add any known issues here)

## Platform-Specific Setup Notes

* **(GNOME/Wayland)** (Please add any platform-specific setup notes here)
* **(GNOME/X11)** (Please add any platform-specific setup notes here)
* **(KDE/Wayland)** (Please add any platform-specific setup notes here)
* **(KDE/X11)** (Please add any platform-specific setup notes here)
* **(Sway)** (Please add any platform-specific setup notes here)
* **(i3)** (Please add any platform-specific setup notes here)

## Recommended Backends

* **(GNOME/Wayland)** (Please add your recommended backend for this platform here)
* **(GNOME/X11)** (Please add your recommended backend for this platform here)
* **(KDE/Wayland)** (Please add your recommended backend for this platform here)
* **(KDE/X11)** (Please add your recommended backend for this platform here)
* **(Sway)** (Please add your recommended backend for this platform here)
* **(i3)** (Please add your recommended backend for this platform here)
75 changes: 31 additions & 44 deletions crates/coldvox-text-injection/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ use std::path::Path;
use std::process::Command;

fn main() {
// We only need to build the test applications if the `real-injection-tests` feature is enabled.
// This avoids adding build-time dependencies for regular users.
if env::var("CARGO_FEATURE_REAL_INJECTION_TESTS").is_ok() {
build_gtk_test_app();
build_terminal_test_app();
Expand All @@ -17,46 +15,37 @@ fn build_gtk_test_app() {
let out_dir = env::var("OUT_DIR").unwrap();
let executable_path = Path::new(&out_dir).join("gtk_test_app");

// Check if pkg-config and GTK3 are available before attempting to build.
let check_pkg_config = Command::new("pkg-config")
.arg("--atleast-version=3.0")
.arg("gtk+-3.0")
.status();

if check_pkg_config.is_err() || !check_pkg_config.unwrap().success() {
println!("cargo:warning=Skipping GTK test app build: GTK+ 3.0 not found by pkg-config.");
return;
}

// Get compiler flags from pkg-config.
let cflags_output = Command::new("pkg-config")
.arg("--cflags")
.arg("gtk+-3.0")
.output()
.expect("Failed to run pkg-config for cflags");

// Get linker flags from pkg-config.
let libs_output = Command::new("pkg-config")
.arg("--libs")
.arg("gtk+-3.0")
.output()
.expect("Failed to run pkg-config for libs");
// Get compiler and linker flags from pkg-config.
let cflags = match Command::new("pkg-config").arg("--cflags").arg("gtk+-3.0").output() {
Ok(output) if output.status.success() => String::from_utf8(output.stdout).unwrap(),
_ => {
println!("cargo:warning=Skipping GTK test app build: pkg-config could not get cflags for gtk+-3.0.");
return;
}
};

let cflags = String::from_utf8(cflags_output.stdout).unwrap();
let libs = String::from_utf8(libs_output.stdout).unwrap();
let libs = match Command::new("pkg-config").arg("--libs").arg("gtk+-3.0").output() {
Ok(output) if output.status.success() => String::from_utf8(output.stdout).unwrap(),
_ => {
println!("cargo:warning=Skipping GTK test app build: pkg-config could not get libs for gtk+-3.0.");
return;
}
};

// Compile the test app using gcc.
let status = Command::new("gcc")
// Compile the test app using gcc, capturing all output.
let gcc_output = Command::new("gcc")
.arg("test-apps/gtk_test_app.c")
.arg("-o")
.arg(&executable_path)
.args(cflags.split_whitespace())
.args(libs.split_whitespace())
.status()
.output() // Capture output instead of just status
.expect("Failed to execute gcc");

if !status.success() {
println!("cargo:warning=Failed to compile GTK test app. Real injection tests against GTK may fail.");
if !gcc_output.status.success() {
println!("cargo:warning=Failed to compile GTK test app.");
println!("cargo:warning=GCC stdout: {}", String::from_utf8_lossy(&gcc_output.stdout));
println!("cargo:warning=GCC stderr: {}", String::from_utf8_lossy(&gcc_output.stderr));
}
}

Expand All @@ -66,24 +55,22 @@ fn build_terminal_test_app() {

let out_dir = env::var("OUT_DIR").unwrap();
let target_dir = Path::new(&out_dir).join("terminal-test-app-target");
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

// Build the terminal test app using `cargo build`.
let status = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
let build_output = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
.current_dir(Path::new(&crate_dir).join("test-apps/terminal-test-app"))
.arg("build")
.arg("--package")
.arg("terminal-test-app")
.arg("--release") // Build in release mode for faster startup.
.arg("--release")
.arg("--target-dir")
.arg(&target_dir)
.status()
.output() // Capture output
.expect("Failed to execute cargo build for terminal-test-app");

if !status.success() {
println!(
"cargo:warning=Failed to build the terminal test app. Real injection tests may fail."
);
if !build_output.status.success() {
println!("cargo:warning=Failed to build the terminal test app.");
println!("cargo:warning=Cargo stdout: {}", String::from_utf8_lossy(&build_output.stdout));
println!("cargo:warning=Cargo stderr: {}", String::from_utf8_lossy(&build_output.stderr));
} else {
// Copy the executable to a known location in OUT_DIR for the tests to find easily.
let src_path = target_dir.join("release/terminal-test-app");
let dest_path = Path::new(&out_dir).join("terminal-test-app");
if let Err(e) = std::fs::copy(&src_path, &dest_path) {
Expand Down
93 changes: 93 additions & 0 deletions crates/coldvox-text-injection/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash
#
# Test Runner for ColdVox Text Injection Backends
#
# This script automates the process of building and running the real-world
# text injection tests for the `coldvox-text-injection` crate. It allows
# you to selectively test different backends.
#
# Usage:
# ./run_tests.sh [backend]
#
# Examples:
# ./run_tests.sh # Run all available backend tests
# ./run_tests.sh atspi # Run only the AT-SPI backend tests
# ./run_tests.sh ydotool # Run only the ydotool backend tests
#

set -euo pipefail

# --- Configuration ---

# The crate to test
CRATE_NAME="coldvox-text-injection"

# --- Helper Functions ---

# Print a message in blue
info() {
echo -e "\033[1;34m[INFO]\033[0m $1"
}

# Print a message in yellow
warn() {
echo -e "\033[1;33m[WARN]\033[0m $1"
}

# Print a message in green
success() {
echo -e "\033[1;32m[SUCCESS]\033[0m $1"
}

# Print an error message and exit
fatal() {
echo -e "\033[1;31m[FATAL]\033[0m $1" >&2
exit 1
}

# --- Main Logic ---

# Ensure the script is run from the repository root
if [ ! -f "Cargo.toml" ]; then
fatal "This script must be run from the repository root."
fi

# Determine which tests to run
TEST_TARGET="${1:-all}"

# Build the tests with the required features
info "Building tests for '$CRATE_NAME' with 'real-injection-tests' feature..."
cargo build --package "$CRATE_NAME" --features real-injection-tests --tests

# Run the tests
case "$TEST_TARGET" in
all)
info "Running all backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- --nocapture
;;
atspi)
info "Running AT-SPI backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- test_atspi --nocapture
;;
ydotool)
info "Running ydotool backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- test_ydotool --nocapture
;;
kdotool)
info "Running kdotool backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- test_kdotool --nocapture
;;
clipboard)
info "Running clipboard backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- test_clipboard --nocapture
;;
enigo)
info "Running enigo backend tests..."
cargo test --package "$CRATE_NAME" --features real-injection-tests -- test_enigo --nocapture
;;
*)
fatal "Unknown test target: '$TEST_TARGET'. Available targets: all, atspi, ydotool, kdotool, clipboard, enigo"
;;
esac

success "Test run completed for '$TEST_TARGET'."
3 changes: 1 addition & 2 deletions crates/coldvox-text-injection/src/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ use crate::types::{InjectionConfig, InjectionResult};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tracing::{info, warn, debug, trace, error};
use coldvox_foundation::error::InjectionError;
use tracing::{info, warn};
use unicode_segmentation::UnicodeSegmentation;

/// Confirmation result for text injection
Expand Down
2 changes: 0 additions & 2 deletions crates/coldvox-text-injection/src/injectors/atspi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ use async_trait::async_trait;
use coldvox_foundation::error::InjectionError;
use std::time::Instant;
use tracing::{debug, trace, warn};
use crate::log_throttle::log_atspi_connection_failure;
use crate::logging::utils;

// Re-export the old Context type for backwards compatibility
#[deprecated(
Expand Down
2 changes: 1 addition & 1 deletion crates/coldvox-text-injection/src/prewarm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::types::{InjectionConfig, InjectionMethod, InjectionResult};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, info, warn, trace};
use tracing::{debug, info, warn};

/// TTL for cached pre-warmed data (3 seconds)
const CACHE_TTL: Duration = Duration::from_secs(3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::clipboard_paste_injector::ClipboardPasteInjector;
use crate::enigo_injector::EnigoInjector;
#[cfg(feature = "atspi")]
use crate::injectors::atspi::AtspiInjector;
#[cfg(feature = "kdotool")]
use crate::kdotool_injector::KdotoolInjector;
#[cfg(feature = "ydotool")]
use crate::ydotool_injector::YdotoolInjector;
// Bring trait into scope so async trait methods (inject_text, is_available) resolve.
Expand Down Expand Up @@ -353,4 +355,49 @@ async fn test_enigo_typing_special_chars() {
run_enigo_typing_test("Enigo types\nnew lines and\ttabs.").await;
}

// TODO(#40): Add tests for kdotool, combo injectors etc.
//--- Kdotool Tests ---
#[cfg(feature = "kdotool")]
/// Helper function to run a complete injection and verification test for the kdotool backend.
async fn run_kdotool_test(test_text: &str) {
let env = TestEnvironment::current();
if !env.can_run_real_tests() {
eprintln!("Skipping kdotool test: no display server found.");
return;
}

let injector = KdotoolInjector::new(Default::default());
if !injector.is_available().await {
println!("Skipping kdotool test: backend is not available (is kdotool running?).");
return;
}

let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app.");
tokio::time::sleep(Duration::from_millis(500)).await;
wait_for_app_ready(&app).await;

injector
.inject_text(test_text)
.await
.unwrap_or_else(|e| panic!("kdotool injection failed for text '{}': {:?}", test_text, e));

verify_injection(&app.output_file, test_text)
.await
.unwrap_or_else(|e| {
panic!(
"Verification failed for kdotool with text '{}': {}",
test_text, e
)
});
}

#[tokio::test]
#[cfg(feature = "kdotool")]
async fn test_kdotool_simple_text() {
run_kdotool_test("Hello from kdotool!").await;
}

#[tokio::test]
#[cfg(feature = "kdotool")]
async fn test_kdotool_unicode_text() {
run_kdotool_test("Hello ColdVox 🎤 测试 (via kdotool)").await;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading