Skip to content

Commit 5015771

Browse files
Merge pull request #36 from osmo-tool/op
Model creator improvements
2 parents 0047749 + ed2844a commit 5015771

6 files changed

Lines changed: 216 additions & 126 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ target/
6565
pyosmo/config.py.bak
6666
.pytest_cache
6767

68-
.hypothesis
68+
.hypothesis
69+
models

model-creator/README.md

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,13 @@ def after(self):
116116

117117
### Step Methods (Actions)
118118

119+
The generated models use PyOsmo's decorator syntax for steps and guards:
120+
119121
Form submission:
120122
```python
121-
def step_submit_login(self):
123+
@step
124+
@guard(lambda self: not self.logged_in)
125+
def submit_login(self):
122126
"""Submit the login form."""
123127
data = {
124128
"username": "testuser",
@@ -136,31 +140,73 @@ def step_submit_login(self):
136140

137141
Navigation:
138142
```python
139-
def step_navigate_to_about(self):
143+
@step
144+
def navigate_to_about(self):
140145
"""Navigate to about page."""
141146
self.response = self.session.get("https://example.com/about")
142147
self.current_page = "about"
143148
print("Navigated to: about")
144149
```
145150

146-
### Guard Methods (Preconditions)
151+
Steps with authentication requirements:
152+
```python
153+
@step
154+
@guard(lambda self: self.logged_in)
155+
def navigate_to_dashboard(self):
156+
"""Navigate to dashboard (requires login)."""
157+
self.response = self.session.get("https://example.com/dashboard")
158+
self.current_page = "dashboard"
159+
print("Navigated to: dashboard")
160+
```
161+
162+
### Main Function
147163

164+
Generated models include a `main()` function for easy execution:
148165
```python
149-
def guard_submit_login(self):
150-
"""Guard for login - can only login when not logged in."""
151-
return not self.logged_in
166+
def main():
167+
"""Run the model with PyOsmo."""
168+
model = WebsiteModel()
169+
170+
osmo = (
171+
Osmo(model)
172+
.weighted_algorithm()
173+
.stop_after_steps(100)
174+
.run_tests(1)
175+
)
176+
177+
print(f"Starting test generation for {model.base_url}")
178+
osmo.generate()
179+
180+
# Print summary
181+
stats = osmo.history.statistics()
182+
print("\n" + "=" * 50)
183+
print("Test generation complete!")
184+
print(f"Steps executed: {stats.total_steps}")
185+
print(f"Unique steps: {len(stats.step_coverage)}")
186+
152187

153-
def guard_submit_logout(self):
154-
"""Guard for logout - can only logout when logged in."""
155-
return self.logged_in
188+
if __name__ == "__main__":
189+
main()
190+
```
191+
192+
You can run the generated model directly:
193+
```bash
194+
python models/example_model.py
156195
```
157196

158197
## Running Generated Models
159198

160-
Once you have a generated model, you can run it with PyOsmo:
199+
Generated models include a `main()` function that can be run directly:
200+
201+
```bash
202+
# Run the model directly (uses built-in defaults)
203+
python models/example_model.py
204+
```
205+
206+
Or use PyOsmo's CLI for more control:
161207

162208
```bash
163-
# Run model exploration
209+
# Run model exploration with custom settings
164210
python -m osmo.explorer -m models/example_model.py:WebsiteModel
165211

166212
# Run with specific seed for reproducibility
@@ -268,10 +314,12 @@ Generated models can be customized after creation:
268314

269315
1. **Add custom verification logic** in the `after()` method
270316
2. **Customize default values** in form submission steps
271-
3. **Add weights** to control action frequency:
317+
3. **Add weights** to control action frequency using the `weight_value` parameter:
272318
```python
273-
def weight_submit_login(self):
274-
return 5 # Higher weight = more frequent
319+
@step(weight_value=5) # Higher weight = more frequent
320+
@guard(lambda self: not self.logged_in)
321+
def submit_login(self):
322+
...
275323
```
276324
4. **Add invariants** for state checking
277325
5. **Customize guards** for complex preconditions

model-creator/examples/example_generated_model.py

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
except ImportError:
1212
raise ImportError('Model requires requests library. Install with: pip install requests')
1313

14+
from pyosmo import Osmo
15+
from pyosmo.decorators import guard, step
16+
1417

1518
class ExampleWebsiteModel:
1619
"""
@@ -50,14 +53,15 @@ def after(self):
5053
# Verify response is valid
5154
assert self.response.status_code < 500, f'Server error: {self.response.status_code}'
5255

53-
# Check for error messages if we expect success
56+
# Could add more sophisticated error checking here
5457
if self.response.status_code == 200:
55-
# Could add more sophisticated error checking here
5658
pass
5759

5860
# --- Form Submission Actions ---
5961

60-
def step_submit_login(self):
62+
@step
63+
@guard(lambda self: not self.logged_in)
64+
def submit_login(self):
6165
"""Submit the login form."""
6266
data = {
6367
'username': 'testuser',
@@ -77,12 +81,9 @@ def step_submit_login(self):
7781
self.current_page = 'login'
7882
print('Executed: submit_login')
7983

80-
def guard_submit_login(self):
81-
"""Guard for submit_login."""
82-
# Can only login when not logged in
83-
return not self.logged_in
84-
85-
def step_submit_register(self):
84+
@step
85+
@guard(lambda self: not self.logged_in)
86+
def submit_register(self):
8687
"""Submit the registration form."""
8788
data = {
8889
'username': 'testuser',
@@ -99,12 +100,9 @@ def step_submit_register(self):
99100
self.current_page = 'register'
100101
print('Executed: submit_register')
101102

102-
def guard_submit_register(self):
103-
"""Guard for submit_register."""
104-
# Can only register when not logged in
105-
return not self.logged_in
106-
107-
def step_submit_logout(self):
103+
@step
104+
@guard(lambda self: self.logged_in)
105+
def submit_logout(self):
108106
"""Submit the logout form."""
109107
data = {}
110108

@@ -119,12 +117,8 @@ def step_submit_logout(self):
119117
self.current_page = 'logout'
120118
print('Executed: submit_logout')
121119

122-
def guard_submit_logout(self):
123-
"""Guard for submit_logout."""
124-
# Can only logout when logged in
125-
return self.logged_in
126-
127-
def step_submit_contact(self):
120+
@step
121+
def submit_contact(self):
128122
"""Submit the contact form."""
129123
data = {
130124
'name': 'Test User',
@@ -140,11 +134,8 @@ def step_submit_contact(self):
140134
self.current_page = 'contact'
141135
print('Executed: submit_contact')
142136

143-
def guard_submit_contact(self):
144-
"""Guard for submit_contact."""
145-
return True
146-
147-
def step_submit_search(self):
137+
@step
138+
def submit_search(self):
148139
"""Submit the search form."""
149140
data = {
150141
'q': 'test_q',
@@ -158,70 +149,62 @@ def step_submit_search(self):
158149
self.current_page = 'search'
159150
print('Executed: submit_search')
160151

161-
def guard_submit_search(self):
162-
"""Guard for submit_search."""
163-
return True
164-
165152
# --- Navigation Actions ---
166153

167-
def step_navigate_to_home(self):
154+
@step
155+
def navigate_to_home(self):
168156
"""Navigate to home."""
169157
self.response = self.session.get('https://example.com')
170158
self.current_page = 'home'
171159
print('Navigated to: home')
172160

173-
def guard_navigate_to_home(self):
174-
"""Guard for navigate_to_home."""
175-
return True
176-
177-
def step_navigate_to_about(self):
161+
@step
162+
def navigate_to_about(self):
178163
"""Navigate to about."""
179164
self.response = self.session.get('https://example.com/about')
180165
self.current_page = 'about'
181166
print('Navigated to: about')
182167

183-
def guard_navigate_to_about(self):
184-
"""Guard for navigate_to_about."""
185-
return True
186-
187-
def step_navigate_to_contact(self):
168+
@step
169+
def navigate_to_contact(self):
188170
"""Navigate to contact."""
189171
self.response = self.session.get('https://example.com/contact')
190172
self.current_page = 'contact'
191173
print('Navigated to: contact')
192174

193-
def guard_navigate_to_contact(self):
194-
"""Guard for navigate_to_contact."""
195-
return True
196-
197-
def step_navigate_to_login(self):
175+
@step
176+
@guard(lambda self: not self.logged_in)
177+
def navigate_to_login(self):
198178
"""Navigate to login."""
199179
self.response = self.session.get('https://example.com/login')
200180
self.current_page = 'login'
201181
print('Navigated to: login')
202182

203-
def guard_navigate_to_login(self):
204-
"""Guard for navigate_to_login."""
205-
# Only show login page when not logged in
206-
return not self.logged_in
207-
208-
def step_navigate_to_dashboard(self):
183+
@step
184+
@guard(lambda self: self.logged_in)
185+
def navigate_to_dashboard(self):
209186
"""Navigate to dashboard."""
210187
self.response = self.session.get('https://example.com/dashboard')
211188
self.current_page = 'dashboard'
212189
print('Navigated to: dashboard')
213190

214-
def guard_navigate_to_dashboard(self):
215-
"""Guard for navigate_to_dashboard."""
216-
# Dashboard requires authentication
217-
return self.logged_in
218191

219-
# --- Optional: Custom Methods ---
192+
def main():
193+
"""Run the model with PyOsmo."""
194+
model = ExampleWebsiteModel()
195+
196+
osmo = Osmo(model).weighted_algorithm().stop_after_steps(100).run_tests(1)
197+
198+
print(f'Starting test generation for {model.base_url}')
199+
osmo.generate()
200+
201+
# Print summary
202+
stats = osmo.history.statistics()
203+
print('\n' + '=' * 50)
204+
print('Test generation complete!')
205+
print(f'Steps executed: {stats.total_steps}')
206+
print(f'Unique steps: {len(stats.step_coverage)}')
220207

221-
def weight_submit_login(self):
222-
"""Make login more likely when not logged in."""
223-
return 10 if not self.logged_in else 1
224208

225-
def weight_navigate_to_dashboard(self):
226-
"""Dashboard is important - visit it more often when logged in."""
227-
return 15 if self.logged_in else 0
209+
if __name__ == '__main__':
210+
main()

0 commit comments

Comments
 (0)