fishertools 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fishertools/__init__.py +82 -0
- fishertools/config/__init__.py +24 -0
- fishertools/config/manager.py +247 -0
- fishertools/config/models.py +96 -0
- fishertools/config/parser.py +265 -0
- fishertools/decorators.py +93 -0
- fishertools/documentation/__init__.py +38 -0
- fishertools/documentation/api.py +242 -0
- fishertools/documentation/generator.py +502 -0
- fishertools/documentation/models.py +126 -0
- fishertools/documentation/visual.py +583 -0
- fishertools/errors/__init__.py +29 -0
- fishertools/errors/exceptions.py +191 -0
- fishertools/errors/explainer.py +303 -0
- fishertools/errors/formatters.py +386 -0
- fishertools/errors/models.py +228 -0
- fishertools/errors/patterns.py +119 -0
- fishertools/errors/recovery.py +467 -0
- fishertools/examples/__init__.py +22 -0
- fishertools/examples/models.py +118 -0
- fishertools/examples/repository.py +770 -0
- fishertools/helpers.py +116 -0
- fishertools/integration.py +451 -0
- fishertools/learn/__init__.py +18 -0
- fishertools/learn/examples.py +550 -0
- fishertools/learn/tips.py +281 -0
- fishertools/learning/__init__.py +32 -0
- fishertools/learning/core.py +349 -0
- fishertools/learning/models.py +112 -0
- fishertools/learning/progress.py +314 -0
- fishertools/learning/session.py +500 -0
- fishertools/learning/tutorial.py +626 -0
- fishertools/legacy/__init__.py +76 -0
- fishertools/legacy/deprecated.py +261 -0
- fishertools/legacy/deprecation.py +149 -0
- fishertools/safe/__init__.py +16 -0
- fishertools/safe/collections.py +242 -0
- fishertools/safe/files.py +240 -0
- fishertools/safe/strings.py +15 -0
- fishertools/utils.py +57 -0
- fishertools-0.2.1.dist-info/METADATA +256 -0
- fishertools-0.2.1.dist-info/RECORD +81 -0
- fishertools-0.2.1.dist-info/WHEEL +5 -0
- fishertools-0.2.1.dist-info/licenses/LICENSE +21 -0
- fishertools-0.2.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +6 -0
- tests/conftest.py +25 -0
- tests/test_config/__init__.py +3 -0
- tests/test_config/test_basic_config.py +57 -0
- tests/test_config/test_config_error_handling.py +287 -0
- tests/test_config/test_config_properties.py +435 -0
- tests/test_documentation/__init__.py +3 -0
- tests/test_documentation/test_documentation_properties.py +253 -0
- tests/test_documentation/test_visual_documentation_properties.py +444 -0
- tests/test_errors/__init__.py +3 -0
- tests/test_errors/test_api.py +301 -0
- tests/test_errors/test_error_handling.py +354 -0
- tests/test_errors/test_explainer.py +173 -0
- tests/test_errors/test_formatters.py +338 -0
- tests/test_errors/test_models.py +248 -0
- tests/test_errors/test_patterns.py +270 -0
- tests/test_examples/__init__.py +3 -0
- tests/test_examples/test_example_repository_properties.py +204 -0
- tests/test_examples/test_specific_examples.py +303 -0
- tests/test_integration.py +298 -0
- tests/test_integration_enhancements.py +462 -0
- tests/test_learn/__init__.py +3 -0
- tests/test_learn/test_examples.py +221 -0
- tests/test_learn/test_tips.py +285 -0
- tests/test_learning/__init__.py +3 -0
- tests/test_learning/test_interactive_learning_properties.py +337 -0
- tests/test_learning/test_learning_system_properties.py +194 -0
- tests/test_learning/test_progress_tracking_properties.py +279 -0
- tests/test_legacy/__init__.py +3 -0
- tests/test_legacy/test_backward_compatibility.py +236 -0
- tests/test_legacy/test_deprecation_warnings.py +208 -0
- tests/test_safe/__init__.py +3 -0
- tests/test_safe/test_collections_properties.py +189 -0
- tests/test_safe/test_files.py +104 -0
- tests/test_structure.py +58 -0
- tests/test_structure_enhancements.py +115 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the tips module in fishertools.learn.
|
|
3
|
+
|
|
4
|
+
Tests the show_best_practice function and related utilities for
|
|
5
|
+
displaying Python best practices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from io import StringIO
|
|
10
|
+
import sys
|
|
11
|
+
from fishertools.learn.tips import (
|
|
12
|
+
show_best_practice,
|
|
13
|
+
list_available_topics,
|
|
14
|
+
get_topic_summary,
|
|
15
|
+
BEST_PRACTICES
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestShowBestPractice:
|
|
20
|
+
"""Test the show_best_practice function."""
|
|
21
|
+
|
|
22
|
+
def capture_output(self, func, *args, **kwargs):
|
|
23
|
+
"""Helper method to capture print output."""
|
|
24
|
+
old_stdout = sys.stdout
|
|
25
|
+
sys.stdout = captured_output = StringIO()
|
|
26
|
+
try:
|
|
27
|
+
func(*args, **kwargs)
|
|
28
|
+
return captured_output.getvalue()
|
|
29
|
+
finally:
|
|
30
|
+
sys.stdout = old_stdout
|
|
31
|
+
|
|
32
|
+
def test_show_valid_topic(self):
|
|
33
|
+
"""Test showing best practices for a valid topic."""
|
|
34
|
+
output = self.capture_output(show_best_practice, "variables")
|
|
35
|
+
|
|
36
|
+
# Should contain expected sections
|
|
37
|
+
assert "Переменные в Python" in output
|
|
38
|
+
assert "ЛУЧШИЕ ПРАКТИКИ:" in output
|
|
39
|
+
assert "ПРИМЕР КОДА:" in output
|
|
40
|
+
|
|
41
|
+
# Should contain practical advice
|
|
42
|
+
assert "snake_case" in output
|
|
43
|
+
assert "описательные имена" in output
|
|
44
|
+
|
|
45
|
+
# Should contain code examples
|
|
46
|
+
assert "age = " in output or "name = " in output
|
|
47
|
+
|
|
48
|
+
def test_show_invalid_topic(self):
|
|
49
|
+
"""Test showing best practices for an invalid topic."""
|
|
50
|
+
output = self.capture_output(show_best_practice, "nonexistent_topic")
|
|
51
|
+
|
|
52
|
+
# Should show error message
|
|
53
|
+
assert "❌ Тема 'nonexistent_topic' не найдена" in output
|
|
54
|
+
assert "📚 Доступные темы:" in output
|
|
55
|
+
|
|
56
|
+
def test_case_insensitive_topic(self):
|
|
57
|
+
"""Test that topic matching is case insensitive."""
|
|
58
|
+
output1 = self.capture_output(show_best_practice, "VARIABLES")
|
|
59
|
+
output2 = self.capture_output(show_best_practice, "variables")
|
|
60
|
+
|
|
61
|
+
# Both should show the same content
|
|
62
|
+
assert "Переменные в Python" in output1
|
|
63
|
+
assert "Переменные в Python" in output2
|
|
64
|
+
|
|
65
|
+
def test_whitespace_handling(self):
|
|
66
|
+
"""Test that whitespace is handled correctly."""
|
|
67
|
+
output = self.capture_output(show_best_practice, " variables ")
|
|
68
|
+
|
|
69
|
+
# Should work despite extra whitespace
|
|
70
|
+
assert "Переменные в Python" in output
|
|
71
|
+
|
|
72
|
+
def test_all_topics_display_correctly(self):
|
|
73
|
+
"""Test that all available topics display correctly."""
|
|
74
|
+
topics = list_available_topics()
|
|
75
|
+
|
|
76
|
+
for topic in topics:
|
|
77
|
+
output = self.capture_output(show_best_practice, topic)
|
|
78
|
+
|
|
79
|
+
# Each should produce valid output
|
|
80
|
+
assert len(output) > 100 # Should be substantial content
|
|
81
|
+
assert "ЛУЧШИЕ ПРАКТИКИ:" in output
|
|
82
|
+
assert "ПРИМЕР КОДА:" in output
|
|
83
|
+
|
|
84
|
+
# Should not contain error messages (but may contain ❌ in educational content)
|
|
85
|
+
assert "не найдена" not in output
|
|
86
|
+
assert "Доступные темы:" not in output
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestListAvailableTopics:
|
|
90
|
+
"""Test the list_available_topics function."""
|
|
91
|
+
|
|
92
|
+
def test_returns_list(self):
|
|
93
|
+
"""Test that function returns a list."""
|
|
94
|
+
result = list_available_topics()
|
|
95
|
+
assert isinstance(result, list)
|
|
96
|
+
|
|
97
|
+
def test_contains_expected_topics(self):
|
|
98
|
+
"""Test that list contains expected topics."""
|
|
99
|
+
topics = list_available_topics()
|
|
100
|
+
|
|
101
|
+
# Should contain core Python topics
|
|
102
|
+
expected_topics = [
|
|
103
|
+
"variables", "functions", "lists",
|
|
104
|
+
"dictionaries", "error_handling"
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for topic in expected_topics:
|
|
108
|
+
assert topic in topics
|
|
109
|
+
|
|
110
|
+
def test_all_topics_have_data(self):
|
|
111
|
+
"""Test that all listed topics have corresponding data."""
|
|
112
|
+
topics = list_available_topics()
|
|
113
|
+
|
|
114
|
+
for topic in topics:
|
|
115
|
+
assert topic in BEST_PRACTICES
|
|
116
|
+
assert "title" in BEST_PRACTICES[topic]
|
|
117
|
+
assert "practices" in BEST_PRACTICES[topic]
|
|
118
|
+
assert "example" in BEST_PRACTICES[topic]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestGetTopicSummary:
|
|
122
|
+
"""Test the get_topic_summary function."""
|
|
123
|
+
|
|
124
|
+
def test_valid_topic_returns_summary(self):
|
|
125
|
+
"""Test getting summary for a valid topic."""
|
|
126
|
+
summary = get_topic_summary("variables")
|
|
127
|
+
|
|
128
|
+
assert isinstance(summary, str)
|
|
129
|
+
assert len(summary) > 0
|
|
130
|
+
assert "Переменные в Python" in summary
|
|
131
|
+
|
|
132
|
+
def test_invalid_topic_returns_error(self):
|
|
133
|
+
"""Test getting summary for an invalid topic."""
|
|
134
|
+
summary = get_topic_summary("nonexistent")
|
|
135
|
+
assert "не найдена" in summary
|
|
136
|
+
|
|
137
|
+
def test_case_insensitive_lookup(self):
|
|
138
|
+
"""Test that topic lookup is case insensitive."""
|
|
139
|
+
summary1 = get_topic_summary("VARIABLES")
|
|
140
|
+
summary2 = get_topic_summary("variables")
|
|
141
|
+
|
|
142
|
+
assert summary1 == summary2
|
|
143
|
+
assert "Переменные в Python" in summary1
|
|
144
|
+
|
|
145
|
+
def test_whitespace_handling(self):
|
|
146
|
+
"""Test that whitespace is handled in topic lookup."""
|
|
147
|
+
summary = get_topic_summary(" variables ")
|
|
148
|
+
|
|
149
|
+
assert "Переменные в Python" in summary
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestBestPracticesData:
|
|
153
|
+
"""Test the BEST_PRACTICES data structure."""
|
|
154
|
+
|
|
155
|
+
def test_all_practices_have_required_fields(self):
|
|
156
|
+
"""Test that all best practices have required fields."""
|
|
157
|
+
for topic, data in BEST_PRACTICES.items():
|
|
158
|
+
assert "title" in data
|
|
159
|
+
assert "practices" in data
|
|
160
|
+
assert "example" in data
|
|
161
|
+
|
|
162
|
+
# Fields should not be empty
|
|
163
|
+
assert len(data["title"]) > 0
|
|
164
|
+
assert len(data["practices"]) > 0
|
|
165
|
+
assert len(data["example"]) > 0
|
|
166
|
+
|
|
167
|
+
def test_practices_contain_educational_content(self):
|
|
168
|
+
"""Test that practices contain educational content."""
|
|
169
|
+
for topic, data in BEST_PRACTICES.items():
|
|
170
|
+
practices = data["practices"]
|
|
171
|
+
|
|
172
|
+
# Should contain visual indicators
|
|
173
|
+
assert "🔹" in practices or "✅" in practices or "❌" in practices
|
|
174
|
+
|
|
175
|
+
# Should contain Russian explanations
|
|
176
|
+
assert any(char in practices for char in "абвгдеёжзийклмнопрстуфхцчшщъыьэюя")
|
|
177
|
+
|
|
178
|
+
def test_examples_contain_actual_code(self):
|
|
179
|
+
"""Test that examples contain actual Python code."""
|
|
180
|
+
for topic, data in BEST_PRACTICES.items():
|
|
181
|
+
example = data["example"]
|
|
182
|
+
|
|
183
|
+
# Should contain Python-like syntax
|
|
184
|
+
assert any(keyword in example for keyword in ["def ", "=", "print(", "if ", "for "])
|
|
185
|
+
|
|
186
|
+
def test_examples_are_educational(self):
|
|
187
|
+
"""Test that examples contain educational content."""
|
|
188
|
+
for topic, data in BEST_PRACTICES.items():
|
|
189
|
+
example = data["example"]
|
|
190
|
+
|
|
191
|
+
# Should contain comments (educational explanations)
|
|
192
|
+
assert "#" in example or '"""' in example
|
|
193
|
+
|
|
194
|
+
# Should contain Russian explanations in comments or strings
|
|
195
|
+
assert any(char in example for char in "абвгдеёжзийклмнопрстуфхцчшщъыьэюя")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestContentQuality:
|
|
199
|
+
"""Test the quality and accuracy of educational content."""
|
|
200
|
+
|
|
201
|
+
def test_variables_topic_content(self):
|
|
202
|
+
"""Test specific content for variables topic."""
|
|
203
|
+
data = BEST_PRACTICES["variables"]
|
|
204
|
+
|
|
205
|
+
# Should mention key concepts
|
|
206
|
+
assert "snake_case" in data["practices"]
|
|
207
|
+
assert "описательные имена" in data["practices"]
|
|
208
|
+
assert "константы" in data["practices"]
|
|
209
|
+
|
|
210
|
+
# Example should demonstrate good practices
|
|
211
|
+
example = data["example"]
|
|
212
|
+
assert "user_name" in example or "first_name" in example # snake_case
|
|
213
|
+
assert "MAX_" in example # constants
|
|
214
|
+
|
|
215
|
+
def test_functions_topic_content(self):
|
|
216
|
+
"""Test specific content for functions topic."""
|
|
217
|
+
data = BEST_PRACTICES["functions"]
|
|
218
|
+
|
|
219
|
+
# Should mention key concepts
|
|
220
|
+
assert "docstring" in data["practices"]
|
|
221
|
+
assert "type hints" in data["practices"]
|
|
222
|
+
|
|
223
|
+
# Example should demonstrate good practices
|
|
224
|
+
example = data["example"]
|
|
225
|
+
assert '"""' in example # docstring
|
|
226
|
+
assert "->" in example or ":" in example # type hints or parameters
|
|
227
|
+
|
|
228
|
+
def test_error_handling_topic_content(self):
|
|
229
|
+
"""Test specific content for error handling topic."""
|
|
230
|
+
data = BEST_PRACTICES["error_handling"]
|
|
231
|
+
|
|
232
|
+
# Should mention key concepts
|
|
233
|
+
assert "try" in data["practices"] or "except" in data["practices"]
|
|
234
|
+
assert "finally" in data["practices"] or "контекстные менеджеры" in data["practices"]
|
|
235
|
+
|
|
236
|
+
# Example should demonstrate error handling
|
|
237
|
+
example = data["example"]
|
|
238
|
+
assert "try:" in example
|
|
239
|
+
assert "except" in example
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestIntegration:
|
|
243
|
+
"""Integration tests for the tips module."""
|
|
244
|
+
|
|
245
|
+
def test_complete_workflow(self):
|
|
246
|
+
"""Test complete workflow of discovering and showing best practices."""
|
|
247
|
+
# Get available topics
|
|
248
|
+
topics = list_available_topics()
|
|
249
|
+
assert len(topics) > 0
|
|
250
|
+
|
|
251
|
+
# Get summary for first topic
|
|
252
|
+
first_topic = topics[0]
|
|
253
|
+
summary = get_topic_summary(first_topic)
|
|
254
|
+
assert "не найдена" not in summary
|
|
255
|
+
|
|
256
|
+
# Show best practices for the topic
|
|
257
|
+
output = self.capture_output(show_best_practice, first_topic)
|
|
258
|
+
assert "не найдена" not in output # Should not be an error
|
|
259
|
+
assert summary in output # Should contain the title
|
|
260
|
+
|
|
261
|
+
def test_error_handling_consistency(self):
|
|
262
|
+
"""Test that error handling is consistent across functions."""
|
|
263
|
+
invalid_topic = "definitely_not_a_topic"
|
|
264
|
+
|
|
265
|
+
# show_best_practice should display error message
|
|
266
|
+
output = self.capture_output(show_best_practice, invalid_topic)
|
|
267
|
+
assert "❌" in output
|
|
268
|
+
|
|
269
|
+
# get_topic_summary should return error message
|
|
270
|
+
summary = get_topic_summary(invalid_topic)
|
|
271
|
+
assert "не найдена" in summary
|
|
272
|
+
|
|
273
|
+
# list_available_topics should not include invalid topic
|
|
274
|
+
topics = list_available_topics()
|
|
275
|
+
assert invalid_topic not in topics
|
|
276
|
+
|
|
277
|
+
def capture_output(self, func, *args, **kwargs):
|
|
278
|
+
"""Helper method to capture print output."""
|
|
279
|
+
old_stdout = sys.stdout
|
|
280
|
+
sys.stdout = captured_output = StringIO()
|
|
281
|
+
try:
|
|
282
|
+
func(*args, **kwargs)
|
|
283
|
+
return captured_output.getvalue()
|
|
284
|
+
finally:
|
|
285
|
+
sys.stdout = old_stdout
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for Interactive Learning completeness.
|
|
3
|
+
|
|
4
|
+
Feature: fishertools-enhancements
|
|
5
|
+
Property 5: Interactive Learning Completeness
|
|
6
|
+
Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from hypothesis import given, strategies as st, assume
|
|
11
|
+
from fishertools.learning.session import InteractiveSessionManager
|
|
12
|
+
from fishertools.learning.tutorial import TutorialEngine
|
|
13
|
+
from fishertools.learning.models import DifficultyLevel, ExerciseStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestInteractiveLearningCompleteness:
|
|
17
|
+
"""Property tests for Interactive Learning completeness."""
|
|
18
|
+
|
|
19
|
+
def setup_method(self):
|
|
20
|
+
"""Set up test fixtures."""
|
|
21
|
+
self.session_manager = InteractiveSessionManager()
|
|
22
|
+
self.tutorial_engine = TutorialEngine()
|
|
23
|
+
|
|
24
|
+
@given(
|
|
25
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
26
|
+
topic=st.sampled_from(["variables", "lists", "functions", "loops", "conditionals"]),
|
|
27
|
+
level=st.sampled_from([DifficultyLevel.BEGINNER, DifficultyLevel.INTERMEDIATE, DifficultyLevel.ADVANCED])
|
|
28
|
+
)
|
|
29
|
+
def test_interactive_session_creation_completeness(self, user_id, topic, level):
|
|
30
|
+
"""
|
|
31
|
+
Property 5: For any learning session, the Learning_System should
|
|
32
|
+
provide interactive exercises with proper session management.
|
|
33
|
+
|
|
34
|
+
**Validates: Requirements 5.1**
|
|
35
|
+
"""
|
|
36
|
+
assume(len(user_id.strip()) > 0)
|
|
37
|
+
|
|
38
|
+
user_id = user_id.strip()
|
|
39
|
+
|
|
40
|
+
# Test session creation
|
|
41
|
+
session = self.session_manager.create_session(user_id, topic, level)
|
|
42
|
+
|
|
43
|
+
# Property: Session should be created successfully
|
|
44
|
+
assert session is not None, "Interactive session should be created"
|
|
45
|
+
assert session.session_id, "Session should have a unique ID"
|
|
46
|
+
assert session.topic == topic, "Session topic should match requested topic"
|
|
47
|
+
assert session.level == level, "Session level should match requested level"
|
|
48
|
+
assert isinstance(session.exercises, list), "Session should have exercises list"
|
|
49
|
+
assert len(session.exercises) > 0, "Session should have at least one exercise"
|
|
50
|
+
assert session.current_exercise_index == 0, "Should start with first exercise"
|
|
51
|
+
assert not session.is_completed, "New session should not be completed"
|
|
52
|
+
|
|
53
|
+
# Property: Session should be retrievable
|
|
54
|
+
retrieved_session = self.session_manager.get_session(session.session_id)
|
|
55
|
+
assert retrieved_session is not None, "Session should be retrievable"
|
|
56
|
+
assert retrieved_session.session_id == session.session_id, "Retrieved session should match"
|
|
57
|
+
|
|
58
|
+
# Property: Each exercise should be properly formed
|
|
59
|
+
for exercise in session.exercises:
|
|
60
|
+
assert exercise.id, "Exercise should have an ID"
|
|
61
|
+
assert exercise.title, "Exercise should have a title"
|
|
62
|
+
assert exercise.description, "Exercise should have a description"
|
|
63
|
+
assert exercise.starter_code is not None, "Exercise should have starter code"
|
|
64
|
+
assert isinstance(exercise.hints, list), "Exercise should have hints list"
|
|
65
|
+
assert exercise.difficulty_level == level, "Exercise difficulty should match session level"
|
|
66
|
+
assert exercise.topic == topic, "Exercise topic should match session topic"
|
|
67
|
+
assert exercise.status == ExerciseStatus.NOT_STARTED, "New exercises should not be started"
|
|
68
|
+
|
|
69
|
+
@given(
|
|
70
|
+
topic=st.sampled_from(["variables", "lists", "functions", "loops"]),
|
|
71
|
+
level=st.sampled_from([DifficultyLevel.BEGINNER, DifficultyLevel.INTERMEDIATE])
|
|
72
|
+
)
|
|
73
|
+
def test_exercise_creation_completeness(self, topic, level):
|
|
74
|
+
"""
|
|
75
|
+
Property 5: For any learning session, the Learning_System should
|
|
76
|
+
create appropriate interactive exercises for the topic and level.
|
|
77
|
+
|
|
78
|
+
**Validates: Requirements 5.1, 5.2**
|
|
79
|
+
"""
|
|
80
|
+
# Test exercise creation
|
|
81
|
+
exercise = self.tutorial_engine.create_interactive_exercise(topic, level)
|
|
82
|
+
|
|
83
|
+
# Property: Exercise should be created with all required components
|
|
84
|
+
assert exercise is not None, "Exercise should be created"
|
|
85
|
+
assert exercise.id, "Exercise should have a unique ID"
|
|
86
|
+
assert exercise.title, "Exercise should have a title"
|
|
87
|
+
assert exercise.description, "Exercise should have a description"
|
|
88
|
+
assert exercise.starter_code is not None, "Exercise should have starter code"
|
|
89
|
+
assert exercise.expected_output, "Exercise should have expected output"
|
|
90
|
+
assert isinstance(exercise.hints, list), "Exercise should have hints list"
|
|
91
|
+
assert len(exercise.hints) > 0, "Exercise should have at least one hint"
|
|
92
|
+
assert exercise.difficulty_level == level, "Exercise difficulty should match requested level"
|
|
93
|
+
assert exercise.topic == topic, "Exercise topic should match requested topic"
|
|
94
|
+
assert exercise.status == ExerciseStatus.NOT_STARTED, "New exercise should not be started"
|
|
95
|
+
assert exercise.attempts == 0, "New exercise should have 0 attempts"
|
|
96
|
+
assert exercise.max_attempts > 0, "Exercise should have maximum attempts limit"
|
|
97
|
+
|
|
98
|
+
@given(
|
|
99
|
+
topic=st.sampled_from(["variables", "lists", "functions"]),
|
|
100
|
+
solution=st.text(min_size=1, max_size=200)
|
|
101
|
+
)
|
|
102
|
+
def test_solution_validation_completeness(self, topic, solution):
|
|
103
|
+
"""
|
|
104
|
+
Property 5: For any learning session, the Learning_System should
|
|
105
|
+
validate solutions and provide appropriate feedback.
|
|
106
|
+
|
|
107
|
+
**Validates: Requirements 5.2, 5.3**
|
|
108
|
+
"""
|
|
109
|
+
assume(len(solution.strip()) > 0)
|
|
110
|
+
|
|
111
|
+
# Create exercise for testing
|
|
112
|
+
exercise = self.tutorial_engine.create_interactive_exercise(topic, DifficultyLevel.BEGINNER)
|
|
113
|
+
|
|
114
|
+
# Test solution validation
|
|
115
|
+
result = self.tutorial_engine.validate_solution(exercise, solution)
|
|
116
|
+
|
|
117
|
+
# Property: Validation should always return a result
|
|
118
|
+
assert result is not None, "Validation should return a result"
|
|
119
|
+
assert hasattr(result, 'is_correct'), "Result should have is_correct attribute"
|
|
120
|
+
assert hasattr(result, 'feedback'), "Result should have feedback attribute"
|
|
121
|
+
assert hasattr(result, 'errors'), "Result should have errors attribute"
|
|
122
|
+
assert hasattr(result, 'suggestions'), "Result should have suggestions attribute"
|
|
123
|
+
|
|
124
|
+
# Property: Feedback should be provided
|
|
125
|
+
assert isinstance(result.feedback, str), "Feedback should be a string"
|
|
126
|
+
assert len(result.feedback) > 0, "Feedback should not be empty"
|
|
127
|
+
|
|
128
|
+
# Property: Errors and suggestions should be lists
|
|
129
|
+
assert isinstance(result.errors, list), "Errors should be a list"
|
|
130
|
+
assert isinstance(result.suggestions, list), "Suggestions should be a list"
|
|
131
|
+
|
|
132
|
+
# Property: If incorrect, should provide helpful information
|
|
133
|
+
if not result.is_correct:
|
|
134
|
+
assert len(result.errors) > 0 or len(result.suggestions) > 0, "Should provide errors or suggestions for incorrect solutions"
|
|
135
|
+
|
|
136
|
+
@given(
|
|
137
|
+
topic=st.sampled_from(["variables", "lists", "functions"]),
|
|
138
|
+
attempt=st.text(max_size=100)
|
|
139
|
+
)
|
|
140
|
+
def test_hint_provision_completeness(self, topic, attempt):
|
|
141
|
+
"""
|
|
142
|
+
Property 5: For any learning session, the Learning_System should
|
|
143
|
+
provide hints when users need help.
|
|
144
|
+
|
|
145
|
+
**Validates: Requirements 5.3**
|
|
146
|
+
"""
|
|
147
|
+
# Create exercise for testing
|
|
148
|
+
exercise = self.tutorial_engine.create_interactive_exercise(topic, DifficultyLevel.BEGINNER)
|
|
149
|
+
|
|
150
|
+
# Test hint provision
|
|
151
|
+
hint = self.tutorial_engine.provide_hint(exercise, attempt)
|
|
152
|
+
|
|
153
|
+
# Property: Should always provide a hint
|
|
154
|
+
assert hint is not None, "Should provide a hint"
|
|
155
|
+
assert isinstance(hint, str), "Hint should be a string"
|
|
156
|
+
assert len(hint) > 0, "Hint should not be empty"
|
|
157
|
+
|
|
158
|
+
# Property: Hint should be helpful and relevant
|
|
159
|
+
assert len(hint) > 10, "Hint should be substantial enough to be helpful"
|
|
160
|
+
|
|
161
|
+
# Property: Multiple hint requests should work
|
|
162
|
+
hint2 = self.tutorial_engine.provide_hint(exercise, attempt + " more code")
|
|
163
|
+
assert hint2 is not None, "Should provide multiple hints"
|
|
164
|
+
assert isinstance(hint2, str), "Second hint should be a string"
|
|
165
|
+
|
|
166
|
+
@given(
|
|
167
|
+
topic=st.sampled_from(["variables", "lists", "functions"])
|
|
168
|
+
)
|
|
169
|
+
def test_solution_explanation_completeness(self, topic):
|
|
170
|
+
"""
|
|
171
|
+
Property 5: For any learning session, the Learning_System should
|
|
172
|
+
provide detailed explanations of exercise solutions.
|
|
173
|
+
|
|
174
|
+
**Validates: Requirements 5.4**
|
|
175
|
+
"""
|
|
176
|
+
# Create exercise for testing
|
|
177
|
+
exercise = self.tutorial_engine.create_interactive_exercise(topic, DifficultyLevel.BEGINNER)
|
|
178
|
+
|
|
179
|
+
# Test solution explanation
|
|
180
|
+
explanations = self.tutorial_engine.explain_solution(exercise)
|
|
181
|
+
|
|
182
|
+
# Property: Should provide explanations
|
|
183
|
+
assert explanations is not None, "Should provide solution explanations"
|
|
184
|
+
assert isinstance(explanations, list), "Explanations should be a list"
|
|
185
|
+
|
|
186
|
+
# Property: Each explanation should be well-formed
|
|
187
|
+
for explanation in explanations:
|
|
188
|
+
assert hasattr(explanation, 'step_number'), "Explanation should have step number"
|
|
189
|
+
assert hasattr(explanation, 'description'), "Explanation should have description"
|
|
190
|
+
assert hasattr(explanation, 'code_snippet'), "Explanation should have code snippet"
|
|
191
|
+
assert hasattr(explanation, 'related_concepts'), "Explanation should have related concepts"
|
|
192
|
+
|
|
193
|
+
assert explanation.step_number > 0, "Step number should be positive"
|
|
194
|
+
assert isinstance(explanation.description, str), "Description should be a string"
|
|
195
|
+
assert len(explanation.description) > 0, "Description should not be empty"
|
|
196
|
+
assert isinstance(explanation.code_snippet, str), "Code snippet should be a string"
|
|
197
|
+
assert isinstance(explanation.related_concepts, list), "Related concepts should be a list"
|
|
198
|
+
|
|
199
|
+
@given(
|
|
200
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
201
|
+
topic=st.sampled_from(["variables", "lists", "functions"])
|
|
202
|
+
)
|
|
203
|
+
def test_additional_examples_provision_completeness(self, user_id, topic):
|
|
204
|
+
"""
|
|
205
|
+
Property 5: For any learning session, the Learning_System should
|
|
206
|
+
provide additional examples when users struggle.
|
|
207
|
+
|
|
208
|
+
**Validates: Requirements 5.5**
|
|
209
|
+
"""
|
|
210
|
+
assume(len(user_id.strip()) > 0)
|
|
211
|
+
|
|
212
|
+
user_id = user_id.strip()
|
|
213
|
+
|
|
214
|
+
# Create session
|
|
215
|
+
session = self.session_manager.create_session(user_id, topic, DifficultyLevel.BEGINNER)
|
|
216
|
+
|
|
217
|
+
# Test additional examples provision
|
|
218
|
+
examples = self.session_manager.provide_additional_examples(session.session_id, topic)
|
|
219
|
+
|
|
220
|
+
# Property: Should provide additional examples
|
|
221
|
+
assert examples is not None, "Should provide additional examples"
|
|
222
|
+
assert isinstance(examples, list), "Examples should be a list"
|
|
223
|
+
assert len(examples) > 0, "Should provide at least one example"
|
|
224
|
+
|
|
225
|
+
# Property: Each example should be helpful
|
|
226
|
+
for example in examples:
|
|
227
|
+
assert isinstance(example, str), "Each example should be a string"
|
|
228
|
+
assert len(example) > 0, "Examples should not be empty"
|
|
229
|
+
assert len(example) > 10, "Examples should be substantial enough to be helpful"
|
|
230
|
+
|
|
231
|
+
@given(
|
|
232
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
233
|
+
topic=st.sampled_from(["variables", "lists", "functions"])
|
|
234
|
+
)
|
|
235
|
+
def test_session_workflow_completeness(self, user_id, topic):
|
|
236
|
+
"""
|
|
237
|
+
Property 5: For any learning session, the complete workflow should
|
|
238
|
+
work from creation to completion.
|
|
239
|
+
|
|
240
|
+
**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5**
|
|
241
|
+
"""
|
|
242
|
+
assume(len(user_id.strip()) > 0)
|
|
243
|
+
|
|
244
|
+
user_id = user_id.strip()
|
|
245
|
+
|
|
246
|
+
# Create session
|
|
247
|
+
session = self.session_manager.create_session(user_id, topic, DifficultyLevel.BEGINNER)
|
|
248
|
+
|
|
249
|
+
# Property: Should be able to get current exercise
|
|
250
|
+
current_exercise = self.session_manager.get_current_exercise(session.session_id)
|
|
251
|
+
assert current_exercise is not None, "Should have a current exercise"
|
|
252
|
+
|
|
253
|
+
# Property: Should be able to get session progress
|
|
254
|
+
progress = self.session_manager.get_session_progress(session.session_id)
|
|
255
|
+
assert progress is not None, "Should provide session progress"
|
|
256
|
+
assert 'session_id' in progress, "Progress should include session ID"
|
|
257
|
+
assert 'topic' in progress, "Progress should include topic"
|
|
258
|
+
assert 'current_exercise' in progress, "Progress should include current exercise"
|
|
259
|
+
assert 'total_exercises' in progress, "Progress should include total exercises"
|
|
260
|
+
assert 'progress_percentage' in progress, "Progress should include progress percentage"
|
|
261
|
+
|
|
262
|
+
# Property: Should be able to get hints
|
|
263
|
+
hint = self.session_manager.get_hint(session.session_id)
|
|
264
|
+
assert hint is not None, "Should provide hints"
|
|
265
|
+
assert isinstance(hint, str), "Hint should be a string"
|
|
266
|
+
|
|
267
|
+
# Property: Should be able to submit solutions (even if incorrect)
|
|
268
|
+
test_solution = "# test solution"
|
|
269
|
+
result = self.session_manager.submit_solution(session.session_id, test_solution)
|
|
270
|
+
assert result is not None, "Should validate solutions"
|
|
271
|
+
|
|
272
|
+
# Property: Should be able to complete session
|
|
273
|
+
summary = self.session_manager.complete_session(session.session_id)
|
|
274
|
+
assert summary is not None, "Should provide completion summary"
|
|
275
|
+
assert 'session_id' in summary, "Summary should include session ID"
|
|
276
|
+
assert 'topic' in summary, "Summary should include topic"
|
|
277
|
+
assert 'success_rate' in summary, "Summary should include success rate"
|
|
278
|
+
assert 'feedback' in summary, "Summary should include feedback"
|
|
279
|
+
|
|
280
|
+
def test_error_handling_completeness(self):
|
|
281
|
+
"""
|
|
282
|
+
Property 5: Interactive learning system should handle errors gracefully.
|
|
283
|
+
|
|
284
|
+
**Validates: Requirements 5.1, 5.2, 5.3**
|
|
285
|
+
"""
|
|
286
|
+
# Property: Invalid session IDs should be handled gracefully
|
|
287
|
+
assert self.session_manager.get_session("invalid_id") is None, "Invalid session ID should return None"
|
|
288
|
+
assert self.session_manager.get_hint("invalid_id") is None, "Invalid session ID should return None for hints"
|
|
289
|
+
|
|
290
|
+
# Property: Invalid user inputs should be handled gracefully
|
|
291
|
+
with pytest.raises(ValueError):
|
|
292
|
+
self.session_manager.create_session("", "topic", DifficultyLevel.BEGINNER)
|
|
293
|
+
|
|
294
|
+
with pytest.raises(ValueError):
|
|
295
|
+
self.session_manager.create_session("user", "", DifficultyLevel.BEGINNER)
|
|
296
|
+
|
|
297
|
+
# Property: Empty solutions should be handled gracefully
|
|
298
|
+
exercise = self.tutorial_engine.create_interactive_exercise("variables", DifficultyLevel.BEGINNER)
|
|
299
|
+
result = self.tutorial_engine.validate_solution(exercise, "")
|
|
300
|
+
assert result is not None, "Should handle empty solutions"
|
|
301
|
+
assert not result.is_correct, "Empty solution should be incorrect"
|
|
302
|
+
assert len(result.feedback) > 0, "Should provide feedback for empty solutions"
|
|
303
|
+
|
|
304
|
+
# Property: Invalid code should be handled gracefully
|
|
305
|
+
invalid_code_result = self.tutorial_engine.validate_solution(exercise, "invalid python code !!!")
|
|
306
|
+
assert invalid_code_result is not None, "Should handle invalid code"
|
|
307
|
+
assert not invalid_code_result.is_correct, "Invalid code should be incorrect"
|
|
308
|
+
|
|
309
|
+
@given(
|
|
310
|
+
topic=st.sampled_from(["variables", "lists", "functions"]),
|
|
311
|
+
code_line=st.text(min_size=1, max_size=100).filter(lambda x: x.strip())
|
|
312
|
+
)
|
|
313
|
+
def test_step_explanation_integration_completeness(self, topic, code_line):
|
|
314
|
+
"""
|
|
315
|
+
Property 5: Interactive learning should integrate with step-by-step explanations.
|
|
316
|
+
|
|
317
|
+
**Validates: Requirements 5.4**
|
|
318
|
+
"""
|
|
319
|
+
assume(len(code_line.strip()) > 0)
|
|
320
|
+
|
|
321
|
+
# Test step explanation generation
|
|
322
|
+
try:
|
|
323
|
+
explanation = self.tutorial_engine.generate_step_explanation(code_line.strip())
|
|
324
|
+
|
|
325
|
+
# Property: Should generate explanations for valid code
|
|
326
|
+
assert explanation is not None, "Should generate explanation"
|
|
327
|
+
assert hasattr(explanation, 'description'), "Should have description"
|
|
328
|
+
assert hasattr(explanation, 'code_snippet'), "Should have code snippet"
|
|
329
|
+
assert hasattr(explanation, 'related_concepts'), "Should have related concepts"
|
|
330
|
+
|
|
331
|
+
# Property: Explanation should be informative
|
|
332
|
+
assert len(explanation.description) > 0, "Description should not be empty"
|
|
333
|
+
assert explanation.code_snippet == code_line.strip(), "Code snippet should match input"
|
|
334
|
+
|
|
335
|
+
except ValueError:
|
|
336
|
+
# Invalid code should raise ValueError, which is acceptable
|
|
337
|
+
pass
|