Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
thoughts/
data/testuser/collection.anki2-wal
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ init-env: ## init-env
test: ## Run all tests (unit, integration, and doc tests) with debug logging
pushd $(pkg_src) && RUST_LOG=INFO cargo test --all-features --all-targets -- --test-threads=1 #--nocapture

.PHONY: refresh-test-fixture
refresh-test-fixture: ## Refresh test fixture from golden dataset
@echo "Refreshing test fixture from golden dataset..."
./ankiview/tests/fixtures/copy_golden_dataset.sh

.PHONY: test-verbose
test-verbose: ## Run tests with verbose logging
pushd $(pkg_src) && RUST_LOG=debug cargo test --all-features --all-targets -- --test-threads=1 --nocapture

################################################################################
# Building, Deploying \
BUILDING: ## ##################################################################
Expand Down
3 changes: 3 additions & 0 deletions ankiview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ thiserror = "2.0.11"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

[[bin]]
name = "build_test_collection"
path = "tests/fixtures/build_test_collection.rs"

[profile.release]
codegen-units = 1
Expand Down
64 changes: 64 additions & 0 deletions ankiview/tests/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Test Fixtures

## Golden Test Dataset

**Source**: `/Users/Q187392/dev/s/private/ankiview/data/testuser/`
**Fixture Location**: `test_collection/`

**IMPORTANT**: The golden dataset in the source location is READ-ONLY. Never modify it. All tests work with copies.

### Structure
- 15 notes with real-world content
- Basic card type (front/back)
- 4 media files (PNG images)
- Collection size: ~1MB
- Media directory: ~140KB

### Content Coverage
- Data structures (DAG, Tree, DFS)
- Algorithms and complexity
- Data science metrics (F1, accuracy)
- Database concepts (star schema)
- Embeddings and ML concepts
- Geographic reference systems

### Media Files
- `dag.png` (37KB) - Referenced by note 1695797540370
- `star-schema.png` (16KB) - Referenced by note 1713763428669
- `mercator.png` (24KB) - Referenced by note 1737647330399
- `wsg-enu2.png` (58KB) - Referenced by note 1737647330399

### Refreshing Fixture from Golden Dataset

If the golden dataset is updated, refresh the fixture:

```bash
chmod +x ankiview/tests/fixtures/copy_golden_dataset.sh
./ankiview/tests/fixtures/copy_golden_dataset.sh
```

### Note IDs for Testing

Use these note IDs in integration tests:

```rust
pub mod test_notes {
// Notes with images
pub const DAG_NOTE: i64 = 1695797540370;
pub const STAR_SCHEMA: i64 = 1713763428669;
pub const MERCATOR: i64 = 1737647330399;

// Text-heavy notes
pub const TREE: i64 = 1695797540371;
pub const RECURSIVE_DFS: i64 = 1695797540372;
pub const TAIL_RECURSION: i64 = 1698125272387;

// Data science notes
pub const F1_SCORE: i64 = 1714489634039;
pub const ACCURACY: i64 = 1714489634040;
pub const COLBERT: i64 = 1715928977633;

// For testing errors
pub const NONEXISTENT: i64 = 999999999;
}
```
120 changes: 120 additions & 0 deletions ankiview/tests/fixtures/build_test_collection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Build script to create test collection fixture
// Run manually: cargo run --bin build_test_collection
//
// This script creates a minimal Anki collection using the Anki library.
// Due to the complexity and version-specific nature of the Anki API,
// an alternative approach is to manually create the collection in Anki desktop
// and copy it here. This script serves as documentation of what the collection should contain.

use anki::collection::CollectionBuilder;
use std::path::PathBuf;

fn main() -> anyhow::Result<()> {
println!("Creating test collection...\n");
println!("Note: Due to Anki API complexity, this script creates an empty collection.");
println!("You should add notes manually using Anki desktop, then copy the collection here.\n");

let fixture_dir = PathBuf::from("tests/fixtures/test_collection");

// Remove old collection if exists
if fixture_dir.exists() {
std::fs::remove_dir_all(&fixture_dir)?;
}
std::fs::create_dir_all(&fixture_dir)?;

let collection_path = fixture_dir.join("collection.anki2");
let col = CollectionBuilder::new(&collection_path).build()?;

println!("Created empty collection at: {:?}", collection_path);

// Close collection
col.close(None)?;

// Create media directory
let media_dir = fixture_dir.join("collection.media");
std::fs::create_dir_all(&media_dir)?;

// Create test images
create_test_media(&media_dir)?;

println!("\n==============================================");
println!("MANUAL STEPS REQUIRED:");
println!("==============================================\n");
println!("1. Open Anki desktop application");
println!("2. Create a new profile or use existing one");
println!("3. Add the following 8 notes with Basic card type:\n");
println!(" Note 1:");
println!(" Front: What is Rust?");
println!(" Back: A systems programming language\n");
println!(" Note 2:");
println!(" Front: What is the quadratic formula?");
println!(r#" Back: <pre><code class="language-tex">$x = \frac{{-b \pm \sqrt{{b^2 - 4ac}}}}{{2a}}$</code></pre>"#);
println!();
println!(" Note 3:");
println!(" Front: How to create a vector in Rust?");
println!(r#" Back: <pre><code class="language-rust">let v: Vec<i32> = vec![1, 2, 3];</code></pre>"#);
println!();
println!(" Note 4:");
println!(" Front: Rust logo");
println!(r#" Back: <img src="rust-logo.png" alt="Rust logo">"#);
println!();
println!(" Note 5:");
println!(" Front: External image test");
println!(r#" Back: <img src="https://example.com/test.jpg" alt="External">"#);
println!();
println!(" Note 6:");
println!(" Front: HTML entities test");
println!(" Back: Less than: &lt; Greater than: &gt; Ampersand: &amp;");
println!();
println!(" Note 7:");
println!(" Front: Question with no answer");
println!(" Back: (leave empty)");
println!();
println!(" Note 8:");
println!(" Front: Tagged question");
println!(" Back: Tagged answer");
println!(" Tags: test rust programming");
println!();
println!("4. Close Anki");
println!("5. Copy the collection.anki2 file to:");
println!(" {}", collection_path.display());
println!("6. Copy media files from profile's collection.media/ to:");
println!(" {}", media_dir.display());
println!("7. Note the IDs of the created notes (use SQLite browser or query)");
println!("8. Update tests/helpers/mod.rs with the actual note IDs\n");
println!("==============================================\n");

Ok(())
}

fn create_test_media(media_dir: &std::path::Path) -> anyhow::Result<()> {
// Create a simple 1x1 PNG file (rust-logo.png)
// PNG signature + IHDR chunk for 1x1 red pixel
let rust_logo_png = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
0x00, 0x00, 0x00, 0x0D, // IHDR length
0x49, 0x48, 0x44, 0x52, // IHDR
0x00, 0x00, 0x00, 0x01, // width: 1
0x00, 0x00, 0x00, 0x01, // height: 1
0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, compression, filter, interlace
0x90, 0x77, 0x53, 0xDE, // CRC
0x00, 0x00, 0x00, 0x0C, // IDAT length
0x49, 0x44, 0x41, 0x54, // IDAT
0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00,
0x18, 0xDD, 0x8D, 0xB4, // CRC
0x00, 0x00, 0x00, 0x00, // IEND length
0x49, 0x45, 0x4E, 0x44, // IEND
0xAE, 0x42, 0x60, 0x82, // CRC
];

let rust_logo_path = media_dir.join("rust-logo.png");
std::fs::write(&rust_logo_path, &rust_logo_png)?;
println!("Created test image: {:?}", rust_logo_path);

// Create another simple PNG (sample.jpg - actually a PNG despite the name)
let sample_path = media_dir.join("sample.jpg");
std::fs::write(&sample_path, &rust_logo_png)?;
println!("Created test image: {:?}", sample_path);

Ok(())
}
45 changes: 45 additions & 0 deletions ankiview/tests/fixtures/copy_golden_dataset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Copy golden test dataset to fixtures directory
# This script should be run from the repository root

set -euo pipefail

GOLDEN_SOURCE="/Users/Q187392/dev/s/private/ankiview/data/testuser"
FIXTURE_TARGET="ankiview/tests/fixtures/test_collection"

echo "Copying golden dataset to test fixtures..."

# Remove old fixture if exists
if [ -d "$FIXTURE_TARGET" ]; then
echo "Removing existing fixture at $FIXTURE_TARGET"
rm -rf "$FIXTURE_TARGET"
fi

# Create fixture directory
mkdir -p "$FIXTURE_TARGET"

# Copy collection file (close any open SQLite connections first)
echo "Copying collection.anki2..."
cp "$GOLDEN_SOURCE/collection.anki2" "$FIXTURE_TARGET/"

# Copy media directory
echo "Copying media files..."
cp -r "$GOLDEN_SOURCE/collection.media" "$FIXTURE_TARGET/"

# Copy media database
echo "Copying media database..."
cp "$GOLDEN_SOURCE/collection.media.db2" "$FIXTURE_TARGET/"

# Verify files were copied
echo ""
echo "Verification:"
ls -lh "$FIXTURE_TARGET/collection.anki2"
ls -lh "$FIXTURE_TARGET/collection.media.db2"
echo ""
echo "Media files:"
ls -lh "$FIXTURE_TARGET/collection.media/"
echo ""
echo "Golden dataset copied successfully!"
echo ""
echo "IMPORTANT: Do not modify files in $GOLDEN_SOURCE"
echo "Tests will work with copies of this fixture."
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions ankiview/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use ankiview::infrastructure::AnkiRepository;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tempfile::TempDir;

/// Test fixture for working with temporary Anki collections
#[allow(dead_code)]
pub struct TestCollection {
_temp_dir: TempDir,
pub collection_path: PathBuf,
pub media_dir: PathBuf,
}

impl TestCollection {
/// Create a new test collection by copying the fixture
pub fn new() -> Result<Self> {
let temp_dir = tempfile::tempdir()
.context("Failed to create temporary directory")?;

let fixture_path = Self::fixture_collection_path();
let collection_path = temp_dir.path().join("collection.anki2");

// Copy fixture collection to temp location
std::fs::copy(&fixture_path, &collection_path)
.context("Failed to copy test collection fixture")?;

// Copy media directory
let fixture_media = fixture_path.parent().unwrap().join("collection.media");
let media_dir = temp_dir.path().join("collection.media");

if fixture_media.exists() {
copy_dir_all(&fixture_media, &media_dir)
.context("Failed to copy media directory")?;
} else {
std::fs::create_dir_all(&media_dir)
.context("Failed to create media directory")?;
}

Ok(Self {
_temp_dir: temp_dir,
collection_path,
media_dir,
})
}

/// Get path to the fixture collection
fn fixture_collection_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/test_collection/collection.anki2")
}

/// Open repository for this test collection
pub fn open_repository(&self) -> Result<AnkiRepository> {
AnkiRepository::new(&self.collection_path)
}
}

/// Recursively copy directory contents
fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let dst_path = dst.join(entry.file_name());

if file_type.is_dir() {
copy_dir_all(&entry.path(), &dst_path)?;
} else {
std::fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}

/// Known test note IDs from golden dataset
#[allow(dead_code)]
pub mod test_notes {
// Notes with images - good for testing media path resolution
pub const DAG_NOTE: i64 = 1695797540370; // Has dag.png image
pub const STAR_SCHEMA: i64 = 1713763428669; // Has star-schema.png image
pub const MERCATOR: i64 = 1737647330399; // Has mercator.png and wsg-enu2.png images

// Text-heavy notes - good for testing content rendering
pub const TREE: i64 = 1695797540371;
pub const RECURSIVE_DFS: i64 = 1695797540372;
pub const TAIL_RECURSION: i64 = 1698125272387;
pub const BIG_O: i64 = 1713934919822;

// Data science notes - good for testing HTML formatting
pub const F1_SCORE: i64 = 1714489634039;
pub const ACCURACY: i64 = 1714489634040;
pub const COLBERT: i64 = 1715928977633;

// Additional notes
pub const SCHEMA_REASONING: i64 = 1726838512787;
pub const RRF: i64 = 1727071084388;
pub const AGENT: i64 = 1748163225945;
pub const IMBALANCED: i64 = 1748169001421;

// For testing error cases
pub const NONEXISTENT: i64 = 999999999;
}
Loading