Spaces:
Sleeping
Sleeping
rmm
commited on
Commit
·
a4adf97
1
Parent(s):
8a40c2d
test: now using fixture that loads real data in the file_upload
Browse files- added simple tests for author_email
- added a test that validates the file_uploader process (for multi-file
handling), by getting real image data, extracting metadata and
presenting it visually (see `test_mockupload_list_realdata`)
- added some explanations to the tests
tests/test_demo_multifile_upload.py
CHANGED
|
@@ -8,13 +8,21 @@ import pytest
|
|
| 8 |
from unittest.mock import MagicMock, patch
|
| 9 |
from streamlit.testing.v1 import AppTest
|
| 10 |
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
# zero test: no inputs -> empty session state
|
| 14 |
-
# (or maybe even non-existent session state; for file_uploader we are not allowed to initialise the keyed variable, st borks)
|
| 15 |
|
| 16 |
-
#
|
|
|
|
|
|
|
| 17 |
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
# for expectations
|
|
@@ -118,15 +126,122 @@ def mock_uploadedFile_List_ImageData(mock_uploadedFile):
|
|
| 118 |
return create_list_of_mocks_realdata
|
| 119 |
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
def test_no_input_no_interaction():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
| 124 |
-
|
| 125 |
assert at.session_state.observations == {}
|
| 126 |
assert at.session_state.input_author_email == spoof_metadata.get("author_email")
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
| 130 |
@patch("streamlit.file_uploader")
|
| 131 |
def test_mockupload_list(mock_file_uploader_rtn: MagicMock, mock_uploadedFile_List):
|
| 132 |
# Create a list of 2 mock files
|
|
|
|
| 8 |
from unittest.mock import MagicMock, patch
|
| 9 |
from streamlit.testing.v1 import AppTest
|
| 10 |
|
| 11 |
+
# tests for apptest/demo_multifile_upload
|
| 12 |
+
# - the functionality in the test harness is a file_uploader that is configured
|
| 13 |
+
# for multi-file input; and uses a callback to buffer the files into session state.
|
| 14 |
+
# - the handling of individual files includes extracting metadata from the files
|
| 15 |
+
# - a text_area is created for each file, to display the metadata extracted;
|
| 16 |
+
# this deviates from the presentation in the real app, but the extracted info
|
| 17 |
+
# is the same (here we put it all in text which is far easier to validate using AppTest)
|
| 18 |
+
# - the demo also has the author email input
|
| 19 |
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
# zero test: no inputs -> empty session state
|
| 22 |
+
# (or maybe even non-existent session state; for file_uploader we are not
|
| 23 |
+
# allowed to initialise the keyed variable, st borks)
|
| 24 |
|
| 25 |
+
# many test: list of >=2 inputs -> session state with 2 files
|
| 26 |
|
| 27 |
|
| 28 |
# for expectations
|
|
|
|
| 126 |
return create_list_of_mocks_realdata
|
| 127 |
|
| 128 |
|
| 129 |
+
# simple tests on the author email input via AppTest
|
| 130 |
+
# - empty input should propagate to session state
|
| 131 |
+
# - invalid email should trigger an error
|
| 132 |
def test_no_input_no_interaction():
|
| 133 |
+
with patch.dict(spoof_metadata, {"author_email": None}):
|
| 134 |
+
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
| 135 |
+
assert at.session_state.observations == {}
|
| 136 |
+
assert at.session_state.input_author_email == None
|
| 137 |
+
|
| 138 |
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
|
|
|
| 139 |
assert at.session_state.observations == {}
|
| 140 |
assert at.session_state.input_author_email == spoof_metadata.get("author_email")
|
| 141 |
|
| 142 |
+
def test_bad_email():
|
| 143 |
+
with patch.dict(spoof_metadata, {"author_email": "notanemail"}):
|
| 144 |
+
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
| 145 |
+
assert at.session_state.input_author_email == "notanemail"
|
| 146 |
+
assert at.error[0].value == "Please enter a valid email address."
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# test when we load real data files, with all properties as per real app
|
| 150 |
+
# - if files loaded correctly and metadata is extracted correctly, we should see the
|
| 151 |
+
# the data in both the session state and in the visual elements.
|
| 152 |
+
@patch("streamlit.file_uploader")
|
| 153 |
+
def test_mockupload_list_realdata(mock_file_rv: MagicMock, mock_uploadedFile_List_ImageData):
|
| 154 |
+
#def test_mockupload_list(mock_file_uploader_rtn: MagicMock, mock_uploadedFile_List):
|
| 155 |
+
num_files = 3
|
| 156 |
+
PRINT_PROPS = False
|
| 157 |
+
# Create a list of n mock files
|
| 158 |
+
mock_files = mock_uploadedFile_List_ImageData(num_files=num_files)
|
| 159 |
+
|
| 160 |
+
# Set the return value of the mocked file_uploader to the list of mock files
|
| 161 |
+
mock_file_rv.return_value = mock_files
|
| 162 |
+
|
| 163 |
+
# Run the Streamlit app
|
| 164 |
+
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
| 165 |
+
|
| 166 |
+
# put the mocked file_upload into session state, as if it were the result of a file upload, with the key 'file_uploader_data'
|
| 167 |
+
at.session_state["file_uploader_data"] = mock_files
|
| 168 |
+
|
| 169 |
+
#print(f"[I] session state: {at.session_state}")
|
| 170 |
+
#print(f"[I] uploaded files: {at.session_state.file_uploader_data}")
|
| 171 |
+
|
| 172 |
+
if PRINT_PROPS:
|
| 173 |
+
print(f"[I] uploaded files: ({len(at.session_state.file_uploader_data)}) {at.session_state.file_uploader_data}")
|
| 174 |
+
for _f in at.session_state.file_uploader_data:
|
| 175 |
+
#print(f"\t[I] props: {dir(_f)}")
|
| 176 |
+
print(f" [I] name: {_f.name}")
|
| 177 |
+
print(f"\t[I] size: {_f.size}")
|
| 178 |
+
print(f"\t[I] type: {_f.type}")
|
| 179 |
+
# lets make an image from the data
|
| 180 |
+
im = Image.open(_f)
|
| 181 |
+
|
| 182 |
+
# lets see what metadata we can get to.
|
| 183 |
+
dt = get_image_datetime(_f)
|
| 184 |
+
print(f"\t[I] datetime: {dt}")
|
| 185 |
+
lat, lon = get_image_latlon(_f)
|
| 186 |
+
print(f"\t[I] lat, lon: {lat}, {lon}")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# we expect to get the following info from the files
|
| 190 |
+
# file1:
|
| 191 |
+
# datetime: 2024:10:24 15:59:45
|
| 192 |
+
# lat, lon: 46.51860277777778, 6.562075
|
| 193 |
+
# file2:
|
| 194 |
+
# datetime: None
|
| 195 |
+
# lat, lon: 46.51860277777778, 6.562075
|
| 196 |
+
|
| 197 |
+
# let's run assertions on the backend data (session_state)
|
| 198 |
+
# and then on the front end too (visual elements)
|
| 199 |
+
f1 = at.session_state.file_uploader_data[0]
|
| 200 |
+
f2 = at.session_state.file_uploader_data[1]
|
| 201 |
+
|
| 202 |
+
assert get_image_datetime(f1) == "2024:10:24 15:59:45"
|
| 203 |
+
assert get_image_datetime(f2) == None
|
| 204 |
+
# use a tolerance of 1e-6, assert that the lat, lon is close to 46.5186
|
| 205 |
+
assert abs(get_image_latlon(f1)[0] - 46.51860277777778) < 1e-6
|
| 206 |
+
assert abs(get_image_latlon(f1)[1] - 6.562075) < 1e-6
|
| 207 |
+
assert abs(get_image_latlon(f2)[0] - 46.51860277777778) < 1e-6
|
| 208 |
+
assert abs(get_image_latlon(f2)[1] - 6.562075) < 1e-6
|
| 209 |
+
|
| 210 |
+
# need to run the script top-to-bottom to get the text_area elements
|
| 211 |
+
# since they are dynamically created.
|
| 212 |
+
at.run()
|
| 213 |
+
|
| 214 |
+
# since we uplaoded num_files files, hopefully we get num_files text areas
|
| 215 |
+
assert len(at.text_area) == num_files
|
| 216 |
+
# expecting
|
| 217 |
+
exp0 = "index: 0, name: cakes.jpg, datetime: 2024:10:24 15:59:45, lat: 46.51860277777778, lon:6.562075"
|
| 218 |
+
exp1 = "index: 1, name: cakes_no_exif_datetime.jpg, datetime: None, lat: 46.51860277777778, lon:6.562075"
|
| 219 |
+
exp2 = "index: 2, name: cakes_no_exif_gps.jpg, datetime: 2024:10:24 15:59:45, lat: None, lon:None"
|
| 220 |
+
|
| 221 |
+
assert at.text_area[0].value == exp0
|
| 222 |
+
assert at.text_area[1].value == exp1
|
| 223 |
+
if num_files >= 1:
|
| 224 |
+
assert at.text_area(key='metadata_0').value == exp0
|
| 225 |
+
if num_files >= 2:
|
| 226 |
+
assert at.text_area(key='metadata_1').value == exp1
|
| 227 |
+
if num_files >= 3:
|
| 228 |
+
assert at.text_area(key='metadata_2').value == exp2
|
| 229 |
+
|
| 230 |
+
# {"fname": "cakes.jpg", "size": 1234, "type": "image/jpeg"},
|
| 231 |
+
# {"fname": "cakes_no_exif_datetime.jpg", "size": 12345, "type": "image/jpeg"},
|
| 232 |
+
# {"fname": "cakes_no_exif_gps.jpg", "size": 123456, "type": "image/jpeg"},
|
| 233 |
+
#]
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# Verify the behavior in your app
|
| 237 |
+
assert len(at.session_state.file_uploader_data) == num_files
|
| 238 |
+
|
| 239 |
+
assert at.session_state.file_uploader_data[0].size == 1234 # Check properties of the first file
|
| 240 |
+
assert at.session_state.file_uploader_data[1].name == "cakes_no_exif_datetime.jpg"
|
| 241 |
|
| 242 |
|
| 243 |
+
# this test was a stepping stone; when I was mocking files that didn't have any real data
|
| 244 |
+
# - it helped to explore how properties should be set in the mock object and generator funcs.
|
| 245 |
@patch("streamlit.file_uploader")
|
| 246 |
def test_mockupload_list(mock_file_uploader_rtn: MagicMock, mock_uploadedFile_List):
|
| 247 |
# Create a list of 2 mock files
|