# Tests for ankigen_core/exporters.py import pytest import pandas as pd from unittest.mock import patch, MagicMock, ANY import genanki import gradio # Module to test from ankigen_core import exporters # --- Anki Model Definition Tests --- def test_basic_model_structure(): """Test the structure of the BASIC_MODEL.""" model = exporters.BASIC_MODEL assert isinstance(model, genanki.Model) assert model.name == "AnkiGen Enhanced" # Check some key fields exist field_names = [f["name"] for f in model.fields] assert "Question" in field_names assert "Answer" in field_names assert "Explanation" in field_names assert "Difficulty" in field_names # Check number of templates (should be 1 based on code) assert len(model.templates) == 1 # Check CSS is present assert isinstance(model.css, str) assert len(model.css) > 100 # Basic check for non-empty CSS # Check model ID is within the random range (roughly) assert (1 << 30) <= model.model_id < (1 << 31) def test_cloze_model_structure(): """Test the structure of the CLOZE_MODEL.""" model = exporters.CLOZE_MODEL assert isinstance(model, genanki.Model) assert model.name == "AnkiGen Cloze Enhanced" # Check some key fields exist field_names = [f["name"] for f in model.fields] assert "Text" in field_names assert "Extra" in field_names assert "Difficulty" in field_names assert "SourceTopic" in field_names # Check model type is Cloze by looking for cloze syntax in the template assert len(model.templates) > 0 assert "{{cloze:Text}}" in model.templates[0]["qfmt"] # Check number of templates (should be 1 based on code) assert len(model.templates) == 1 # Check CSS is present assert isinstance(model.css, str) assert len(model.css) > 100 # Basic check for non-empty CSS # Check model ID is within the random range (roughly) assert (1 << 30) <= model.model_id < (1 << 31) # Ensure model IDs are different (highly likely due to random range) assert exporters.BASIC_MODEL.model_id != exporters.CLOZE_MODEL.model_id # --- export_csv Tests --- @patch("tempfile.NamedTemporaryFile") def test_export_csv_success(mock_named_temp_file): """Test successful CSV export.""" # Setup mock temp file mock_file = MagicMock() mock_file.name = "/tmp/test_anki_cards.csv" mock_named_temp_file.return_value.__enter__.return_value = mock_file # Create sample DataFrame data = { "Question": ["Q1"], "Answer": ["A1"], "Explanation": ["E1"], "Example": ["Ex1"], } df = pd.DataFrame(data) # Mock the to_csv method to return a dummy string dummy_csv_string = "Question,Answer,Explanation,Example\\nQ1,A1,E1,Ex1" df.to_csv = MagicMock(return_value=dummy_csv_string) # Call the function result_path = exporters.export_csv(df) # Assertions mock_named_temp_file.assert_called_once_with( mode="w+", delete=False, suffix=".csv", encoding="utf-8" ) df.to_csv.assert_called_once_with(index=False) mock_file.write.assert_called_once_with(dummy_csv_string) assert result_path == mock_file.name def test_export_csv_none_input(): """Test export_csv with None input raises gr.Error.""" with pytest.raises(gradio.Error, match="No card data available"): exporters.export_csv(None) @patch("tempfile.NamedTemporaryFile") def test_export_csv_empty_dataframe(mock_named_temp_file): """Test export_csv with an empty DataFrame raises gr.Error.""" mock_file = MagicMock() mock_file.name = "/tmp/empty_anki_cards.csv" mock_named_temp_file.return_value.__enter__.return_value = mock_file df = pd.DataFrame() # Empty DataFrame df.to_csv = MagicMock() with pytest.raises(gradio.Error, match="No card data available"): exporters.export_csv(df) # --- export_deck Tests --- @pytest.fixture def mock_deck_and_package(): """Fixture to mock genanki.Deck and genanki.Package.""" with ( patch("genanki.Deck") as MockDeck, patch("genanki.Package") as MockPackage, patch("tempfile.NamedTemporaryFile") as MockTempFile, patch("random.randrange") as MockRandRange, ): # Mock randrange for deterministic deck ID mock_deck_instance = MagicMock() MockDeck.return_value = mock_deck_instance mock_package_instance = MagicMock() MockPackage.return_value = mock_package_instance mock_temp_file_instance = MagicMock() mock_temp_file_instance.name = "/tmp/test_deck.apkg" MockTempFile.return_value.__enter__.return_value = mock_temp_file_instance MockRandRange.return_value = 1234567890 # Deterministic ID yield { "Deck": MockDeck, "deck_instance": mock_deck_instance, "Package": MockPackage, "package_instance": mock_package_instance, "TempFile": MockTempFile, "temp_file_instance": mock_temp_file_instance, "RandRange": MockRandRange, } def create_sample_card_data( card_type="basic", question="Q1", answer="A1", explanation="E1", example="Ex1", prerequisites="P1", learning_outcomes="LO1", common_misconceptions="CM1", difficulty="Beginner", topic="Topic1", ): return { "Card_Type": card_type, "Question": question, "Answer": answer, "Explanation": explanation, "Example": example, "Prerequisites": prerequisites, "Learning_Outcomes": learning_outcomes, "Common_Misconceptions": common_misconceptions, "Difficulty": difficulty, "Topic": topic, } def test_export_deck_success_basic_cards(mock_deck_and_package): """Test successful deck export with basic cards.""" sample_data = [create_sample_card_data(card_type="basic")] df = pd.DataFrame(sample_data) subject = "Test Subject" with patch("genanki.Note") as MockNote: mock_note_instance = MagicMock() MockNote.return_value = mock_note_instance result_file = exporters.export_deck(df, subject) mock_deck_and_package["Deck"].assert_called_once_with( 1234567890, f"AnkiGen - {subject}" ) mock_deck_and_package["deck_instance"].add_model.assert_any_call( exporters.BASIC_MODEL ) mock_deck_and_package["deck_instance"].add_model.assert_any_call( exporters.CLOZE_MODEL ) MockNote.assert_called_once_with( model=exporters.BASIC_MODEL, fields=["Q1", "A1", "E1", "Ex1", "P1", "LO1", "CM1", "Beginner"], ) mock_deck_and_package["deck_instance"].add_note.assert_called_once_with( mock_note_instance ) mock_deck_and_package["Package"].assert_called_once_with( mock_deck_and_package["deck_instance"] ) mock_deck_and_package["package_instance"].write_to_file.assert_called_once_with( "/tmp/test_deck.apkg" ) assert result_file == "/tmp/test_deck.apkg" def test_export_deck_success_cloze_cards(mock_deck_and_package): """Test successful deck export with cloze cards.""" sample_data = [ create_sample_card_data( card_type="cloze", question="This is a {{c1::cloze}} question." ) ] df = pd.DataFrame(sample_data) subject = "Cloze Subject" with patch("genanki.Note") as MockNote: mock_note_instance = MagicMock() MockNote.return_value = mock_note_instance exporters.export_deck(df, subject) # Match the exact multiline string output from the f-string in export_deck expected_extra = ( "
Ex1