ivoryos 1.2.5__py3-none-any.whl → 1.4.4__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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
tests/conftest.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
import bcrypt
|
|
4
|
+
import pytest
|
|
5
|
+
from ivoryos.config import get_config
|
|
6
|
+
|
|
7
|
+
from ivoryos import create_app, socketio, db as _db, utils, global_config
|
|
8
|
+
from ivoryos.utils.db_models import User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(scope='session')
|
|
12
|
+
def app():
|
|
13
|
+
"""Create a new app instance for the test session."""
|
|
14
|
+
_app = create_app(get_config('testing'))
|
|
15
|
+
return _app
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client(app):
|
|
19
|
+
"""A test client for the app."""
|
|
20
|
+
with app.test_client() as client:
|
|
21
|
+
with app.app_context():
|
|
22
|
+
_db.create_all()
|
|
23
|
+
yield client
|
|
24
|
+
with app.app_context():
|
|
25
|
+
_db.drop_all()
|
|
26
|
+
|
|
27
|
+
# @pytest.fixture(scope='session')
|
|
28
|
+
# def db(app):
|
|
29
|
+
# """Session-wide test database."""
|
|
30
|
+
# with app.app_context():
|
|
31
|
+
# _db.create_all()
|
|
32
|
+
# yield _db
|
|
33
|
+
# _db.drop_all()
|
|
34
|
+
|
|
35
|
+
@pytest.fixture(scope='module')
|
|
36
|
+
def init_database(app):
|
|
37
|
+
"""
|
|
38
|
+
Creates the database tables and seeds it with a default test user.
|
|
39
|
+
This runs once per test module.
|
|
40
|
+
"""
|
|
41
|
+
with app.app_context():
|
|
42
|
+
# Drop everything first to ensure a clean slate
|
|
43
|
+
_db.drop_all()
|
|
44
|
+
# Create the database tables
|
|
45
|
+
_db.create_all()
|
|
46
|
+
|
|
47
|
+
# Insert a default user for authentication tests
|
|
48
|
+
# Note: In a real app with password hashing, you'd call a hash function here.
|
|
49
|
+
password = 'password'
|
|
50
|
+
bcrypt_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
|
51
|
+
default_user = User(username='testuser', password=bcrypt_password)
|
|
52
|
+
_db.session.add(default_user)
|
|
53
|
+
_db.session.commit()
|
|
54
|
+
|
|
55
|
+
yield _db # this is where the testing happens!
|
|
56
|
+
|
|
57
|
+
# Teardown: drop all tables after the tests in the module are done
|
|
58
|
+
_db.drop_all()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------
|
|
62
|
+
# Authentication Fixture
|
|
63
|
+
# ---------------------
|
|
64
|
+
|
|
65
|
+
@pytest.fixture(scope='function')
|
|
66
|
+
def auth(client, init_database):
|
|
67
|
+
"""
|
|
68
|
+
Logs in the default user for a single test function.
|
|
69
|
+
Depends on `init_database` to ensure the user exists.
|
|
70
|
+
Handles logout as part of teardown.
|
|
71
|
+
"""
|
|
72
|
+
# Log in the default user
|
|
73
|
+
client.post('/ivoryos/auth/login', data={
|
|
74
|
+
'username': 'testuser',
|
|
75
|
+
'password': 'password'
|
|
76
|
+
}, follow_redirects=True)
|
|
77
|
+
|
|
78
|
+
yield client # this is where the testing happens!
|
|
79
|
+
|
|
80
|
+
# Log out the user after the test is done
|
|
81
|
+
client.get('/ivoryos/auth/logout', follow_redirects=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def socketio_client(app):
|
|
86
|
+
"""A test client for Socket.IO."""
|
|
87
|
+
return socketio.test_client(app)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestEnum(Enum):
|
|
91
|
+
"""An example Enum for testing type conversion."""
|
|
92
|
+
OPTION_A = 'A'
|
|
93
|
+
OPTION_B = 'B'
|
|
94
|
+
|
|
95
|
+
class DummyModule:
|
|
96
|
+
"""A more comprehensive dummy instrument for testing."""
|
|
97
|
+
def int_method(self, arg: int = 1):
|
|
98
|
+
return arg
|
|
99
|
+
|
|
100
|
+
def float_method(self, arg: float = 1.0):
|
|
101
|
+
return arg
|
|
102
|
+
|
|
103
|
+
def bool_method(self, arg: bool = False):
|
|
104
|
+
return arg
|
|
105
|
+
|
|
106
|
+
def list_method(self, arg: list = None):
|
|
107
|
+
return arg or []
|
|
108
|
+
|
|
109
|
+
def enum_method(self, arg: TestEnum = TestEnum.OPTION_A):
|
|
110
|
+
return arg
|
|
111
|
+
|
|
112
|
+
def str_method(self) -> dict:
|
|
113
|
+
return {'status': 'OK'}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.fixture
|
|
117
|
+
def test_deck(app):
|
|
118
|
+
"""
|
|
119
|
+
A fixture that creates and loads a predictable 'deck' of dummy instruments
|
|
120
|
+
for testing the dynamic control routes.
|
|
121
|
+
"""
|
|
122
|
+
dummy_module = DummyModule()
|
|
123
|
+
snapshot = utils.create_deck_snapshot(dummy_module)
|
|
124
|
+
|
|
125
|
+
with app.app_context():
|
|
126
|
+
global_config.deck_snapshot = snapshot
|
|
127
|
+
global_config.deck = dummy_module # instantiate the class
|
|
128
|
+
|
|
129
|
+
yield DummyModule
|
|
130
|
+
|
|
131
|
+
with app.app_context():
|
|
132
|
+
global_config.deck_snapshot = {}
|
|
133
|
+
global_config.deck = {}
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from ivoryos.utils.db_models import User, db
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_get_signup(client):
|
|
5
|
+
"""
|
|
6
|
+
GIVEN a client
|
|
7
|
+
WHEN a GET request is made to /ivoryos/auth/signup
|
|
8
|
+
THEN check that signup page loads with 200 status and contains "Signup" text
|
|
9
|
+
"""
|
|
10
|
+
response = client.get("/ivoryos/auth/signup", follow_redirects=True)
|
|
11
|
+
assert response.status_code == 200
|
|
12
|
+
assert b"Signup" in response.data
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_route_auth_signup(client):
|
|
16
|
+
"""
|
|
17
|
+
GIVEN a client
|
|
18
|
+
WHEN a POST request is made to /ivoryos/auth/signup with valid credentials
|
|
19
|
+
THEN check that signup succeeds with 200 status and the user is created in database
|
|
20
|
+
"""
|
|
21
|
+
response = client.post("/ivoryos/auth/signup",
|
|
22
|
+
data={
|
|
23
|
+
"username": "second_testuser",
|
|
24
|
+
"password": "password"
|
|
25
|
+
},
|
|
26
|
+
follow_redirects=True
|
|
27
|
+
)
|
|
28
|
+
assert response.status_code == 200
|
|
29
|
+
assert b"Login" in response.data
|
|
30
|
+
|
|
31
|
+
# Verify user was created
|
|
32
|
+
with client.application.app_context():
|
|
33
|
+
user = db.session.query(User).filter(User.username == 'second_testuser').first()
|
|
34
|
+
assert user is not None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_duplicate_user_signup(client, init_database):
|
|
38
|
+
"""
|
|
39
|
+
GIVEN a client and init_database fixture
|
|
40
|
+
WHEN a POST request is made to signup with an existing username
|
|
41
|
+
THEN check that signup fails with 409 status and appropriate error message
|
|
42
|
+
"""
|
|
43
|
+
client.post('/ivoryos/auth/signup', data={
|
|
44
|
+
'username': 'existinguser',
|
|
45
|
+
'password': 'anotherpass'
|
|
46
|
+
})
|
|
47
|
+
# Try to create duplicate
|
|
48
|
+
response = client.post('/ivoryos/auth/signup', data={
|
|
49
|
+
'username': 'existinguser',
|
|
50
|
+
'password': 'anotherpass'
|
|
51
|
+
})
|
|
52
|
+
assert response.status_code == 409
|
|
53
|
+
assert b"Signup" in response.data
|
|
54
|
+
assert b"User already exists" in response.data
|
|
55
|
+
|
|
56
|
+
# Verify user was created
|
|
57
|
+
users = db.session.query(User).filter(User.username == 'existinguser').all()
|
|
58
|
+
assert len(users) == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_failed_login(client):
|
|
62
|
+
"""
|
|
63
|
+
GIVEN a client and invalid login credentials
|
|
64
|
+
WHEN a POST request is made to /ivoryos/auth/login
|
|
65
|
+
THEN check that login fails with 401 status and the appropriate error message
|
|
66
|
+
"""
|
|
67
|
+
response = client.post('/ivoryos/auth/login', data={
|
|
68
|
+
'username': 'nonexistent',
|
|
69
|
+
'password': 'wrongpass'
|
|
70
|
+
})
|
|
71
|
+
assert response.status_code == 401
|
|
72
|
+
|
|
73
|
+
def test_logout(auth):
|
|
74
|
+
"""
|
|
75
|
+
GIVEN an authenticated client
|
|
76
|
+
WHEN a GET request is made to /ivoryos/auth/logout
|
|
77
|
+
THEN check that logout succeeds with 302 status and redirects to login
|
|
78
|
+
"""
|
|
79
|
+
response = auth.get('/ivoryos/auth/logout')
|
|
80
|
+
assert response.status_code == 302 # Redirect to login
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from unittest.mock import patch, Mock
|
|
2
|
+
|
|
3
|
+
from ivoryos.utils.db_models import Script
|
|
4
|
+
from ivoryos import db
|
|
5
|
+
|
|
6
|
+
def test_control_panel_redirects_anonymous(client):
|
|
7
|
+
"""
|
|
8
|
+
GIVEN an anonymous user
|
|
9
|
+
WHEN the control panel is accessed
|
|
10
|
+
THEN they should be redirected to the login page
|
|
11
|
+
"""
|
|
12
|
+
response = client.get('/ivoryos/control/home/deck', follow_redirects=True)
|
|
13
|
+
assert response.status_code == 200
|
|
14
|
+
assert b'Login' in response.data
|
|
15
|
+
|
|
16
|
+
def test_deck_control_for_auth_user(auth):
|
|
17
|
+
"""
|
|
18
|
+
GIVEN an authenticated user
|
|
19
|
+
WHEN the control panel is accessed
|
|
20
|
+
THEN the page should load successfully
|
|
21
|
+
"""
|
|
22
|
+
response = auth.get('/ivoryos/control/home/deck', follow_redirects=True)
|
|
23
|
+
assert response.status_code == 200
|
|
24
|
+
assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
|
|
25
|
+
|
|
26
|
+
def test_temp_control_for_auth_user(auth):
|
|
27
|
+
"""
|
|
28
|
+
GIVEN an authenticated user
|
|
29
|
+
WHEN the control panel is accessed
|
|
30
|
+
THEN the page should load successfully
|
|
31
|
+
"""
|
|
32
|
+
response = auth.get('/ivoryos/control/home/temp', follow_redirects=True)
|
|
33
|
+
assert response.status_code == 200
|
|
34
|
+
# assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
|
|
35
|
+
|
|
36
|
+
def test_new_controller_page(auth):
|
|
37
|
+
"""Test new controller page loads"""
|
|
38
|
+
response = auth.get('/ivoryos/control/new/')
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
|
|
41
|
+
def test_download_proxy(self, auth_headers):
|
|
42
|
+
"""Test proxy download functionality"""
|
|
43
|
+
with patch('ivoryos.routes.control.control.global_config') as mock_config:
|
|
44
|
+
mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
|
|
45
|
+
response = auth_headers.get('/ivoryos/control/download')
|
|
46
|
+
assert response.status_code == 200
|
|
47
|
+
assert response.headers['Content-Disposition'].startswith('attachment')
|
|
48
|
+
|
|
49
|
+
def test_backend_control_get(self, auth_headers):
|
|
50
|
+
"""Test backend control GET endpoint"""
|
|
51
|
+
with patch('ivoryos.routes.control.control.global_config') as mock_config:
|
|
52
|
+
mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
|
|
53
|
+
response = auth_headers.get('/ivoryos/api/control/')
|
|
54
|
+
assert response.status_code == 200
|
|
55
|
+
assert response.is_json
|
|
56
|
+
|
|
57
|
+
@patch('ivoryos.routes.control.control.runner')
|
|
58
|
+
@patch('ivoryos.routes.control.control.find_instrument_by_name')
|
|
59
|
+
@patch('ivoryos.routes.control.control.create_form_from_module')
|
|
60
|
+
def test_backend_control_post(self, mock_form, mock_find, mock_runner, auth_headers):
|
|
61
|
+
"""Test backend control POST endpoint"""
|
|
62
|
+
# Setup mocks
|
|
63
|
+
mock_instrument = Mock()
|
|
64
|
+
mock_find.return_value = mock_instrument
|
|
65
|
+
mock_field = Mock()
|
|
66
|
+
mock_field.name = 'test_param'
|
|
67
|
+
mock_field.data = 'test_value'
|
|
68
|
+
mock_form_instance = Mock()
|
|
69
|
+
mock_form_instance.__iter__ = Mock(return_value=iter([mock_field]))
|
|
70
|
+
mock_form.return_value = {'test_method': mock_form_instance}
|
|
71
|
+
mock_runner.run_single_step.return_value = 'success'
|
|
72
|
+
response = auth_headers.post('/ivoryos/api/control/test_instrument', data={
|
|
73
|
+
'hidden_name': 'test_method',
|
|
74
|
+
'hidden_wait': 'true'
|
|
75
|
+
})
|
|
76
|
+
assert response.status_code == 200
|
|
77
|
+
|
|
78
|
+
# def test_control(auth, app):
|
|
79
|
+
# """
|
|
80
|
+
# GIVEN an authenticated user and an existing script
|
|
81
|
+
# WHEN a POST request is made to run the script
|
|
82
|
+
# THEN the user should be redirected and a success message shown
|
|
83
|
+
# """
|
|
84
|
+
# # We need to create a script in the database first
|
|
85
|
+
# with app.app_context():
|
|
86
|
+
# script = Script(name='My Test Script', author='testuser', content='print("hello")')
|
|
87
|
+
# db.session.add(script)
|
|
88
|
+
# db.session.commit()
|
|
89
|
+
# script_id = script.id
|
|
90
|
+
#
|
|
91
|
+
# # Simulate running the script
|
|
92
|
+
# response = auth.post(f'/ivoryos/control/run/{script_id}', follow_redirects=True)
|
|
93
|
+
# assert response.status_code == 200
|
|
94
|
+
# assert b'has been initiated' in response.data # Check for a flash message
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep
|
|
4
|
+
from ivoryos import db
|
|
5
|
+
|
|
6
|
+
def test_database_scripts_page(auth):
|
|
7
|
+
"""
|
|
8
|
+
GIVEN an authenticated user
|
|
9
|
+
WHEN they access the script database page
|
|
10
|
+
THEN the page should load and show their scripts
|
|
11
|
+
"""
|
|
12
|
+
# First, create a script so the page has something to render
|
|
13
|
+
with auth.application.app_context():
|
|
14
|
+
script = Script(name='test_script', author='testuser')
|
|
15
|
+
db.session.add(script)
|
|
16
|
+
db.session.commit()
|
|
17
|
+
|
|
18
|
+
response = auth.get('/ivoryos/database/scripts/', follow_redirects=True)
|
|
19
|
+
assert response.status_code == 200
|
|
20
|
+
# assert b'Scripts Database' in response.data
|
|
21
|
+
assert b'<title>IvoryOS | Design Database</title>' in response.data
|
|
22
|
+
|
|
23
|
+
def test_database_workflows_page(auth):
|
|
24
|
+
"""
|
|
25
|
+
GIVEN an authenticated user
|
|
26
|
+
WHEN they access the workflow database page
|
|
27
|
+
THEN the page should load and show past workflow runs
|
|
28
|
+
"""
|
|
29
|
+
# Create a workflow run to display
|
|
30
|
+
with auth.application.app_context():
|
|
31
|
+
run = WorkflowRun(name="untitled", platform="deck",start_time=datetime.now())
|
|
32
|
+
db.session.add(run)
|
|
33
|
+
db.session.commit()
|
|
34
|
+
run_id = run.id
|
|
35
|
+
|
|
36
|
+
response = auth.get('/ivoryos/database/workflows/', follow_redirects=True)
|
|
37
|
+
assert response.status_code == 200
|
|
38
|
+
assert b'Workflow ID' in response.data
|
|
39
|
+
# assert b'run_id' in response.data
|
|
40
|
+
|
|
41
|
+
def test_view_specific_workflow(auth):
|
|
42
|
+
"""
|
|
43
|
+
GIVEN an authenticated user and an existing workflow run
|
|
44
|
+
WHEN they access the specific URL for that workflow
|
|
45
|
+
THEN the detailed view for that run should be displayed
|
|
46
|
+
"""
|
|
47
|
+
with auth.application.app_context():
|
|
48
|
+
run = WorkflowRun(name='test_workflow', platform='test_platform', start_time=datetime.now())
|
|
49
|
+
db.session.add(run)
|
|
50
|
+
db.session.commit()
|
|
51
|
+
run_id = run.id
|
|
52
|
+
|
|
53
|
+
step = WorkflowStep(method_name='test_step', workflow_id=run_id, phase="main", run_error=False, start_time=datetime.now())
|
|
54
|
+
db.session.add(step)
|
|
55
|
+
db.session.commit()
|
|
56
|
+
# run_id = run.id
|
|
57
|
+
|
|
58
|
+
response = auth.get(f'/ivoryos/database/workflows/{run_id}', follow_redirects=True)
|
|
59
|
+
assert response.status_code == 200
|
|
60
|
+
# assert b'test_step' in response.data # Check for a title on the view page
|
|
61
|
+
assert b'test_workflow' in response.data
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def test_design_page_loads_for_auth_user(auth):
|
|
2
|
+
"""
|
|
3
|
+
GIVEN an authenticated user
|
|
4
|
+
WHEN the design page is accessed
|
|
5
|
+
THEN the page should load successfully
|
|
6
|
+
"""
|
|
7
|
+
response = auth.get('/ivoryos/design/script/', follow_redirects=True)
|
|
8
|
+
assert response.status_code == 200
|
|
9
|
+
assert b'<title>IvoryOS | Design</title>' in response.data # Assuming this text exists
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_clear_canvas(auth):
|
|
13
|
+
"""
|
|
14
|
+
Tests clearing the design canvas.
|
|
15
|
+
"""
|
|
16
|
+
response = auth.get('/ivoryos/design/clear', follow_redirects=True)
|
|
17
|
+
assert response.status_code == 200
|
|
18
|
+
# assert b'Operations' in response.data
|
|
19
|
+
|
|
20
|
+
# def test_add_action(auth, test_deck):
|
|
21
|
+
# """
|
|
22
|
+
# Tests adding an action to the design canvas.
|
|
23
|
+
# """
|
|
24
|
+
# response = auth.post('/ivoryos/design/script/deck.dummy/', data={
|
|
25
|
+
# 'hidden_name': 'int_method',
|
|
26
|
+
# 'arg': '10'
|
|
27
|
+
# }, follow_redirects=True)
|
|
28
|
+
# assert response.status_code == 200
|
|
29
|
+
|
|
30
|
+
def test_experiment_run_page(auth):
|
|
31
|
+
"""
|
|
32
|
+
Tests the experiment run page.
|
|
33
|
+
"""
|
|
34
|
+
response = auth.get('/ivoryos/design/campaign')
|
|
35
|
+
assert response.status_code == 200
|
|
36
|
+
assert b'Run Panel' in response.data
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from flask_login import current_user
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_home_page_authenticated(auth, app):
|
|
5
|
+
"""
|
|
6
|
+
GIVEN an authenticated user (using the 'auth' fixture)
|
|
7
|
+
WHEN the home page is accessed
|
|
8
|
+
THEN check that they see the main application page
|
|
9
|
+
"""
|
|
10
|
+
with auth.application.test_request_context('/ivoryos/'):
|
|
11
|
+
# Manually trigger the before_request functions that Flask-Login uses
|
|
12
|
+
app.preprocess_request()
|
|
13
|
+
|
|
14
|
+
# Assert that the `current_user` proxy is now populated and authenticated
|
|
15
|
+
assert current_user.is_authenticated
|
|
16
|
+
assert current_user.username == 'testuser'
|
|
17
|
+
|
|
18
|
+
def test_help_page(client):
|
|
19
|
+
"""
|
|
20
|
+
GIVEN an unauthenticated user
|
|
21
|
+
WHEN they access the help page
|
|
22
|
+
THEN check that the page loads successfully and contains documentation content
|
|
23
|
+
"""
|
|
24
|
+
response = client.get('/ivoryos/help')
|
|
25
|
+
assert response.status_code == 200
|
|
26
|
+
assert b'Documentations' in response.data
|
|
27
|
+
|
|
28
|
+
def test_prefix_redirect(auth):
|
|
29
|
+
"""
|
|
30
|
+
GIVEN an authenticated user (using the 'auth' fixture)
|
|
31
|
+
WHEN the home page is accessed without prefix
|
|
32
|
+
THEN check that they see the main application page
|
|
33
|
+
"""
|
|
34
|
+
response = auth.get('/', follow_redirects=True)
|
|
35
|
+
assert response.status_code == 200
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
def test_socket_connection(socketio_client):
|
|
2
|
+
"""
|
|
3
|
+
Test that a client can successfully connect to the Socket.IO server.
|
|
4
|
+
"""
|
|
5
|
+
assert socketio_client.is_connected()
|
|
6
|
+
socketio_client.disconnect()
|
|
7
|
+
assert not socketio_client.is_connected()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# def test_logger_socket_event(socketio_client):
|
|
11
|
+
# """
|
|
12
|
+
# Test the custom logging event handler.
|
|
13
|
+
# (This assumes you have a handler like `@socketio.on('start_log')`)
|
|
14
|
+
# """
|
|
15
|
+
# # Connect the client
|
|
16
|
+
# socketio_client.connect()
|
|
17
|
+
#
|
|
18
|
+
# # Emit an event from the client to the server
|
|
19
|
+
# socketio_client.emit('start_log', {'logger_name': 'my_test_logger'})
|
|
20
|
+
#
|
|
21
|
+
# # Check what the server sent back to the client
|
|
22
|
+
# received = socketio_client.get_received()
|
|
23
|
+
#
|
|
24
|
+
# assert len(received) > 0
|
|
25
|
+
# assert received[0]['name'] == 'log_message' # Check for the event name
|
|
26
|
+
# assert 'Logger my_test_logger started' in received[0]['args'][0]['data']
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from tests.conftest import TestEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_int_conversion(auth, test_deck):
|
|
7
|
+
"""Tests that a string from a form is converted to an integer."""
|
|
8
|
+
with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.int_method') as mock_method:
|
|
9
|
+
auth.post('/ivoryos/control/deck.dummy/call/int_method', data={'arg': '123'})
|
|
10
|
+
# Check that the mock was called with an integer
|
|
11
|
+
mock_method.assert_called_with(arg=123)
|
|
12
|
+
|
|
13
|
+
def test_float_conversion(auth, test_deck):
|
|
14
|
+
"""Tests that a string from a form is converted to a float."""
|
|
15
|
+
with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.float_method') as mock_method:
|
|
16
|
+
auth.post('/ivoryos/control/deck.dummy/call/float_method', data={'arg': '123.45'})
|
|
17
|
+
# Check that the mock was called with a float
|
|
18
|
+
mock_method.assert_called_with(arg=123.45)
|
|
19
|
+
|
|
20
|
+
def test_bool_conversion(auth, test_deck):
|
|
21
|
+
"""Tests that a string from a form is converted to a boolean."""
|
|
22
|
+
with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.bool_method') as mock_method:
|
|
23
|
+
# Test with 'true'
|
|
24
|
+
auth.post('/ivoryos/control/deck.dummy/call/bool_method', data={'arg': 'true'})
|
|
25
|
+
mock_method.assert_called_with(arg=True)
|
|
26
|
+
# Test with 'false'
|
|
27
|
+
auth.post('/ivoryos/control/deck.dummy/call/bool_method', data={'arg': 'false'})
|
|
28
|
+
mock_method.assert_called_with(arg=False)
|
|
29
|
+
|
|
30
|
+
def test_list_conversion(auth, test_deck):
|
|
31
|
+
"""Tests that a comma-separated string from a form is converted to a list."""
|
|
32
|
+
with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.list_method') as mock_method:
|
|
33
|
+
auth.post('/ivoryos/control/deck.dummy/call/list_method', data={'arg': 'a,b,c'})
|
|
34
|
+
# Check that the mock was called with a list of strings
|
|
35
|
+
mock_method.assert_called_with(arg=['a', 'b', 'c'])
|
|
36
|
+
|
|
37
|
+
def test_enum_conversion(auth, test_deck):
|
|
38
|
+
"""Tests that a string from a form is converted to an Enum member."""
|
|
39
|
+
with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.enum_method') as mock_method:
|
|
40
|
+
auth.post('/ivoryos/control/deck.dummy/call/enum_method', data={'arg': 'OPTION_B'})
|
|
41
|
+
# Check that the mock was called with the correct Enum member
|
|
42
|
+
mock_method.assert_called_with(arg=TestEnum.OPTION_B)
|
tests/unit/test_util.py
ADDED
ivoryos/routes/api/api.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from flask import Blueprint, jsonify, request, current_app
|
|
3
|
-
|
|
4
|
-
from ivoryos.routes.control.control import find_instrument_by_name
|
|
5
|
-
from ivoryos.utils.form import create_form_from_module
|
|
6
|
-
from ivoryos.utils.global_config import GlobalConfig
|
|
7
|
-
from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
|
|
8
|
-
|
|
9
|
-
from ivoryos.socket_handlers import abort_pending, abort_current, pause, retry, runner
|
|
10
|
-
from ivoryos.utils.task_runner import TaskRunner
|
|
11
|
-
|
|
12
|
-
api = Blueprint('api', __name__)
|
|
13
|
-
global_config = GlobalConfig()
|
|
14
|
-
task_runner = TaskRunner()
|
|
15
|
-
|
|
16
|
-
#TODO: add authentication and authorization to the API endpoints
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@api.route("/control/", strict_slashes=False, methods=['GET'])
|
|
20
|
-
@api.route("/control/<string:instrument>", methods=['POST'])
|
|
21
|
-
def backend_control(instrument: str=None):
|
|
22
|
-
"""
|
|
23
|
-
.. :quickref: Backend Control; backend control
|
|
24
|
-
|
|
25
|
-
backend control through http requests
|
|
26
|
-
|
|
27
|
-
.. http:get:: /api/control/
|
|
28
|
-
|
|
29
|
-
:param instrument: instrument name
|
|
30
|
-
:type instrument: str
|
|
31
|
-
|
|
32
|
-
.. http:post:: /api/control/
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
if instrument:
|
|
36
|
-
inst_object = find_instrument_by_name(instrument)
|
|
37
|
-
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
38
|
-
|
|
39
|
-
if request.method == 'POST':
|
|
40
|
-
method_name = request.form.get("hidden_name", None)
|
|
41
|
-
form = forms.get(method_name, None)
|
|
42
|
-
if form:
|
|
43
|
-
kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
|
|
44
|
-
wait = request.form.get("hidden_wait", "true") == "true"
|
|
45
|
-
output = task_runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
46
|
-
current_app=current_app._get_current_object())
|
|
47
|
-
return jsonify(output), 200
|
|
48
|
-
|
|
49
|
-
snapshot = global_config.deck_snapshot.copy()
|
|
50
|
-
# Iterate through each instrument in the snapshot
|
|
51
|
-
for instrument_key, instrument_data in snapshot.items():
|
|
52
|
-
# Iterate through each function associated with the current instrument
|
|
53
|
-
for function_key, function_data in instrument_data.items():
|
|
54
|
-
# Convert the function signature to a string representation
|
|
55
|
-
function_data['signature'] = str(function_data['signature'])
|
|
56
|
-
return jsonify(snapshot), 200
|