File size: 7,728 Bytes
7505a0b 2e64b2c 7505a0b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | import pandas as pd
from ankigen.models import Card, CardFront, CardBack
from ankigen.ui_logic import (
cards_to_dataframe,
dataframe_to_cards,
update_mode_visibility,
)
def test_cards_to_dataframe_empty():
"""Test cards_to_dataframe with an empty list of cards."""
df = cards_to_dataframe([])
assert isinstance(df, pd.DataFrame)
assert df.empty
assert list(df.columns) == [
"ID",
"Topic",
"Front",
"Back",
"Tags",
"Card Type",
"Explanation",
"Example",
"Source_URL",
]
def test_cards_to_dataframe_single():
"""Test cards_to_dataframe with a single card."""
card = Card(
front=CardFront(question="What is Python?"),
back=CardBack(answer="A programming language", explanation="E", example="Ex"),
metadata={
"topic": "Programming",
"tags": ["python", "coding"],
"source_url": "http://python.org",
},
card_type="Basic",
)
df = cards_to_dataframe([card])
assert len(df) == 1
assert df.iloc[0]["ID"] == 1
assert df.iloc[0]["Topic"] == "Programming"
assert df.iloc[0]["Front"] == "What is Python?"
assert df.iloc[0]["Back"] == "A programming language"
assert df.iloc[0]["Tags"] == "python, coding"
assert df.iloc[0]["Card Type"] == "Basic"
assert df.iloc[0]["Source_URL"] == "http://python.org"
def test_cards_to_dataframe_multiple():
"""Test cards_to_dataframe with multiple cards having varying metadata/tags."""
cards = [
Card(
front=CardFront(question="Q1"),
back=CardBack(answer="A1", explanation="E1", example="Ex1"),
metadata={"topic": "T1", "tags": ["tag1"]},
),
Card(
front=CardFront(question="Q2"),
back=CardBack(answer="A2", explanation="E2", example="Ex2"),
metadata={"tags": ["tag2", "tag3"]},
),
Card(
front=CardFront(question="Q3"),
back=CardBack(answer="A3", explanation="E3", example="Ex3"),
metadata={},
),
]
df = cards_to_dataframe(cards)
assert len(df) == 3
assert df.iloc[0]["Topic"] == "T1"
assert df.iloc[0]["Tags"] == "tag1"
assert df.iloc[1]["Topic"] == "N/A"
assert df.iloc[1]["Tags"] == "tag2, tag3"
assert df.iloc[2]["Topic"] == "N/A"
assert df.iloc[2]["Tags"] == ""
def test_cards_to_dataframe_no_metadata():
"""Test cards_to_dataframe with cards that have no metadata."""
card = Card(
front=CardFront(question="Q"),
back=CardBack(answer="A", explanation="E", example="Ex"),
metadata=None,
)
df = cards_to_dataframe([card])
assert len(df) == 1
assert df.iloc[0]["Topic"] == "N/A"
assert df.iloc[0]["Tags"] == ""
assert df.iloc[0]["Source_URL"] == ""
def test_dataframe_to_cards_empty():
"""Test dataframe_to_cards with empty dataframe and/or empty cards."""
# Both empty
assert dataframe_to_cards(pd.DataFrame(), []) == []
# Empty DF, non-empty cards
original_cards = [
Card(
front=CardFront(question="Q"),
back=CardBack(answer="A", explanation="E", example="Ex"),
)
]
assert dataframe_to_cards(pd.DataFrame(), original_cards) == []
def test_dataframe_to_cards_round_trip():
"""Test a normal round-trip from cards to dataframe and back with edits."""
original_cards = [
Card(
front=CardFront(question="Original Q"),
back=CardBack(answer="Original A", explanation="E", example="Ex"),
metadata={"topic": "Original T", "tags": ["tag1"]},
)
]
df = cards_to_dataframe(original_cards)
# Edit the dataframe
df.at[0, "Front"] = "Updated Q"
df.at[0, "Back"] = "Updated A"
df.at[0, "Tags"] = "tag1, tag2"
df.at[0, "Topic"] = "Updated T"
df.at[0, "Card Type"] = "Cloze"
updated_cards = dataframe_to_cards(df, original_cards)
assert len(updated_cards) == 1
assert updated_cards[0].front.question == "Updated Q"
assert updated_cards[0].back.answer == "Updated A"
assert updated_cards[0].metadata["tags"] == ["tag1", "tag2"]
assert updated_cards[0].metadata["topic"] == "Updated T"
assert updated_cards[0].card_type == "Cloze"
def test_dataframe_to_cards_out_of_bounds_id(mocker):
"""Test handling of IDs that are out of bounds for the original cards list."""
mock_logger = mocker.patch("ankigen.ui_logic.logger")
original_cards = [
Card(
front=CardFront(question="Q1"),
back=CardBack(answer="A1", explanation="E1", example="Ex1"),
)
]
# ID 2 is out of bounds (only 1 card exists)
df = pd.DataFrame({"ID": [2], "Front": ["Q2"]})
result = dataframe_to_cards(df, original_cards)
assert result == []
mock_logger.warning.assert_called_with(
"Card ID 2 from DataFrame is out of bounds for original_cards list."
)
def test_dataframe_to_cards_error_handling(mocker):
"""Test handling of rows that cause errors during processing."""
mock_logger = mocker.patch("ankigen.ui_logic.logger")
original_cards = [
Card(
front=CardFront(question="Q1"),
back=CardBack(answer="A1", explanation="E1", example="Ex1"),
)
]
# Case 1: Missing ID column (KeyError)
# Desired behavior: the function should handle this gracefully, log an error,
# and not raise an UnboundLocalError.
df_missing_id = pd.DataFrame({"Front": ["Q1"]})
result = dataframe_to_cards(df_missing_id, original_cards)
# Expect that no cards are returned for rows missing a valid ID
assert result == []
mock_logger.error.assert_called()
def test_dataframe_to_cards_error_handling_recovery(mocker):
"""Test that it recovers gracefully if an error occurs after ID is parsed."""
mock_logger = mocker.patch("ankigen.ui_logic.logger")
# Patch the class method instead of instance because Pydantic blocks instance patching of methods
mocker.patch(
"ankigen.models.CardFront.model_copy",
side_effect=AttributeError("Mock Error"),
)
card = Card(
front=CardFront(question="Q1"),
back=CardBack(answer="A1", explanation="E1", example="Ex1"),
)
original_cards = [card]
df = pd.DataFrame({"ID": [1], "Front": ["New Q"]})
result = dataframe_to_cards(df, original_cards)
# It should catch the error, log it, and use the original card
assert len(result) == 1
assert result[0] is card
assert mock_logger.error.call_count >= 1
def test_update_mode_visibility(mocker):
"""Verify update_mode_visibility returns the expected 5-tuple of gr.update() calls."""
# Mock gr.update to return its arguments for easy verification
mocker.patch("gradio.update", side_effect=lambda **kwargs: kwargs)
result = update_mode_visibility("subject", "Biology")
assert isinstance(result, tuple)
assert len(result) == 5
# Verify each update call
assert result[0] == {"visible": True}
assert result[1] == {"visible": True}
assert result[2] == {"value": "Biology"}
# result[3] should have a value that is an empty DataFrame with specific columns
assert isinstance(result[3]["value"], pd.DataFrame)
assert list(result[3]["value"].columns) == [
"Index",
"Topic",
"Card_Type",
"Question",
"Answer",
"Explanation",
"Example",
"Prerequisites",
"Learning_Outcomes",
"Difficulty",
]
# result[4] is total_cards_html
assert "total-cards-count" in result[4]["value"]
assert result[4]["visible"] is False
|