Spaces:
Sleeping
Sleeping
Merge pull request #38 from sdsc-ordes/fix/spoof-metadata
Browse files- .github/workflows/python-pytest.yml +1 -0
- .github/workflows/python-visualtests.yml +2 -0
- docs/dev_notes.md +24 -7
- src/input/input_handling.py +33 -12
- tests/test_demo_input_sidebar.py +9 -2
- tests/test_demo_multifile_upload.py +6 -2
- tests/test_main.py +5 -2
- tests/visual_selenium/test_visual_main.py +3 -0
.github/workflows/python-pytest.yml
CHANGED
@@ -34,3 +34,4 @@ jobs:
|
|
34 |
- name: Run quick tests with pytest
|
35 |
run: |
|
36 |
pytest -m "not slow and not visual" --strict-markers --ignore=tests/visual_selenium
|
|
|
|
34 |
- name: Run quick tests with pytest
|
35 |
run: |
|
36 |
pytest -m "not slow and not visual" --strict-markers --ignore=tests/visual_selenium
|
37 |
+
|
.github/workflows/python-visualtests.yml
CHANGED
@@ -51,3 +51,5 @@ jobs:
|
|
51 |
# otherwise, not one step it consistently fails at.)
|
52 |
run: |
|
53 |
pytest -m "visual" --strict-markers tests/visual_selenium/ -s --demo
|
|
|
|
|
|
51 |
# otherwise, not one step it consistently fails at.)
|
52 |
run: |
|
53 |
pytest -m "visual" --strict-markers tests/visual_selenium/ -s --demo
|
54 |
+
|
55 |
+
# DEBUG_AUTOPOPULATE_METADATA=True streamlit run src/main.py
|
docs/dev_notes.md
CHANGED
@@ -13,7 +13,7 @@ Then use a web browser to view the site indiciated, by default: http://localhost
|
|
13 |
|
14 |
# How to build and view docs locally
|
15 |
|
16 |
-
We have a CI action to
|
17 |
To validate locally, you need the deps listed in `requirements.txt` installed.
|
18 |
|
19 |
Run
|
@@ -51,14 +51,15 @@ The CI runs with `--strict-markers` so any new marker must be registered in
|
|
51 |
|
52 |
- the basic CI action runs the fast tests only, skipping all tests marked
|
53 |
`visual` and `slow`
|
54 |
-
- the CI action on PR runs the `slow` tests, but
|
55 |
-
-
|
56 |
|
57 |
Check all tests are marked ok, and that they are filtered correctly by the
|
58 |
groupings used in CI:
|
59 |
```bash
|
60 |
pytest --collect-only -m "not slow and not visual" --strict-markers --ignore=tests/visual_selenium
|
61 |
pytest --collect-only -m "not visual" --strict-markers --ignore=tests/visual_selenium
|
|
|
62 |
```
|
63 |
|
64 |
|
@@ -97,7 +98,8 @@ pytest --cov-report=lcov --cov=src
|
|
97 |
|
98 |
We use seleniumbase to test the visual appearance of the app, including the
|
99 |
presence of elements that appear through the workflow. This testing takes quite
|
100 |
-
a long time to execute
|
|
|
101 |
|
102 |
```bash
|
103 |
# install packages for app and for visual testing
|
@@ -106,14 +108,15 @@ pip install -r tests/visual_selenium/requirements_visual.txt
|
|
106 |
```
|
107 |
|
108 |
**Running tests**
|
109 |
-
The execution of these tests requires that the site/app is running already
|
|
|
110 |
|
111 |
-
|
112 |
```bash
|
113 |
streamlit run src/main.py
|
114 |
```
|
115 |
|
116 |
-
In another tab:
|
117 |
```bash
|
118 |
# run just the visual tests
|
119 |
pytest -m "visual" --strict-markers
|
@@ -132,3 +135,17 @@ pytest -m "not slow and not visual" --strict-markers --ignore=tests/visual_selen
|
|
132 |
Initially we have an action setup that runs all tests in the `tests` directory, within the `test/tests` branch.
|
133 |
|
134 |
TODO: Add some test report & coverage badges to the README.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
# How to build and view docs locally
|
15 |
|
16 |
+
We have a CI action to present the docs on github.io.
|
17 |
To validate locally, you need the deps listed in `requirements.txt` installed.
|
18 |
|
19 |
Run
|
|
|
51 |
|
52 |
- the basic CI action runs the fast tests only, skipping all tests marked
|
53 |
`visual` and `slow`
|
54 |
+
- the CI action on PR runs the `slow` tests, but still excluding `visual`.
|
55 |
+
- a second action for the visual tests runs on PR.
|
56 |
|
57 |
Check all tests are marked ok, and that they are filtered correctly by the
|
58 |
groupings used in CI:
|
59 |
```bash
|
60 |
pytest --collect-only -m "not slow and not visual" --strict-markers --ignore=tests/visual_selenium
|
61 |
pytest --collect-only -m "not visual" --strict-markers --ignore=tests/visual_selenium
|
62 |
+
pytest --collect-only -m "visual" --strict-markers tests/visual_selenium/ -s --demo
|
63 |
```
|
64 |
|
65 |
|
|
|
98 |
|
99 |
We use seleniumbase to test the visual appearance of the app, including the
|
100 |
presence of elements that appear through the workflow. This testing takes quite
|
101 |
+
a long time to execute. It is configured in a separate CI action
|
102 |
+
(`python-visualtests.yml`).
|
103 |
|
104 |
```bash
|
105 |
# install packages for app and for visual testing
|
|
|
108 |
```
|
109 |
|
110 |
**Running tests**
|
111 |
+
The execution of these tests requires that the site/app is running already, which
|
112 |
+
is handled by a fixture (that starts the app in another thread).
|
113 |
|
114 |
+
Alternatively, in one tab, run:
|
115 |
```bash
|
116 |
streamlit run src/main.py
|
117 |
```
|
118 |
|
119 |
+
In another tab, run:
|
120 |
```bash
|
121 |
# run just the visual tests
|
122 |
pytest -m "visual" --strict-markers
|
|
|
135 |
Initially we have an action setup that runs all tests in the `tests` directory, within the `test/tests` branch.
|
136 |
|
137 |
TODO: Add some test report & coverage badges to the README.
|
138 |
+
|
139 |
+
|
140 |
+
## Environment flags used in development
|
141 |
+
|
142 |
+
- `DEBUG_AUTOPOPULATE_METADATA=True` : Set this env variable to have the text
|
143 |
+
inputs autopopulated, to make stepping through the workflow faster during
|
144 |
+
development work.
|
145 |
+
|
146 |
+
Typical usage:
|
147 |
+
|
148 |
+
```bash
|
149 |
+
DEBUG_AUTOPOPULATE_METADATA=True streamlit run src/main.py
|
150 |
+
```
|
151 |
+
|
src/input/input_handling.py
CHANGED
@@ -2,6 +2,7 @@ from typing import List, Tuple
|
|
2 |
import datetime
|
3 |
import logging
|
4 |
import hashlib
|
|
|
5 |
|
6 |
import streamlit as st
|
7 |
from streamlit.delta_generator import DeltaGenerator
|
@@ -23,15 +24,31 @@ both the UI elements (setup_input_UI) and the validation functions.
|
|
23 |
'''
|
24 |
allowed_image_types = ['jpg', 'jpeg', 'png', 'webp']
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
# an arbitrary set of defaults so testing is less painful...
|
27 |
# ideally we add in some randomization to the defaults
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
def check_inputs_are_set(empty_ok:bool=False, debug:bool=False) -> bool:
|
37 |
"""
|
@@ -50,12 +67,15 @@ def check_inputs_are_set(empty_ok:bool=False, debug:bool=False) -> bool:
|
|
50 |
return empty_ok
|
51 |
|
52 |
exp_input_key_stubs = ["input_latitude", "input_longitude", "input_date", "input_time"]
|
53 |
-
#exp_input_key_stubs = ["input_latitude", "input_longitude", "input_author_email", "input_date", "input_time",
|
54 |
|
55 |
vals = []
|
56 |
# the author_email is global/one-off - no hash extension.
|
57 |
if "input_author_email" in st.session_state:
|
58 |
val = st.session_state["input_author_email"]
|
|
|
|
|
|
|
|
|
59 |
vals.append(val)
|
60 |
if debug:
|
61 |
msg = f"{'input_author_email':15}, {(val is not None):8}, {val}"
|
@@ -190,10 +210,11 @@ def metadata_inputs_one_file(file:UploadedFile, image_hash:str, dbg_ix:int=0) ->
|
|
190 |
msg = f"[D] {filename}: lat, lon from image metadata: {latitude0}, {longitude0}"
|
191 |
m_logger.debug(msg)
|
192 |
|
193 |
-
if
|
194 |
-
latitude0:
|
195 |
-
|
196 |
-
longitude0
|
|
|
197 |
|
198 |
image = st.session_state.images.get(image_hash, None)
|
199 |
# add the UI elements
|
|
|
2 |
import datetime
|
3 |
import logging
|
4 |
import hashlib
|
5 |
+
import os
|
6 |
|
7 |
import streamlit as st
|
8 |
from streamlit.delta_generator import DeltaGenerator
|
|
|
24 |
'''
|
25 |
allowed_image_types = ['jpg', 'jpeg', 'png', 'webp']
|
26 |
|
27 |
+
def _is_str_true(v:str) -> bool:
|
28 |
+
''' convert a string to boolean: if contains True or 1 (or yes), return True '''
|
29 |
+
# https://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python
|
30 |
+
return v.lower() in ("yes", "true", "t", "1")
|
31 |
+
|
32 |
+
def load_debug_autopopulate() -> bool:
|
33 |
+
return _is_str_true( os.getenv("DEBUG_AUTOPOPULATE_METADATA", "False"))
|
34 |
+
|
35 |
+
|
36 |
# an arbitrary set of defaults so testing is less painful...
|
37 |
# ideally we add in some randomization to the defaults
|
38 |
+
dbg_populate_metadata = load_debug_autopopulate()
|
39 |
+
|
40 |
+
# the other main option would be argparse, where we can run `streamlit run src/main.py -- --debug` or similar
|
41 |
+
# - I think env vars are simple and clean enough, it isn't really a CLI that we want to offer debug options, it is for dev.
|
42 |
+
if dbg_populate_metadata:
|
43 |
+
spoof_metadata = {
|
44 |
+
"latitude": 0.5,
|
45 |
+
"longitude": 44,
|
46 |
+
"author_email": "[email protected]",
|
47 |
+
"date": None,
|
48 |
+
"time": None,
|
49 |
+
}
|
50 |
+
else:
|
51 |
+
spoof_metadata = {}
|
52 |
|
53 |
def check_inputs_are_set(empty_ok:bool=False, debug:bool=False) -> bool:
|
54 |
"""
|
|
|
67 |
return empty_ok
|
68 |
|
69 |
exp_input_key_stubs = ["input_latitude", "input_longitude", "input_date", "input_time"]
|
|
|
70 |
|
71 |
vals = []
|
72 |
# the author_email is global/one-off - no hash extension.
|
73 |
if "input_author_email" in st.session_state:
|
74 |
val = st.session_state["input_author_email"]
|
75 |
+
# if val is a string and empty, set to None
|
76 |
+
if isinstance(val, str) and not val:
|
77 |
+
val = None
|
78 |
+
|
79 |
vals.append(val)
|
80 |
if debug:
|
81 |
msg = f"{'input_author_email':15}, {(val is not None):8}, {val}"
|
|
|
210 |
msg = f"[D] {filename}: lat, lon from image metadata: {latitude0}, {longitude0}"
|
211 |
m_logger.debug(msg)
|
212 |
|
213 |
+
if spoof_metadata:
|
214 |
+
if latitude0 is None: # get some default values if not found in exifdata
|
215 |
+
latitude0:float = spoof_metadata.get('latitude', 0) + dbg_ix
|
216 |
+
if longitude0 is None:
|
217 |
+
longitude0:float = spoof_metadata.get('longitude', 0) - dbg_ix
|
218 |
|
219 |
image = st.session_state.images.get(image_hash, None)
|
220 |
# add the UI elements
|
tests/test_demo_input_sidebar.py
CHANGED
@@ -3,6 +3,7 @@ from pathlib import Path
|
|
3 |
from io import BytesIO
|
4 |
from PIL import Image
|
5 |
import numpy as np
|
|
|
6 |
|
7 |
import pytest
|
8 |
from unittest.mock import MagicMock, patch
|
@@ -12,7 +13,7 @@ import time
|
|
12 |
|
13 |
from input.input_handling import spoof_metadata
|
14 |
from input.input_observation import InputObservation
|
15 |
-
from input.input_handling import buffer_uploaded_files
|
16 |
|
17 |
from streamlit.runtime.uploaded_file_manager import UploadedFile
|
18 |
|
@@ -184,7 +185,13 @@ def test_no_input_no_interaction():
|
|
184 |
at = AppTest.from_file(SCRIPT_UNDER_TEST, default_timeout=10).run()
|
185 |
verify_initial_session_state(at)
|
186 |
|
187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
# print (f"[I] whole tree: {at._tree}")
|
190 |
# for elem in at.sidebar.markdown:
|
|
|
3 |
from io import BytesIO
|
4 |
from PIL import Image
|
5 |
import numpy as np
|
6 |
+
import os
|
7 |
|
8 |
import pytest
|
9 |
from unittest.mock import MagicMock, patch
|
|
|
13 |
|
14 |
from input.input_handling import spoof_metadata
|
15 |
from input.input_observation import InputObservation
|
16 |
+
from input.input_handling import buffer_uploaded_files, load_debug_autopopulate
|
17 |
|
18 |
from streamlit.runtime.uploaded_file_manager import UploadedFile
|
19 |
|
|
|
185 |
at = AppTest.from_file(SCRIPT_UNDER_TEST, default_timeout=10).run()
|
186 |
verify_initial_session_state(at)
|
187 |
|
188 |
+
dbg = load_debug_autopopulate()
|
189 |
+
#var = at.session_state.input_author_email
|
190 |
+
#_cprint(f"[I] input email is '{var}' type: {type(var)} | is None? {var is None} | {dbg}", PURPLE)
|
191 |
+
if dbg: # autopopulated
|
192 |
+
assert at.session_state.input_author_email == spoof_metadata.get("author_email")
|
193 |
+
else: # should be empty, the user has to fill it in
|
194 |
+
assert at.session_state.input_author_email == ""
|
195 |
|
196 |
# print (f"[I] whole tree: {at._tree}")
|
197 |
# for elem in at.sidebar.markdown:
|
tests/test_demo_multifile_upload.py
CHANGED
@@ -26,7 +26,7 @@ from streamlit.testing.v1 import AppTest
|
|
26 |
|
27 |
|
28 |
# for expectations
|
29 |
-
from input.input_handling import spoof_metadata
|
30 |
from input.input_validator import get_image_datetime, get_image_latlon
|
31 |
|
32 |
|
@@ -137,7 +137,11 @@ def test_no_input_no_interaction():
|
|
137 |
|
138 |
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
139 |
assert at.session_state.observations == {}
|
140 |
-
|
|
|
|
|
|
|
|
|
141 |
|
142 |
def test_bad_email():
|
143 |
with patch.dict(spoof_metadata, {"author_email": "notanemail"}):
|
|
|
26 |
|
27 |
|
28 |
# for expectations
|
29 |
+
from input.input_handling import spoof_metadata, load_debug_autopopulate
|
30 |
from input.input_validator import get_image_datetime, get_image_latlon
|
31 |
|
32 |
|
|
|
137 |
|
138 |
at = AppTest.from_file("src/apptest/demo_multifile_upload.py").run()
|
139 |
assert at.session_state.observations == {}
|
140 |
+
dbg = load_debug_autopopulate()
|
141 |
+
if dbg: # autopopulated
|
142 |
+
assert at.session_state.input_author_email == spoof_metadata.get("author_email")
|
143 |
+
else: # should be empty, the user has to fill it in
|
144 |
+
assert at.session_state.input_author_email == ""
|
145 |
|
146 |
def test_bad_email():
|
147 |
with patch.dict(spoof_metadata, {"author_email": "notanemail"}):
|
tests/test_main.py
CHANGED
@@ -3,7 +3,7 @@ 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 |
|
@@ -72,7 +72,10 @@ def test_click_validate_after_data_entry(mock_file_rv: MagicMock, mock_uploadedF
|
|
72 |
assert infer_button.disabled == True
|
73 |
|
74 |
|
75 |
-
# 2. upload files, and trigger the callback
|
|
|
|
|
|
|
76 |
|
77 |
# put the mocked file_upload into session state, as if it were the result of a file upload, with the key 'file_uploader_data'
|
78 |
at.session_state["file_uploader_data"] = mock_files
|
|
|
3 |
from streamlit.testing.v1 import AppTest
|
4 |
import time
|
5 |
|
6 |
+
from input.input_handling import spoof_metadata, load_debug_autopopulate
|
7 |
from input.input_observation import InputObservation
|
8 |
from input.input_handling import buffer_uploaded_files
|
9 |
|
|
|
72 |
assert infer_button.disabled == True
|
73 |
|
74 |
|
75 |
+
# 2. upload files, enter email, and trigger the callback
|
76 |
+
if not load_debug_autopopulate():
|
77 |
+
# fill the text box with a dummy email
|
78 |
+
at.session_state.input_author_email = "[email protected]"
|
79 |
|
80 |
# put the mocked file_upload into session state, as if it were the result of a file upload, with the key 'file_uploader_data'
|
81 |
at.session_state["file_uploader_data"] = mock_files
|
tests/visual_selenium/test_visual_main.py
CHANGED
@@ -208,6 +208,7 @@ class RecorderTest(BaseCase):
|
|
208 |
# - setup steps:
|
209 |
# - open the app
|
210 |
# - upload two images
|
|
|
211 |
# - validate the data entry
|
212 |
# - click the infer button, wait for ML
|
213 |
# - the real test steps:
|
@@ -228,6 +229,8 @@ class RecorderTest(BaseCase):
|
|
228 |
'input[data-testid="stFileUploaderDropzoneInput"]',
|
229 |
"\n".join([str(img_f1), str(img_f2)]),
|
230 |
)
|
|
|
|
|
231 |
|
232 |
# advance to the next step, by clicking the validate button (wait for it first)
|
233 |
wait_for_element(self, By.XPATH, "//button//strong[contains(text(), 'Validate')]")
|
|
|
208 |
# - setup steps:
|
209 |
# - open the app
|
210 |
# - upload two images
|
211 |
+
# - enter author email
|
212 |
# - validate the data entry
|
213 |
# - click the infer button, wait for ML
|
214 |
# - the real test steps:
|
|
|
229 |
'input[data-testid="stFileUploaderDropzoneInput"]',
|
230 |
"\n".join([str(img_f1), str(img_f2)]),
|
231 |
)
|
232 |
+
# enter author email
|
233 |
+
self.type('input[aria-label="Author Email"]', "[email protected]\n")
|
234 |
|
235 |
# advance to the next step, by clicking the validate button (wait for it first)
|
236 |
wait_for_element(self, By.XPATH, "//button//strong[contains(text(), 'Validate')]")
|