rmm commited on
Commit
de87a9b
·
1 Parent(s): 35f3306

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!

Files changed (1) hide show
  1. 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
+