Spaces:
Sleeping
Sleeping
rmm
commited on
Commit
·
d938857
1
Parent(s):
1b9a931
test: end-to-end test of upload -> inference
Browse files- a few steps need tweaking but reasonable start
- lots of verbose output to clean up!
- tests/test_main.py +294 -0
tests/test_main.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from unittest.mock import MagicMock, patch
|
| 3 |
+
from streamlit.testing.v1 import AppTest
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
from input.input_handling import spoof_metadata
|
| 7 |
+
from input.input_observation import InputObservation
|
| 8 |
+
from input.input_handling import buffer_uploaded_files
|
| 9 |
+
|
| 10 |
+
from streamlit.runtime.uploaded_file_manager import UploadedFile
|
| 11 |
+
from numpy import ndarray
|
| 12 |
+
|
| 13 |
+
from test_demo_multifile_upload import (
|
| 14 |
+
mock_uploadedFile_List_ImageData, mock_uploadedFile,
|
| 15 |
+
MockUploadedFile, )
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
from test_demo_input_sidebar import (
|
| 19 |
+
verify_initial_session_state, verify_session_state_after_processing_files,
|
| 20 |
+
wrapped_buffer_uploaded_files_allowed_once)
|
| 21 |
+
|
| 22 |
+
from test_demo_input_sidebar import _cprint, OKBLUE, OKGREEN, OKCYAN, FAIL, ENDC
|
| 23 |
+
|
| 24 |
+
TIMEOUT = 15
|
| 25 |
+
SCRIPT_UNDER_TEST = "src/main.py"
|
| 26 |
+
|
| 27 |
+
def debug_check_images(at:AppTest, msg:str=""):
|
| 28 |
+
_cprint(f"[I] num images in session state {msg}: {len(at.session_state.images)}", OKCYAN)
|
| 29 |
+
for i, (key, img) in enumerate(at.session_state.images.items()):
|
| 30 |
+
#for i, img in enumerate(at.session_state.images.values()):
|
| 31 |
+
#assert isinstance(img, ndarray)
|
| 32 |
+
if isinstance(img, ndarray):
|
| 33 |
+
print(f"image {i}: {img.shape} [{key}]")
|
| 34 |
+
else:
|
| 35 |
+
print(f"image {i}: {type(img)} [{key}]")
|
| 36 |
+
|
| 37 |
+
def nooop():
|
| 38 |
+
_cprint("skipping the buffering -- shoul only happen once", FAIL)
|
| 39 |
+
raise RuntimeError
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
@patch("streamlit.file_uploader")
|
| 43 |
+
def test_click_validate_after_data_entry(mock_file_rv: MagicMock, mock_uploadedFile_List_ImageData):
|
| 44 |
+
# this test goes through several stages of the workflow
|
| 45 |
+
#
|
| 46 |
+
|
| 47 |
+
# 1. get app started
|
| 48 |
+
|
| 49 |
+
# first we need to upload >0 files
|
| 50 |
+
num_files = 2
|
| 51 |
+
mock_files = mock_uploadedFile_List_ImageData(num_files=num_files)
|
| 52 |
+
mock_file_rv.return_value = mock_files
|
| 53 |
+
|
| 54 |
+
t0 = time.time()
|
| 55 |
+
at = AppTest.from_file(SCRIPT_UNDER_TEST, default_timeout=TIMEOUT).run()
|
| 56 |
+
t1 = time.time()
|
| 57 |
+
_cprint(f"[T] time to load: {t1-t0}", OKCYAN)
|
| 58 |
+
verify_initial_session_state(at)
|
| 59 |
+
|
| 60 |
+
# 1-Test: at this initial state, we expect:
|
| 61 |
+
# - the workflow state is 'doing_data_entry'
|
| 62 |
+
# - the validate button is disabled
|
| 63 |
+
# - the infer button (on main tab) is disabled
|
| 64 |
+
# - note: props of the button: label, value, proto, disabled.
|
| 65 |
+
# don't need to check others here
|
| 66 |
+
|
| 67 |
+
assert at.session_state.workflow_fsm.current_state == 'doing_data_entry'
|
| 68 |
+
assert at.sidebar.button[1].disabled == True
|
| 69 |
+
infer_button = at.tabs[0].button[0]
|
| 70 |
+
assert infer_button.disabled == True
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# 2. upload files, and trigger the callback
|
| 74 |
+
|
| 75 |
+
# put the mocked file_upload into session state, as if it were the result of a file upload, with the key 'file_uploader_data'
|
| 76 |
+
at.session_state["file_uploader_data"] = mock_files
|
| 77 |
+
# the side effect cant run until now (need file_uploader_data to be set)
|
| 78 |
+
if wrapped_buffer_uploaded_files_allowed_once.called == 0:
|
| 79 |
+
mock_file_rv.side_effect = wrapped_buffer_uploaded_files_allowed_once
|
| 80 |
+
else:
|
| 81 |
+
mock_file_rv.side_effect = nooop
|
| 82 |
+
|
| 83 |
+
_cprint(f"[I] buffering called {wrapped_buffer_uploaded_files_allowed_once.called} times", OKGREEN)
|
| 84 |
+
|
| 85 |
+
t2 = time.time()
|
| 86 |
+
at.run()
|
| 87 |
+
t3 = time.time()
|
| 88 |
+
_cprint(f"[T] time to run with file processing: {t3-t2}", OKCYAN)
|
| 89 |
+
|
| 90 |
+
# 2-Test: after uploading the files, we should have:
|
| 91 |
+
# - the workflow state moved on to 'data_entry_complete'
|
| 92 |
+
# - several changes applied to the session_state (handled by verify_session_state_after_processing_files)
|
| 93 |
+
# - the validate button is enabled
|
| 94 |
+
# - the infer button is still disabled
|
| 95 |
+
|
| 96 |
+
verify_session_state_after_processing_files(at, num_files)
|
| 97 |
+
debug_check_images(at, "after processing files")
|
| 98 |
+
_cprint(f"[I] buffering called {wrapped_buffer_uploaded_files_allowed_once.called} times", OKGREEN)
|
| 99 |
+
|
| 100 |
+
assert at.session_state.workflow_fsm.current_state == 'data_entry_complete'
|
| 101 |
+
|
| 102 |
+
assert at.sidebar.button[1].disabled == False
|
| 103 |
+
infer_button = at.tabs[0].button[0]
|
| 104 |
+
assert infer_button.disabled == True
|
| 105 |
+
|
| 106 |
+
print(at.markdown[0])
|
| 107 |
+
|
| 108 |
+
# 3. data entry complete, click the validate button
|
| 109 |
+
at.sidebar.button[1].click().run()
|
| 110 |
+
t4 = time.time()
|
| 111 |
+
_cprint(f"[T] time to run step 3: {t4-t3}", OKCYAN)
|
| 112 |
+
|
| 113 |
+
# 3-Test: after validating the data, we should have:
|
| 114 |
+
# - the state (backend) should move to data_entry_validated
|
| 115 |
+
# - the UI should show the new state (in sidebar.markdown[0])
|
| 116 |
+
# - the infer button should now be enabled
|
| 117 |
+
# - the validate button should be disabled
|
| 118 |
+
|
| 119 |
+
assert at.session_state.workflow_fsm.current_state == 'data_entry_validated'
|
| 120 |
+
assert "data_entry_validated" in at.sidebar.markdown[0].value
|
| 121 |
+
|
| 122 |
+
# TODO: this part of the test currently fails because hte main code doesn't
|
| 123 |
+
# change the button; in this exec path/branch, the button is not rendered at all.
|
| 124 |
+
# so if we did at.run() after the click, the button is absent entierly!
|
| 125 |
+
# If we don't run, the button is still present in its old state (enabled)
|
| 126 |
+
# for btn in at.sidebar.button:
|
| 127 |
+
# print(f"button: {btn.label} {btn.disabled}")
|
| 128 |
+
# #assert at.sidebar.button[1].disabled == True
|
| 129 |
+
|
| 130 |
+
infer_button = at.tabs[0].button[0]
|
| 131 |
+
assert infer_button.disabled == False
|
| 132 |
+
|
| 133 |
+
debug_check_images(at, "after validation button")
|
| 134 |
+
_cprint(f"[I] buffering called {wrapped_buffer_uploaded_files_allowed_once.called} times", OKGREEN)
|
| 135 |
+
|
| 136 |
+
# # at this point, we want to retrieve the main area, get the tabs child,
|
| 137 |
+
# # and then on the first tab get the first button & check not disabled (will click next step)
|
| 138 |
+
# #print(at._tree)
|
| 139 |
+
# # fragile: assume the first child is 'main'
|
| 140 |
+
# # robust: walk through children until we find the main area
|
| 141 |
+
# # main_area = at._tree.children[0]
|
| 142 |
+
# # main_area = None
|
| 143 |
+
# # for _id, child in at._tree.children.items():
|
| 144 |
+
# # if child.type == 'main':
|
| 145 |
+
# # main_area = child
|
| 146 |
+
# # break
|
| 147 |
+
# # assert main_area is not None
|
| 148 |
+
|
| 149 |
+
# # ah, we can go direct to the tabs. they are only plausible in main. (not supported in sidebar)
|
| 150 |
+
# infer_tab = at.tabs[0]
|
| 151 |
+
# #print(f"tab: {infer_tab}")
|
| 152 |
+
# #print(dir(infer_tab))
|
| 153 |
+
# btn = infer_tab.button[0]
|
| 154 |
+
# print(f"button: {btn}")
|
| 155 |
+
# print(btn.label)
|
| 156 |
+
# print(btn.disabled)
|
| 157 |
+
|
| 158 |
+
# infer_button = at.tabs[0].button[0]
|
| 159 |
+
# assert infer_button.disabled == False
|
| 160 |
+
|
| 161 |
+
# check pre-ML click that we are ready for it.
|
| 162 |
+
|
| 163 |
+
debug_check_images(at, "before clicking infer. ")
|
| 164 |
+
_cprint(f"[I] buffering called {wrapped_buffer_uploaded_files_allowed_once.called} times", OKGREEN)
|
| 165 |
+
TEST_ML = True
|
| 166 |
+
SKIP_CHECK_OVERRIDE = True
|
| 167 |
+
# 4. launch ML inference by clicking the button
|
| 168 |
+
if TEST_ML:
|
| 169 |
+
# infer_button = at.tabs[0].button[0]
|
| 170 |
+
# assert infer_button.disabled == False
|
| 171 |
+
# now test the ML step
|
| 172 |
+
infer_button.click().run()
|
| 173 |
+
t5 = time.time()
|
| 174 |
+
_cprint(f"[T] time to run with step 4: {t5-t4}", OKCYAN)
|
| 175 |
+
|
| 176 |
+
# 4-Test: after clicking the infer button, we should have:
|
| 177 |
+
# - workflow should have moved on to 'ml_classification_completed'
|
| 178 |
+
# - the main tab button should now have new text (confirm species predictions)
|
| 179 |
+
# - we should have the results presented on the main area
|
| 180 |
+
# - 2+6 image elements (the source image, images of 3 predictions) * num_files
|
| 181 |
+
# - 2 dropdown elements (one for each image) + 1 for the page selector
|
| 182 |
+
# - all of the observations should have class_overriden == False
|
| 183 |
+
|
| 184 |
+
assert at.session_state.workflow_fsm.current_state == 'ml_classification_completed'
|
| 185 |
+
# check the observations
|
| 186 |
+
for i, obs in enumerate(at.session_state.observations.values()):
|
| 187 |
+
print(f"obs {i}: {obs}")
|
| 188 |
+
assert isinstance(obs, InputObservation)
|
| 189 |
+
assert obs.class_overriden == False
|
| 190 |
+
|
| 191 |
+
# check the visual elements
|
| 192 |
+
infer_tab = at.tabs[0]
|
| 193 |
+
print(f"tab: {infer_tab}")
|
| 194 |
+
img_elems = infer_tab.get("imgs")
|
| 195 |
+
print(f"imgs: {len(img_elems)}")
|
| 196 |
+
assert len(img_elems) == num_files*4
|
| 197 |
+
|
| 198 |
+
infer_button = infer_tab.button[0]
|
| 199 |
+
assert infer_button.disabled == False
|
| 200 |
+
assert 'Confirm species predictions' in infer_button.label
|
| 201 |
+
|
| 202 |
+
# we have 1 per file, and also one more to select the page of results being shown.
|
| 203 |
+
# - hmm, so we aren't going to see the right number if it goes multipage :(
|
| 204 |
+
# - but this test specifically uses 2 inputs.
|
| 205 |
+
assert len(infer_tab.selectbox) == num_files + 1
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# 5. manually override the class of one of the observations
|
| 209 |
+
idx_to_override = 1
|
| 210 |
+
infer_tab.selectbox[idx_to_override].select_index(20).run() # FRAGILE!
|
| 211 |
+
|
| 212 |
+
# 5-TEST.
|
| 213 |
+
# - expect that all class_overriden are False, except for the one we just set
|
| 214 |
+
# - also expect there still to be num_files*4 images (2+6 per file) etc
|
| 215 |
+
for i, obs in enumerate(at.session_state.observations.values()):
|
| 216 |
+
print(f"obs {i}: {obs.class_overriden} {obs.to_dict()}")
|
| 217 |
+
assert isinstance(obs, InputObservation)
|
| 218 |
+
if not SKIP_CHECK_OVERRIDE:
|
| 219 |
+
if i == idx_to_override:
|
| 220 |
+
assert obs.class_overriden == True
|
| 221 |
+
else:
|
| 222 |
+
assert obs.class_overriden == False
|
| 223 |
+
|
| 224 |
+
# 6. confirm the species predictions, get ready to allow upload
|
| 225 |
+
infer_tab = at.tabs[0]
|
| 226 |
+
confirm_button = infer_tab.button[0]
|
| 227 |
+
confirm_button.click().run()
|
| 228 |
+
t6 = time.time()
|
| 229 |
+
_cprint(f"[T] time to run with step 6: {t6-t5}", OKCYAN)
|
| 230 |
+
|
| 231 |
+
# 6-TEST. Now we expect to see:
|
| 232 |
+
# - the workflow state should be 'manual_inspection_completed'
|
| 233 |
+
# - the obsevations should be as per the previous step
|
| 234 |
+
# - the main tab button should now have new text (Upload all observations)
|
| 235 |
+
# - we should have 4n images
|
| 236 |
+
# - we should have only 1 select box (page), (passed stage for overriding class)
|
| 237 |
+
|
| 238 |
+
assert at.session_state.workflow_fsm.current_state == 'manual_inspection_completed'
|
| 239 |
+
for i, obs in enumerate(at.session_state.observations.values()):
|
| 240 |
+
print(f"obs {i}: {obs.class_overriden} {obs.to_dict()}")
|
| 241 |
+
assert isinstance(obs, InputObservation)
|
| 242 |
+
if not SKIP_CHECK_OVERRIDE:
|
| 243 |
+
if i == idx_to_override:
|
| 244 |
+
assert obs.class_overriden == True
|
| 245 |
+
else:
|
| 246 |
+
assert obs.class_overriden == False
|
| 247 |
+
|
| 248 |
+
# we have to trigger a manual refresh?
|
| 249 |
+
at.run()
|
| 250 |
+
infer_tab = at.tabs[0]
|
| 251 |
+
upload_button = infer_tab.button[0]
|
| 252 |
+
assert upload_button.disabled == False
|
| 253 |
+
assert 'Upload all observations' in upload_button.label
|
| 254 |
+
|
| 255 |
+
img_elems = infer_tab.get("imgs")
|
| 256 |
+
assert len(img_elems) == num_files*4
|
| 257 |
+
|
| 258 |
+
assert len(infer_tab.selectbox) == 1
|
| 259 |
+
|
| 260 |
+
# 7. upload the observations
|
| 261 |
+
upload_button.click().run()
|
| 262 |
+
t7 = time.time()
|
| 263 |
+
_cprint(f"[T] time to run with step 7: {t7-t6}", OKCYAN)
|
| 264 |
+
|
| 265 |
+
# 7-TEST. Now we expect to see:
|
| 266 |
+
# - workflow state should be 'data_uploaded'
|
| 267 |
+
# - nothing else in the back end should have changed (is that a mistake? should we
|
| 268 |
+
# add a boolean tracking if the observations have been uploaded?)
|
| 269 |
+
# - a toast presented for each observation uploaded
|
| 270 |
+
# - the images should still be there, and 1 select box (page)
|
| 271 |
+
# - no more button on the main area
|
| 272 |
+
|
| 273 |
+
assert at.session_state.workflow_fsm.current_state == 'data_uploaded'
|
| 274 |
+
|
| 275 |
+
print(at.toast)
|
| 276 |
+
assert len(at.toast) == num_files
|
| 277 |
+
|
| 278 |
+
img_elems = infer_tab.get("imgs")
|
| 279 |
+
assert len(img_elems) == num_files*4
|
| 280 |
+
|
| 281 |
+
assert len(infer_tab.selectbox) == 1
|
| 282 |
+
|
| 283 |
+
print(at.button)
|
| 284 |
+
print(infer_tab.button)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
|