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,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for Learning System completeness.
|
|
3
|
+
|
|
4
|
+
Feature: fishertools-enhancements
|
|
5
|
+
Property 1: Learning System Completeness
|
|
6
|
+
Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from hypothesis import given, strategies as st, assume
|
|
11
|
+
from fishertools.learning import LearningSystem
|
|
12
|
+
from fishertools.learning.models import DifficultyLevel, CodeContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestLearningSystemCompleteness:
|
|
16
|
+
"""Property tests for Learning System completeness."""
|
|
17
|
+
|
|
18
|
+
def setup_method(self):
|
|
19
|
+
"""Set up test fixtures."""
|
|
20
|
+
self.learning_system = LearningSystem()
|
|
21
|
+
|
|
22
|
+
@given(
|
|
23
|
+
topic=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
24
|
+
level=st.sampled_from(["beginner", "intermediate", "advanced"])
|
|
25
|
+
)
|
|
26
|
+
def test_step_by_step_explanation_completeness(self, topic, level):
|
|
27
|
+
"""
|
|
28
|
+
Property 1: For any fishertools function used by a beginner,
|
|
29
|
+
the Learning_System should provide step-by-step explanations
|
|
30
|
+
that include input/output examples, related topics, and
|
|
31
|
+
level-appropriate content adaptation.
|
|
32
|
+
|
|
33
|
+
**Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5**
|
|
34
|
+
"""
|
|
35
|
+
# Generate simple Python code for testing
|
|
36
|
+
test_codes = [
|
|
37
|
+
"x = 5",
|
|
38
|
+
"my_list = [1, 2, 3]",
|
|
39
|
+
"def greet(name): return f'Hello {name}'",
|
|
40
|
+
"if x > 0: print('positive')",
|
|
41
|
+
"for i in range(3): print(i)"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for code in test_codes:
|
|
45
|
+
# Test that explanations are provided
|
|
46
|
+
explanations = self.learning_system.get_step_by_step_explanation(code)
|
|
47
|
+
|
|
48
|
+
# Property: Should always provide explanations for valid code
|
|
49
|
+
assert len(explanations) > 0, f"No explanations provided for code: {code}"
|
|
50
|
+
|
|
51
|
+
# Property: Each explanation should have required components
|
|
52
|
+
for explanation in explanations:
|
|
53
|
+
assert explanation.step_number > 0, "Step number should be positive"
|
|
54
|
+
assert explanation.description, "Description should not be empty"
|
|
55
|
+
assert explanation.code_snippet, "Code snippet should not be empty"
|
|
56
|
+
assert isinstance(explanation.related_concepts, list), "Related concepts should be a list"
|
|
57
|
+
|
|
58
|
+
@given(
|
|
59
|
+
topic=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
60
|
+
level=st.sampled_from(["beginner", "intermediate", "advanced"])
|
|
61
|
+
)
|
|
62
|
+
def test_tutorial_session_creation_completeness(self, topic, level):
|
|
63
|
+
"""
|
|
64
|
+
Property 1: For any topic request, the Learning_System should
|
|
65
|
+
create a tutorial session with appropriate exercises and content.
|
|
66
|
+
|
|
67
|
+
**Validates: Requirements 1.1, 1.4, 1.5**
|
|
68
|
+
"""
|
|
69
|
+
assume(len(topic.strip()) > 0)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Test tutorial session creation
|
|
73
|
+
session = self.learning_system.start_tutorial(topic, level)
|
|
74
|
+
|
|
75
|
+
# Property: Session should be created successfully
|
|
76
|
+
assert session is not None, "Tutorial session should be created"
|
|
77
|
+
assert session.topic == topic, "Session topic should match requested topic"
|
|
78
|
+
assert session.level.value == level, "Session level should match requested level"
|
|
79
|
+
assert session.session_id, "Session should have a unique ID"
|
|
80
|
+
assert isinstance(session.exercises, list), "Session should have exercises list"
|
|
81
|
+
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
# Invalid inputs should raise ValueError, which is acceptable
|
|
84
|
+
assert "Invalid" in str(e) or "must be" in str(e)
|
|
85
|
+
|
|
86
|
+
@given(
|
|
87
|
+
current_topic=st.text(min_size=1, max_size=50).filter(lambda x: x.strip())
|
|
88
|
+
)
|
|
89
|
+
def test_related_topics_suggestion_completeness(self, current_topic):
|
|
90
|
+
"""
|
|
91
|
+
Property 1: For any current topic, the Learning_System should
|
|
92
|
+
suggest related topics for further learning.
|
|
93
|
+
|
|
94
|
+
**Validates: Requirements 1.4, 1.5**
|
|
95
|
+
"""
|
|
96
|
+
assume(len(current_topic.strip()) > 0)
|
|
97
|
+
|
|
98
|
+
# Test related topics suggestion
|
|
99
|
+
related_topics = self.learning_system.suggest_related_topics(current_topic)
|
|
100
|
+
|
|
101
|
+
# Property: Should return a list (may be empty for unknown topics)
|
|
102
|
+
assert isinstance(related_topics, list), "Related topics should be a list"
|
|
103
|
+
|
|
104
|
+
# Property: All suggested topics should be strings
|
|
105
|
+
for topic in related_topics:
|
|
106
|
+
assert isinstance(topic, str), "Each related topic should be a string"
|
|
107
|
+
assert len(topic.strip()) > 0, "Related topics should not be empty strings"
|
|
108
|
+
|
|
109
|
+
# Property: Should not suggest more than reasonable number of topics
|
|
110
|
+
assert len(related_topics) <= 10, "Should not suggest too many topics"
|
|
111
|
+
|
|
112
|
+
@given(
|
|
113
|
+
content=st.text(min_size=1, max_size=200),
|
|
114
|
+
level=st.sampled_from(["beginner", "intermediate", "advanced"])
|
|
115
|
+
)
|
|
116
|
+
def test_content_adaptation_completeness(self, content, level):
|
|
117
|
+
"""
|
|
118
|
+
Property 1: For any content and level, the Learning_System should
|
|
119
|
+
adapt content appropriately for the specified difficulty level.
|
|
120
|
+
|
|
121
|
+
**Validates: Requirements 1.5**
|
|
122
|
+
"""
|
|
123
|
+
assume(len(content.strip()) > 0)
|
|
124
|
+
|
|
125
|
+
# Test content adaptation
|
|
126
|
+
adapted_content = self.learning_system.adapt_content_for_level(content, level)
|
|
127
|
+
|
|
128
|
+
# Property: Should return adapted content
|
|
129
|
+
assert isinstance(adapted_content, str), "Adapted content should be a string"
|
|
130
|
+
assert len(adapted_content) > 0, "Adapted content should not be empty"
|
|
131
|
+
|
|
132
|
+
# Property: Beginner content should be more detailed
|
|
133
|
+
if level == "beginner":
|
|
134
|
+
# Beginner content often has additional explanatory text
|
|
135
|
+
assert len(adapted_content) >= len(content) * 0.8, "Beginner content should maintain or increase length"
|
|
136
|
+
|
|
137
|
+
@given(
|
|
138
|
+
code=st.text(min_size=1, max_size=100).filter(lambda x: x.strip())
|
|
139
|
+
)
|
|
140
|
+
def test_explanation_with_context_completeness(self, code):
|
|
141
|
+
"""
|
|
142
|
+
Property 1: For any code with context, the Learning_System should
|
|
143
|
+
provide enhanced explanations using the context information.
|
|
144
|
+
|
|
145
|
+
**Validates: Requirements 1.2, 1.3**
|
|
146
|
+
"""
|
|
147
|
+
assume(len(code.strip()) > 0)
|
|
148
|
+
|
|
149
|
+
# Create context
|
|
150
|
+
context = CodeContext(
|
|
151
|
+
function_name="test_function",
|
|
152
|
+
variables={"x": 5, "name": "test"}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Test explanation with context
|
|
157
|
+
explanations = self.learning_system.get_step_by_step_explanation(code, context)
|
|
158
|
+
|
|
159
|
+
# Property: Should provide explanations even with context
|
|
160
|
+
assert isinstance(explanations, list), "Explanations should be a list"
|
|
161
|
+
|
|
162
|
+
# Property: Each explanation should be well-formed
|
|
163
|
+
for explanation in explanations:
|
|
164
|
+
assert hasattr(explanation, 'step_number'), "Should have step number"
|
|
165
|
+
assert hasattr(explanation, 'description'), "Should have description"
|
|
166
|
+
assert hasattr(explanation, 'code_snippet'), "Should have code snippet"
|
|
167
|
+
|
|
168
|
+
except (SyntaxError, ValueError):
|
|
169
|
+
# Invalid code should be handled gracefully
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def test_learning_system_initialization_completeness(self):
|
|
173
|
+
"""
|
|
174
|
+
Property 1: Learning System should initialize properly and
|
|
175
|
+
provide all required functionality.
|
|
176
|
+
|
|
177
|
+
**Validates: Requirements 1.1**
|
|
178
|
+
"""
|
|
179
|
+
# Test initialization
|
|
180
|
+
system = LearningSystem()
|
|
181
|
+
|
|
182
|
+
# Property: Should have all required methods
|
|
183
|
+
required_methods = [
|
|
184
|
+
'start_tutorial',
|
|
185
|
+
'get_step_by_step_explanation',
|
|
186
|
+
'suggest_related_topics',
|
|
187
|
+
'adapt_content_for_level',
|
|
188
|
+
'track_progress',
|
|
189
|
+
'get_user_progress'
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
for method_name in required_methods:
|
|
193
|
+
assert hasattr(system, method_name), f"Should have {method_name} method"
|
|
194
|
+
assert callable(getattr(system, method_name)), f"{method_name} should be callable"
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for Progress Tracking correctness.
|
|
3
|
+
|
|
4
|
+
Feature: fishertools-enhancements
|
|
5
|
+
Property 6: Progress Tracking Correctness
|
|
6
|
+
Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
import tempfile
|
|
11
|
+
import os
|
|
12
|
+
from hypothesis import given, strategies as st, assume
|
|
13
|
+
from fishertools.learning.progress import ProgressSystem
|
|
14
|
+
from fishertools.learning.models import DifficultyLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestProgressTrackingCorrectness:
|
|
18
|
+
"""Property tests for Progress Tracking correctness."""
|
|
19
|
+
|
|
20
|
+
def setup_method(self):
|
|
21
|
+
"""Set up test fixtures with temporary storage."""
|
|
22
|
+
# Use temporary file for testing to avoid conflicts
|
|
23
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
24
|
+
self.storage_path = os.path.join(self.temp_dir, "test_progress.json")
|
|
25
|
+
self.progress_system = ProgressSystem(storage_path=self.storage_path)
|
|
26
|
+
|
|
27
|
+
def teardown_method(self):
|
|
28
|
+
"""Clean up test fixtures."""
|
|
29
|
+
# Clean up temporary files
|
|
30
|
+
if os.path.exists(self.storage_path):
|
|
31
|
+
os.remove(self.storage_path)
|
|
32
|
+
os.rmdir(self.temp_dir)
|
|
33
|
+
|
|
34
|
+
@given(
|
|
35
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
36
|
+
level=st.sampled_from([DifficultyLevel.BEGINNER, DifficultyLevel.INTERMEDIATE, DifficultyLevel.ADVANCED])
|
|
37
|
+
)
|
|
38
|
+
def test_user_profile_creation_correctness(self, user_id, level):
|
|
39
|
+
"""
|
|
40
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
41
|
+
create progress profiles correctly.
|
|
42
|
+
|
|
43
|
+
**Validates: Requirements 6.1**
|
|
44
|
+
"""
|
|
45
|
+
assume(len(user_id.strip()) > 0)
|
|
46
|
+
|
|
47
|
+
# Test profile creation
|
|
48
|
+
progress = self.progress_system.create_user_profile(user_id.strip(), level)
|
|
49
|
+
|
|
50
|
+
# Property: Profile should be created with correct initial values
|
|
51
|
+
assert progress is not None, "Progress profile should be created"
|
|
52
|
+
assert progress.user_id == user_id.strip(), "User ID should match"
|
|
53
|
+
assert progress.current_level == level, "Level should match"
|
|
54
|
+
assert progress.completed_topics == [], "Should start with no completed topics"
|
|
55
|
+
assert progress.total_exercises_completed == 0, "Should start with 0 exercises"
|
|
56
|
+
assert progress.achievements == [], "Should start with no achievements"
|
|
57
|
+
assert progress.session_count == 0, "Should start with 0 sessions"
|
|
58
|
+
assert progress.total_time_spent == 0, "Should start with 0 time spent"
|
|
59
|
+
|
|
60
|
+
# Property: Profile should be retrievable
|
|
61
|
+
retrieved_progress = self.progress_system.get_progress(user_id.strip())
|
|
62
|
+
assert retrieved_progress is not None, "Should be able to retrieve created profile"
|
|
63
|
+
assert retrieved_progress.user_id == progress.user_id, "Retrieved profile should match"
|
|
64
|
+
|
|
65
|
+
@given(
|
|
66
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
67
|
+
topics=st.lists(st.text(min_size=1, max_size=30).filter(lambda x: x.strip()), min_size=1, max_size=10)
|
|
68
|
+
)
|
|
69
|
+
def test_topic_completion_tracking_correctness(self, user_id, topics):
|
|
70
|
+
"""
|
|
71
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
72
|
+
track completed topics correctly.
|
|
73
|
+
|
|
74
|
+
**Validates: Requirements 6.2**
|
|
75
|
+
"""
|
|
76
|
+
assume(len(user_id.strip()) > 0)
|
|
77
|
+
assume(all(len(topic.strip()) > 0 for topic in topics))
|
|
78
|
+
|
|
79
|
+
user_id = user_id.strip()
|
|
80
|
+
topics = [topic.strip() for topic in topics]
|
|
81
|
+
|
|
82
|
+
# Create user profile
|
|
83
|
+
self.progress_system.create_user_profile(user_id)
|
|
84
|
+
|
|
85
|
+
# Track topic completions
|
|
86
|
+
unique_topics_completed = set()
|
|
87
|
+
for i, topic in enumerate(topics):
|
|
88
|
+
self.progress_system.update_progress(user_id, topic, True)
|
|
89
|
+
|
|
90
|
+
# Property: Progress should be updated correctly
|
|
91
|
+
progress = self.progress_system.get_progress(user_id)
|
|
92
|
+
assert progress is not None, "Progress should exist"
|
|
93
|
+
assert topic in progress.completed_topics, f"Topic {topic} should be marked as completed"
|
|
94
|
+
|
|
95
|
+
# Only count unique topics for exercise count
|
|
96
|
+
if topic not in unique_topics_completed:
|
|
97
|
+
unique_topics_completed.add(topic)
|
|
98
|
+
|
|
99
|
+
expected_count = len(unique_topics_completed)
|
|
100
|
+
assert progress.total_exercises_completed == expected_count, f"Exercise count should be {expected_count} for unique topics"
|
|
101
|
+
|
|
102
|
+
# Property: All unique topics should be tracked
|
|
103
|
+
final_progress = self.progress_system.get_progress(user_id)
|
|
104
|
+
assert len(final_progress.completed_topics) == len(set(topics)), "All unique topics should be tracked"
|
|
105
|
+
|
|
106
|
+
# Property: Duplicate completions should not increase count
|
|
107
|
+
duplicate_topic = topics[0]
|
|
108
|
+
initial_count = final_progress.total_exercises_completed
|
|
109
|
+
self.progress_system.update_progress(user_id, duplicate_topic, True)
|
|
110
|
+
|
|
111
|
+
updated_progress = self.progress_system.get_progress(user_id)
|
|
112
|
+
assert updated_progress.total_exercises_completed == initial_count, "Duplicate completions should not increase count"
|
|
113
|
+
|
|
114
|
+
@given(
|
|
115
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip())
|
|
116
|
+
)
|
|
117
|
+
def test_progress_persistence_correctness(self, user_id):
|
|
118
|
+
"""
|
|
119
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
120
|
+
persist progress between sessions correctly.
|
|
121
|
+
|
|
122
|
+
**Validates: Requirements 6.3**
|
|
123
|
+
"""
|
|
124
|
+
assume(len(user_id.strip()) > 0)
|
|
125
|
+
|
|
126
|
+
user_id = user_id.strip()
|
|
127
|
+
|
|
128
|
+
# Create and update progress
|
|
129
|
+
self.progress_system.create_user_profile(user_id)
|
|
130
|
+
self.progress_system.update_progress(user_id, "test_topic", True)
|
|
131
|
+
self.progress_system.add_achievement(user_id, "test_achievement")
|
|
132
|
+
|
|
133
|
+
original_progress = self.progress_system.get_progress(user_id)
|
|
134
|
+
|
|
135
|
+
# Create new progress system instance (simulating new session)
|
|
136
|
+
new_progress_system = ProgressSystem(storage_path=self.storage_path)
|
|
137
|
+
|
|
138
|
+
# Property: Progress should be loaded from storage
|
|
139
|
+
loaded_progress = new_progress_system.get_progress(user_id)
|
|
140
|
+
assert loaded_progress is not None, "Progress should be loaded from storage"
|
|
141
|
+
assert loaded_progress.user_id == original_progress.user_id, "User ID should match"
|
|
142
|
+
assert loaded_progress.completed_topics == original_progress.completed_topics, "Completed topics should match"
|
|
143
|
+
assert loaded_progress.achievements == original_progress.achievements, "Achievements should match"
|
|
144
|
+
assert loaded_progress.total_exercises_completed == original_progress.total_exercises_completed, "Exercise count should match"
|
|
145
|
+
|
|
146
|
+
@given(
|
|
147
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip())
|
|
148
|
+
)
|
|
149
|
+
def test_next_topic_suggestions_correctness(self, user_id):
|
|
150
|
+
"""
|
|
151
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
152
|
+
suggest appropriate next steps based on completion status.
|
|
153
|
+
|
|
154
|
+
**Validates: Requirements 6.4**
|
|
155
|
+
"""
|
|
156
|
+
assume(len(user_id.strip()) > 0)
|
|
157
|
+
|
|
158
|
+
user_id = user_id.strip()
|
|
159
|
+
|
|
160
|
+
# Test suggestions for new user
|
|
161
|
+
suggestions = self.progress_system.suggest_next_topics(user_id)
|
|
162
|
+
|
|
163
|
+
# Property: Should provide suggestions for new users
|
|
164
|
+
assert isinstance(suggestions, list), "Suggestions should be a list"
|
|
165
|
+
assert len(suggestions) > 0, "Should provide suggestions for new users"
|
|
166
|
+
|
|
167
|
+
# Property: All suggestions should be valid strings
|
|
168
|
+
for suggestion in suggestions:
|
|
169
|
+
assert isinstance(suggestion, str), "Each suggestion should be a string"
|
|
170
|
+
assert len(suggestion.strip()) > 0, "Suggestions should not be empty"
|
|
171
|
+
|
|
172
|
+
# Create user and complete some topics
|
|
173
|
+
self.progress_system.create_user_profile(user_id)
|
|
174
|
+
|
|
175
|
+
# Complete beginner topics
|
|
176
|
+
beginner_topics = ["variables", "data_types", "input_output"]
|
|
177
|
+
for topic in beginner_topics:
|
|
178
|
+
self.progress_system.update_progress(user_id, topic, True)
|
|
179
|
+
|
|
180
|
+
# Property: Suggestions should exclude completed topics
|
|
181
|
+
new_suggestions = self.progress_system.suggest_next_topics(user_id)
|
|
182
|
+
for completed_topic in beginner_topics:
|
|
183
|
+
assert completed_topic not in new_suggestions, f"Completed topic {completed_topic} should not be suggested"
|
|
184
|
+
|
|
185
|
+
@given(
|
|
186
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
|
|
187
|
+
achievements=st.lists(st.text(min_size=1, max_size=30).filter(lambda x: x.strip()), min_size=1, max_size=5)
|
|
188
|
+
)
|
|
189
|
+
def test_achievement_tracking_correctness(self, user_id, achievements):
|
|
190
|
+
"""
|
|
191
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
192
|
+
track achievements correctly.
|
|
193
|
+
|
|
194
|
+
**Validates: Requirements 6.5**
|
|
195
|
+
"""
|
|
196
|
+
assume(len(user_id.strip()) > 0)
|
|
197
|
+
assume(all(len(achievement.strip()) > 0 for achievement in achievements))
|
|
198
|
+
|
|
199
|
+
user_id = user_id.strip()
|
|
200
|
+
achievements = [achievement.strip() for achievement in achievements]
|
|
201
|
+
|
|
202
|
+
# Create user profile
|
|
203
|
+
self.progress_system.create_user_profile(user_id)
|
|
204
|
+
|
|
205
|
+
# Add achievements
|
|
206
|
+
for achievement in achievements:
|
|
207
|
+
self.progress_system.add_achievement(user_id, achievement)
|
|
208
|
+
|
|
209
|
+
# Property: Achievement should be added
|
|
210
|
+
progress = self.progress_system.get_progress(user_id)
|
|
211
|
+
assert achievement in progress.achievements, f"Achievement {achievement} should be tracked"
|
|
212
|
+
|
|
213
|
+
# Property: All achievements should be tracked
|
|
214
|
+
final_progress = self.progress_system.get_progress(user_id)
|
|
215
|
+
assert len(final_progress.achievements) == len(set(achievements)), "All unique achievements should be tracked"
|
|
216
|
+
|
|
217
|
+
# Property: Duplicate achievements should not be added
|
|
218
|
+
duplicate_achievement = achievements[0]
|
|
219
|
+
initial_count = len(final_progress.achievements)
|
|
220
|
+
self.progress_system.add_achievement(user_id, duplicate_achievement)
|
|
221
|
+
|
|
222
|
+
updated_progress = self.progress_system.get_progress(user_id)
|
|
223
|
+
assert len(updated_progress.achievements) == initial_count, "Duplicate achievements should not be added"
|
|
224
|
+
|
|
225
|
+
@given(
|
|
226
|
+
user_id=st.text(min_size=1, max_size=50).filter(lambda x: x.strip())
|
|
227
|
+
)
|
|
228
|
+
def test_level_progression_correctness(self, user_id):
|
|
229
|
+
"""
|
|
230
|
+
Property 6: For any user learning journey, the Learning_System should
|
|
231
|
+
handle level progression correctly.
|
|
232
|
+
|
|
233
|
+
**Validates: Requirements 6.4**
|
|
234
|
+
"""
|
|
235
|
+
assume(len(user_id.strip()) > 0)
|
|
236
|
+
|
|
237
|
+
user_id = user_id.strip()
|
|
238
|
+
|
|
239
|
+
# Create beginner user
|
|
240
|
+
self.progress_system.create_user_profile(user_id, DifficultyLevel.BEGINNER)
|
|
241
|
+
|
|
242
|
+
# Complete enough beginner topics to trigger level progression
|
|
243
|
+
beginner_topics = ["variables", "data_types", "input_output", "operators", "conditionals", "loops", "lists", "functions"]
|
|
244
|
+
|
|
245
|
+
for topic in beginner_topics:
|
|
246
|
+
self.progress_system.update_progress(user_id, topic, True)
|
|
247
|
+
|
|
248
|
+
# Property: User should progress to intermediate level
|
|
249
|
+
progress = self.progress_system.get_progress(user_id)
|
|
250
|
+
# Note: Level progression happens when 80% of topics are completed
|
|
251
|
+
# This might or might not trigger depending on the internal logic
|
|
252
|
+
|
|
253
|
+
# Property: Progress should be consistent
|
|
254
|
+
assert progress.current_level in [DifficultyLevel.BEGINNER, DifficultyLevel.INTERMEDIATE], "Level should be valid"
|
|
255
|
+
assert len(progress.completed_topics) == len(beginner_topics), "All topics should be tracked"
|
|
256
|
+
|
|
257
|
+
def test_error_handling_correctness(self):
|
|
258
|
+
"""
|
|
259
|
+
Property 6: Progress tracking should handle errors gracefully.
|
|
260
|
+
|
|
261
|
+
**Validates: Requirements 6.1, 6.2, 6.3**
|
|
262
|
+
"""
|
|
263
|
+
# Property: Invalid user IDs should be handled gracefully
|
|
264
|
+
assert self.progress_system.get_progress("") is None, "Empty user ID should return None"
|
|
265
|
+
assert self.progress_system.get_progress(None) is None, "None user ID should return None"
|
|
266
|
+
|
|
267
|
+
# Property: Invalid topic updates should raise appropriate errors
|
|
268
|
+
with pytest.raises(ValueError):
|
|
269
|
+
self.progress_system.update_progress("", "topic", True)
|
|
270
|
+
|
|
271
|
+
with pytest.raises(ValueError):
|
|
272
|
+
self.progress_system.update_progress("user", "", True)
|
|
273
|
+
|
|
274
|
+
# Property: System should handle missing storage gracefully
|
|
275
|
+
non_existent_path = "/non/existent/path/progress.json"
|
|
276
|
+
system_with_bad_path = ProgressSystem(storage_path=non_existent_path)
|
|
277
|
+
|
|
278
|
+
# Should not crash, just return None
|
|
279
|
+
assert system_with_bad_path.load_progress("test_user") is None, "Should handle missing storage gracefully"
|