diff --git a/.github/workflows/build-windows-executable-app.yaml b/.github/workflows/build-windows-executable-app.yaml index c757504..a86c903 100644 --- a/.github/workflows/build-windows-executable-app.yaml +++ b/.github/workflows/build-windows-executable-app.yaml @@ -311,6 +311,10 @@ jobs: Copy-Item "openms-bin/${file}.exe" -Destination "streamlit_exe/${file}.exe" } + - name: Remove server address for local Windows deployment + run: | + (Get-Content streamlit_exe/.streamlit/config.toml) -notmatch '^address' | Set-Content streamlit_exe/.streamlit/config.toml + - name: Generate Readme.txt shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dce98b6..7baf813 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,19 +8,14 @@ jobs: strategy: matrix: os: [ubuntu-latest] - # Requirements file generated with python=3.11 + # Requirements file generated with python=3.12; tested with python=3.11 python-version: ["3.11"] steps: - uses: actions/checkout@v4 - - uses: conda-incubator/setup-miniconda@v3 + - uses: actions/setup-python@v4 with: - activate-environment: openms python-version: ${{ matrix.python-version }} - channels: defaults,bioconda,conda-forge - - name: Install OpenMS - run: | - conda install openms -y - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/test-win-exe-w-embed-py.yaml b/.github/workflows/test-win-exe-w-embed-py.yaml index 80657ad..deec56d 100644 --- a/.github/workflows/test-win-exe-w-embed-py.yaml +++ b/.github/workflows/test-win-exe-w-embed-py.yaml @@ -61,6 +61,10 @@ jobs: cp settings.json streamlit_exe cp default-parameters.json streamlit_exe cp ${{ env.APP_NAME }}.bat streamlit_exe + + - name: Remove server address for local Windows deployment + run: | + (Get-Content streamlit_exe/.streamlit/config.toml) -notmatch '^address' | Set-Content streamlit_exe/.streamlit/config.toml - name: Generate Readme.txt shell: bash diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 2e2f82e..f3bfa2c 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -7,9 +7,6 @@ developmentMode = false [server] maxUploadSize = 1000 #MB port = 8501 # should be same as configured in deployment repo -address = "0.0.0.0" -enableCORS = false -enableXsrfProtection = false [theme] diff --git a/gdpr_consent/dist/bundle.js b/gdpr_consent/dist/bundle.js index 2d2d814..8614457 100644 --- a/gdpr_consent/dist/bundle.js +++ b/gdpr_consent/dist/bundle.js @@ -235,7 +235,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! streamlit-component-lib */ \"./node_modules/streamlit-component-lib/dist/index.js\");\nvar __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __generator = (undefined && undefined.__generator) || function (thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n};\n\n// Defines the configuration for Klaro\nvar klaroConfig = {\n mustConsent: true,\n acceptAll: true,\n services: []\n};\n// This will make klaroConfig globally accessible\nwindow.klaroConfig = klaroConfig;\n// Function to safely access the Klaro manager\nfunction getKlaroManager() {\n var _a;\n return ((_a = window.klaro) === null || _a === void 0 ? void 0 : _a.getManager) ? window.klaro.getManager() : null;\n}\n// Waits until Klaro Manager is available\nfunction waitForKlaroManager() {\n return __awaiter(this, arguments, void 0, function (maxWaitTime, interval) {\n var startTime, klaroManager;\n if (maxWaitTime === void 0) { maxWaitTime = 5000; }\n if (interval === void 0) { interval = 100; }\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n startTime = Date.now();\n _a.label = 1;\n case 1:\n if (!(Date.now() - startTime < maxWaitTime)) return [3 /*break*/, 3];\n klaroManager = getKlaroManager();\n if (klaroManager) {\n return [2 /*return*/, klaroManager];\n }\n return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, interval); })];\n case 2:\n _a.sent();\n return [3 /*break*/, 1];\n case 3: throw new Error(\"Klaro manager did not become available within the allowed time.\");\n }\n });\n });\n}\n// Helper function to handle unknown errors\nfunction handleError(error) {\n if (error instanceof Error) {\n console.error(\"Error:\", error.message);\n }\n else {\n console.error(\"Unknown error:\", error);\n }\n}\n// Tracking was accepted\nfunction callback() {\n return __awaiter(this, void 0, void 0, function () {\n var manager, return_vals, _i, _a, service, error_1;\n return __generator(this, function (_b) {\n switch (_b.label) {\n case 0:\n _b.trys.push([0, 2, , 3]);\n return [4 /*yield*/, waitForKlaroManager()];\n case 1:\n manager = _b.sent();\n if (manager.confirmed) {\n return_vals = {};\n for (_i = 0, _a = klaroConfig.services; _i < _a.length; _i++) {\n service = _a[_i];\n return_vals[service.name] = manager.getConsent(service.name);\n }\n streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentValue(return_vals);\n }\n return [3 /*break*/, 3];\n case 2:\n error_1 = _b.sent();\n handleError(error_1);\n return [3 /*break*/, 3];\n case 3: return [2 /*return*/];\n }\n });\n });\n}\n// Stores if the component has been rendered before\nvar rendered = false;\nfunction onRender(event) {\n // Klaro does not work if embedded multiple times\n if (rendered) {\n return;\n }\n rendered = true;\n var data = event.detail;\n if (data.args['google_analytics']) {\n klaroConfig.services.push({\n name: 'google-analytics',\n cookies: [\n /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use\n ],\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n if (data.args['piwik_pro']) {\n klaroConfig.services.push({\n name: 'piwik-pro',\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n // Create a new script element\n var script = document.createElement('script');\n // Set the necessary attributes\n script.defer = true;\n script.type = 'application/javascript';\n script.src = 'https://cdn.kiprotect.com/klaro/v0.7/klaro.js';\n // Set the klaro config\n script.setAttribute('data-config', 'klaroConfig');\n // Append the script to the head or body\n document.head.appendChild(script);\n}\n// Attach our `onRender` handler to Streamlit's render event.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.events.addEventListener(streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.RENDER_EVENT, onRender);\n// Tell Streamlit we're ready to start receiving data. We won't get our\n// first RENDER_EVENT until we call this function.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentReady();\n// Finally, tell Streamlit to update the initial height.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setFrameHeight(1000);\n\n\n//# sourceURL=webpack://gdpr_consent/./src/main.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! streamlit-component-lib */ \"./node_modules/streamlit-component-lib/dist/index.js\");\nvar __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __generator = (undefined && undefined.__generator) || function (thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n};\n\n// Defines the configuration for Klaro\nvar klaroConfig = {\n mustConsent: true,\n acceptAll: true,\n services: []\n};\n// This will make klaroConfig globally accessible\nwindow.klaroConfig = klaroConfig;\n// Function to safely access the Klaro manager\nfunction getKlaroManager() {\n var _a;\n return ((_a = window.klaro) === null || _a === void 0 ? void 0 : _a.getManager) ? window.klaro.getManager() : null;\n}\n// Waits until Klaro Manager is available\nfunction waitForKlaroManager() {\n return __awaiter(this, arguments, void 0, function (maxWaitTime, interval) {\n var startTime, klaroManager;\n if (maxWaitTime === void 0) { maxWaitTime = 5000; }\n if (interval === void 0) { interval = 100; }\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n startTime = Date.now();\n _a.label = 1;\n case 1:\n if (!(Date.now() - startTime < maxWaitTime)) return [3 /*break*/, 3];\n klaroManager = getKlaroManager();\n if (klaroManager) {\n return [2 /*return*/, klaroManager];\n }\n return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, interval); })];\n case 2:\n _a.sent();\n return [3 /*break*/, 1];\n case 3: throw new Error(\"Klaro manager did not become available within the allowed time.\");\n }\n });\n });\n}\n// Helper function to handle unknown errors\nfunction handleError(error) {\n if (error instanceof Error) {\n console.error(\"Error:\", error.message);\n }\n else {\n console.error(\"Unknown error:\", error);\n }\n}\n// Tracking was accepted\nfunction callback() {\n return __awaiter(this, void 0, void 0, function () {\n var manager, return_vals, _i, _a, service, error_1;\n return __generator(this, function (_b) {\n switch (_b.label) {\n case 0:\n _b.trys.push([0, 2, , 3]);\n return [4 /*yield*/, waitForKlaroManager()];\n case 1:\n manager = _b.sent();\n if (manager.confirmed) {\n return_vals = {};\n for (_i = 0, _a = klaroConfig.services; _i < _a.length; _i++) {\n service = _a[_i];\n return_vals[service.name] = manager.getConsent(service.name);\n }\n streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentValue(return_vals);\n }\n return [3 /*break*/, 3];\n case 2:\n error_1 = _b.sent();\n handleError(error_1);\n return [3 /*break*/, 3];\n case 3: return [2 /*return*/];\n }\n });\n });\n}\n// Stores if the component has been rendered before\nvar rendered = false;\nfunction onRender(event) {\n // Klaro does not work if embedded multiple times\n if (rendered) {\n return;\n }\n rendered = true;\n var data = event.detail;\n if (data.args['google_analytics']) {\n klaroConfig.services.push({\n name: 'google-analytics',\n cookies: [\n /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use\n ],\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n if (data.args['piwik_pro']) {\n klaroConfig.services.push({\n name: 'piwik-pro',\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n if (data.args['matomo']) {\n klaroConfig.services.push({\n name: 'matomo',\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n // Create a new script element\n var script = document.createElement('script');\n // Set the necessary attributes\n script.defer = true;\n script.type = 'application/javascript';\n script.src = 'https://cdn.kiprotect.com/klaro/v0.7/klaro.js';\n // Set the klaro config\n script.setAttribute('data-config', 'klaroConfig');\n // Append the script to the head or body\n document.head.appendChild(script);\n}\n// Attach our `onRender` handler to Streamlit's render event.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.events.addEventListener(streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.RENDER_EVENT, onRender);\n// Tell Streamlit we're ready to start receiving data. We won't get our\n// first RENDER_EVENT until we call this function.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentReady();\n// Finally, tell Streamlit to update the initial height.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setFrameHeight(1000);\n\n\n//# sourceURL=webpack://gdpr_consent/./src/main.ts?"); /***/ }), diff --git a/gdpr_consent/src/main.ts b/gdpr_consent/src/main.ts index f7219ff..059fef8 100644 --- a/gdpr_consent/src/main.ts +++ b/gdpr_consent/src/main.ts @@ -114,6 +114,16 @@ function onRender(event: Event): void { } ) } + if (data.args['matomo']) { + klaroConfig.services.push( + { + name: 'matomo', + purposes: ['analytics'], + onAccept: callback, + onDecline: callback, + } + ) + } // Create a new script element var script = document.createElement('script') diff --git a/hooks/hook-analytics.py b/hooks/hook-analytics.py index 6b8b2da..c47f0c2 100644 --- a/hooks/hook-analytics.py +++ b/hooks/hook-analytics.py @@ -56,6 +56,21 @@ def piwik_pro_body(piwik_tag): """ +def matomo_head(matomo_url, matomo_tag): + return f""" + + + + """ + + if __name__ == '__main__': # Load configuration @@ -79,6 +94,12 @@ def piwik_pro_body(piwik_tag): piwik_tag = settings['analytics']['piwik-pro']['tag'] index = patch_body(index, piwik_pro_body(piwik_tag)) + # Configure matomo tag manager + if settings['analytics']['matomo']['enabled']: + matomo_url = settings['analytics']['matomo']['url'] + matomo_tag = settings['analytics']['matomo']['tag'] + index = patch_head(index, matomo_head(matomo_url, matomo_tag)) + # Save index.html with open(index_path, 'w') as f: f.write(index) \ No newline at end of file diff --git a/settings.json b/settings.json index d532bf9..f792ddc 100644 --- a/settings.json +++ b/settings.json @@ -9,8 +9,13 @@ "tag": "" }, "piwik-pro": { + "enabled": false, + "tag": "" + }, + "matomo": { "enabled": true, - "tag": "57690c44-d635-43b0-ab43-f8bd3064ca06" + "url": "https://cdn.matomo.cloud/openms.matomo.cloud", + "tag": "yDGK8bfY" } }, "online_deployment": false, diff --git a/src/common/captcha_.py b/src/common/captcha_.py index 5c7afd7..498b133 100644 --- a/src/common/captcha_.py +++ b/src/common/captcha_.py @@ -208,12 +208,13 @@ def captcha_control(): # Check if consent for tracking was given ga = st.session_state.settings['analytics']['google-analytics']['enabled'] pp = st.session_state.settings['analytics']['piwik-pro']['enabled'] - if (ga or pp) and (st.session_state.tracking_consent is None): + mt = st.session_state.settings['analytics']['matomo']['enabled'] + if (ga or pp or mt) and (st.session_state.tracking_consent is None): consent_component = st_components.declare_component("gdpr_consent", path=Path("gdpr_consent")) with st.spinner(): # Ask for consent st.session_state.tracking_consent = consent_component( - google_analytics=ga, piwik_pro=pp + google_analytics=ga, piwik_pro=pp, matomo=mt ) if st.session_state.tracking_consent is None: # No response by user yet diff --git a/src/common/common.py b/src/common/common.py index 4d24e76..b29d14f 100644 --- a/src/common/common.py +++ b/src/common/common.py @@ -405,6 +405,23 @@ def page_setup(page: str = "") -> dict[str, Any]: width=1, height=1, ) + if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and ( + st.session_state.tracking_consent["matomo"] == True + ): + html( + """ + + + + + + """, + width=1, + height=1, + ) # Determine the workspace for the current session if ("workspace" not in st.session_state) or ( diff --git a/test_gui.py b/test_gui.py index 101865c..10f13b6 100644 --- a/test_gui.py +++ b/test_gui.py @@ -93,7 +93,10 @@ def test_view_raw_ms_data(launch, example): # Copy files from example-data/mzML to workspace mzML directory, add to selected files for f in Path("example-data", "mzML").glob("*.mzML"): - shutil.copy(f, mzML_dir) + try: + shutil.copy(f, mzML_dir) + except shutil.SameFileError: + pass # File already exists as a symlink to the same source (on Linux) launch.run() ## TODO: Figure out a way to select a spectrum to be displayed @@ -119,7 +122,10 @@ def test_run_workflow(launch, example): # Copy files from example-data/mzML to workspace mzML directory, add to selected files for f in Path("example-data", "mzML").glob("*.mzML"): - shutil.copy(f, mzML_dir) + try: + shutil.copy(f, mzML_dir) + except shutil.SameFileError: + pass # File already exists as a symlink to the same source (on Linux) launch.run() ## Select experiments to process diff --git a/tests/test_parameter_presets.py b/tests/test_parameter_presets.py index 5104abc..30e0fc0 100644 --- a/tests/test_parameter_presets.py +++ b/tests/test_parameter_presets.py @@ -16,19 +16,33 @@ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PROJECT_ROOT) -# Create mock for streamlit before importing ParameterManager +# Mock streamlit before importing ParameterManager so that the imported module +# uses a controllable `st.session_state` (a plain dict) instead of the real one, +# which requires a running Streamlit app context. This allows unit-testing +# ParameterManager's preset logic (apply_preset, clear_parameter_session_state) +# in isolation. mock_streamlit = MagicMock() mock_streamlit.session_state = {} -sys.modules['streamlit'] = mock_streamlit -# Create mock for pyopenms -mock_pyopenms = MagicMock() -mock_pyopenms.__version__ = "2.9.1" -sys.modules['pyopenms'] = mock_pyopenms +# Temporarily replace streamlit in sys.modules so that ParameterManager's +# `import streamlit as st` picks up the mock. Restore immediately after import +# so other test files (e.g., test_gui.py AppTest) get the real streamlit. +_original_streamlit = sys.modules.get('streamlit') +sys.modules['streamlit'] = mock_streamlit -# Now import after mocks are set up from src.workflow.ParameterManager import ParameterManager +if _original_streamlit is not None: + sys.modules['streamlit'] = _original_streamlit +else: + sys.modules.pop('streamlit', None) + +# Remove cached src.workflow modules that were imported with mocked streamlit so +# that AppTest (in test_gui.py) re-imports them fresh with the real package. +for _key in list(sys.modules.keys()): + if _key.startswith('src.workflow'): + sys.modules.pop(_key, None) + @pytest.fixture def temp_workflow_dir(): diff --git a/tests/test_topp_workflow_parameter.py b/tests/test_topp_workflow_parameter.py index fa4d75e..20ea1d2 100644 --- a/tests/test_topp_workflow_parameter.py +++ b/tests/test_topp_workflow_parameter.py @@ -14,11 +14,6 @@ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PROJECT_ROOT) -# Create mock for pyopenms to avoid dependency on actual OpenMS installation -mock_pyopenms = MagicMock() -mock_pyopenms.__version__ = "2.9.1" # Mock version for testing -sys.modules['pyopenms'] = mock_pyopenms - @pytest.fixture def mock_streamlit(): """Mock essential Streamlit components for testing parameter display.""" @@ -47,12 +42,6 @@ def mock_streamlit(): } -def test_mock_pyopenms(): - """Verify that pyopenms mock is working correctly.""" - import pyopenms - assert hasattr(pyopenms, '__version__') - - def test_topp_parameter_correctness(): """Test that TOPP parameters are displayed with correct values.""" # Define expected parameters with values