diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 0089b4061d..060cf8546b 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -9,16 +9,16 @@ jobs: strategy: fail-fast: false matrix: - debian-version: ["11", "12"] + debian-version: ['11', '12'] include: - - debian-version: "11" - python-version: "3.9" - postgres-version: "13" - postgis-version: "3.2" - - debian-version: "12" - python-version: "3.11" - postgres-version: "15" - postgis-version: "3.3" + - debian-version: '11' + python-version: '3.9' + postgres-version: '13' + postgis-version: '3.2' + - debian-version: '12' + python-version: '3.11' + postgres-version: '15' + postgis-version: '3.3' name: Debian ${{ matrix.debian-version }} @@ -50,7 +50,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "pip" + cache: 'pip' - name: Install GDAL run: | sudo apt update diff --git a/.github/workflows/eval_perf.yml b/.github/workflows/eval_perf.yml new file mode 100644 index 0000000000..c6fbe44030 --- /dev/null +++ b/.github/workflows/eval_perf.yml @@ -0,0 +1,137 @@ +name: Performance Evaluation +on: + workflow_dispatch: + +jobs: + build: + runs-on: self-hosted + + strategy: + fail-fast: false + matrix: + debian-version: ["11", "12"] + include: + - debian-version: "11" + python-version: "3.9" + postgres-version: "13" + postgis-version: "3.2" + - debian-version: "12" + python-version: "3.11" + postgres-version: "15" + postgis-version: "3.3" + + name: Debian ${{ matrix.debian-version }} + + services: + postgres: + image: postgis/postgis:${{ matrix.postgres-version }}-${{ matrix.postgis-version }} + env: + POSTGRES_DB: geonature2db + POSTGRES_PASSWORD: geonatpasswd + POSTGRES_USER: geonatadmin + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Add database extensions + run: | + psql -h localhost -U geonatadmin -d geonature2db -f install/assets/db/add_pg_extensions.sql + env: + PGPASSWORD: geonatpasswd + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + - name: Install GDAL + run: | + sudo apt update + sudo apt install -y libgdal-dev + - name: Install dependencies + if: github.base_ref == 'master' + run: | + echo 'Installation des requirements de prod' + python -m pip install --upgrade pip + python -m pip install \ + -e ..[tests] \ + -r requirements.txt + working-directory: ./backend + - name: Install dependencies + if: github.base_ref != 'master' + run: | + echo 'Installation des requirements de dev' + python -m pip install --upgrade pip + python -m pip install \ + -e ..[tests] \ + -r requirements-dev.in + working-directory: ./backend + - name: Show database branches and dependencies + run: | + geonature db status --dependencies + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + # - name: Restore database + # run: | + # # wget https://www.dropbox.com/scl/fi/17gsthsftfg59mxwmbbre/export_geonature_10000.zip?rlkey=33choleag4xw60wadm802c3oh&dl=1 -O 10kDump.zip + # # unzip 10kDump.zip + # wget https://www.dropbox.com/scl/fi/jjkxyg120bxc0dp8uy8kq/300KDump.sql?rlkey=tyuk2svitcb9nyshn7r09yo7b&dl=1 -O 300KDump.sql + # ls + # psql -h localhost -U geonatadmin -d geonature2db -f 300KDump.sql + # env: + # PGPASSWORD: geonatpasswd + - name: Install database + run: | + install/03b_populate_db.sh + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + srid_local: 2154 + install_bdc_statuts: true + add_sample_data: true + install_sig_layers: true + install_grid_layer_5: true + install_grid_layer_10: true + install_ref_sensitivity: true + - name: Show database status + run: | + geonature db status + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + + - name: Install core modules backend + run: | + pip install -e contrib/occtax + pip install -e contrib/gn_module_occhab + pip install -e contrib/gn_module_validation + - name: Show database status + run: | + geonature db status + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + - name: Install core modules database + run: | + geonature upgrade-modules-db + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + - name: Show database status + run: | + geonature db status --dependencies + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + - name: Load benchmark stable data + run: | + wget https://geonature.fr/data/benchmark_history/benchmark_stable.json -O benchmark_stable.json + + - name: Compare performance to stable data + run: | + pytest --benchmark-only --benchmark-compare-fail="mean:0.1" --benchmark-compare=benchmark_stable.json + env: + GEONATURE_CONFIG_FILE: config/test_config.toml + # https://stackoverflow.com/a/64126737 For posting results on GitHub Pull Requests diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c9de492237..32c6b218c4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -19,16 +19,16 @@ jobs: strategy: fail-fast: false matrix: - debian-version: ["11", "12"] + debian-version: ['11', '12'] include: - - debian-version: "11" - python-version: "3.9" - postgres-version: "13" - postgis-version: "3.2" - - debian-version: "12" - python-version: "3.11" - postgres-version: "15" - postgis-version: "3.3" + - debian-version: '11' + python-version: '3.9' + postgres-version: '13' + postgis-version: '3.2' + - debian-version: '12' + python-version: '3.11' + postgres-version: '15' + postgis-version: '3.3' name: Debian ${{ matrix.debian-version }} @@ -60,7 +60,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "pip" + cache: 'pip' - name: Install GDAL run: | sudo apt update @@ -122,7 +122,7 @@ jobs: GEONATURE_CONFIG_FILE: config/test_config.toml - name: Test with pytest run: | - pytest -v --cov --cov-report xml + pytest -v --cov --cov-report xml --benchmark-skip env: GEONATURE_CONFIG_FILE: config/test_config.toml - name: Upload coverage to Codecov diff --git a/backend/geonature/tests/benchmarks/__init__.py b/backend/geonature/tests/benchmarks/__init__.py new file mode 100644 index 0000000000..86fd72efad --- /dev/null +++ b/backend/geonature/tests/benchmarks/__init__.py @@ -0,0 +1,5 @@ +# Import required for CLater class when using eval() +from flask import url_for +from .benchmark_data import * +from geonature.core.gn_synthese.models import Synthese +from sqlalchemy import select diff --git a/backend/geonature/tests/benchmarks/benchmark_data.py b/backend/geonature/tests/benchmarks/benchmark_data.py new file mode 100644 index 0000000000..75abbc17b7 --- /dev/null +++ b/backend/geonature/tests/benchmarks/benchmark_data.py @@ -0,0 +1,599 @@ +from geonature.utils.env import db +import pytest +from ref_geo.models import BibAreasTypes, LAreas +from sqlalchemy import select + +benchmark_synthese_intersection_data_test_bbox = { + "modif_since_validation": False, + "geoIntersection": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [5.828785, 44.632571], + [5.828785, 45.06773], + [6.625493, 45.06773], + [6.625493, 44.632571], + [5.828785, 44.632571], + ] + ], + }, + }, +} + +benchmark_synthese_intersection_data_test_complex_polygon = { + "modif_since_validation": False, + "geoIntersection": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [6.063715, 44.968659], + [5.974429, 44.981293], + [5.955198, 44.934631], + [5.98256, 44.890826], + [6.040252, 44.867462], + [6.085582, 44.860646], + [6.125418, 44.884012], + [6.141901, 44.876225], + [6.135038, 44.837262], + [6.099322, 44.820697], + [6.064979, 44.804128], + [6.070473, 44.750493], + [6.133664, 44.731947], + [6.176247, 44.756341], + [6.189983, 44.777799], + [6.19273, 44.731947], + [6.261412, 44.737803], + [6.273775, 44.760243], + [6.319105, 44.741706], + [6.279269, 44.688002], + [6.231192, 44.656741], + [6.483941, 44.672375], + [6.396028, 44.628395], + [6.486688, 44.596121], + [6.538887, 44.655764], + [6.533392, 44.688981], + [6.500425, 44.73877], + [6.533392, 44.748528], + [6.514161, 44.790467], + [6.41938, 44.812887], + [6.488062, 44.854779], + [6.536139, 44.856727], + [6.597938, 44.894757], + [6.518262, 44.934648], + [6.437214, 44.932702], + [6.391881, 44.883076], + [6.356164, 44.86068], + [6.251768, 44.874311], + [6.284735, 44.921996], + [6.317703, 44.952143], + [6.385011, 44.964781], + [6.364406, 44.972557], + [6.449572, 44.976444], + [6.479792, 44.984218], + [6.468803, 45.027928], + [6.413857, 45.043461], + [6.336928, 45.055017], + [6.194069, 45.048223], + [6.170716, 44.952049], + [6.161099, 44.949131], + [6.063715, 44.968659], + ] + ], + }, + }, +} + + +def benchmark_synthese_intersection_data_test_commune(): + return { + "modif_since_validation": False, + "area_COM": [ + db.session.scalars( + select(LAreas).join(BibAreasTypes).where(BibAreasTypes.type_code == "COM").limit(1) + ) + .one() + .id_area + ], + } + + +def benchmark_synthese_intersection_data_test_departement(): + return { + "modif_since_validation": False, + "area_DEP": [ + db.session.scalars( + select(LAreas.id_area) + .join(BibAreasTypes) + .where(BibAreasTypes.type_code == "DEP") + .limit(1) + ).first() + ], + } + + +def benchmark_synthese_intersection_data_test_region(): + return { + "modif_since_validation": False, + "area_REG": [ + db.session.scalars( + select(LAreas.id_area) + .join(BibAreasTypes) + .where(BibAreasTypes.type_code == "REG") + .limit(1) + ).first() + ], + } + + +benchmark_synthese_with_tree_taxon = { + "modif_since_validation": False, + "cd_ref_parent": [ + 183787, + 183767, + 183839, + 183840, + 183843, + 183874, + 184358, + 184357, + 184355, + 184354, + 184353, + 184367, + 184376, + 927956, + 200726, + 184350, + 184351, + 184366, + 184363, + 184362, + 184360, + 184359, + 184377, + 184379, + 184374, + 184371, + 184369, + 184368, + 184381, + 184393, + 184391, + 184390, + 184385, + 184387, + 184388, + 184378, + 184438, + 184449, + 184450, + 184493, + 1007942, + 184501, + 184486, + 184488, + 184491, + 184499, + 184492, + 184494, + 200765, + 184737, + 184740, + 184743, + 184752, + 184756, + 184762, + 184766, + 184767, + 184768, + 184771, + 184773, + 184775, + 184776, + 184772, + 184615, + 184616, + 184618, + 184630, + 184643, + 188023, + 200773, + 793340, + 184644, + 184646, + 184649, + 184650, + 184653, + 184658, + 184660, + 184662, + 184666, + 184667, + 184668, + 184670, + 184671, + 184672, + 184674, + 184676, + 184680, + 184682, + 184684, + 184685, + 184689, + 184694, + 184696, + 184699, + 184701, + 184702, + 184703, + 184706, + 184708, + 184709, + 184711, + 184712, + 184714, + 184715, + 184718, + 184720, + 184728, + 184732, + 184734, + 184735, + 184781, + 184791, + 184802, + 184801, + 184890, + 184808, + 184810, + 184815, + 184831, + 184836, + 184838, + 184839, + 184843, + 184847, + 184848, + 184852, + 184853, + 184855, + 184862, + 184865, + 184867, + 184870, + 184872, + 184874, + 184877, + 184879, + 184880, + 184882, + 184883, + 184885, + 184888, + 184889, + 184891, + 184892, + 184894, + 184896, + 184900, + 184907, + 184927, + 184944, + 184947, + 184948, + 184949, + 184950, + 184952, + 839211, + 184961, + 184976, + 185027, + 185028, + 185030, + 185033, + 185043, + 185046, + 185051, + 185064, + 185075, + 185076, + 185077, + 185078, + 185023, + 185081, + 185082, + 185084, + 185085, + 185088, + 185090, + 185124, + 185080, + 185025, + 645275, + 184994, + 834425, + 839203, + 938486, + 184992, + 184988, + 184984, + 184983, + 938483, + 938487, + 185012, + 185204, + 185211, + 188009, + 827922, + 728177, + 187977, + 827924, + 827925, + 714463, + 185129, + 939246, + 185130, + 185131, + 185133, + 185150, + 185157, + 185162, + 185171, + 185188, + 185189, + 185190, + 185193, + 185194, + 185197, + 714275, + 648561, + 610362, + 602182, + 200681, + 185216, + 185220, + 185223, + 185224, + 185226, + 185230, + 185232, + 185234, + 185240, + 185242, + 185244, + 185245, + 185247, + 185249, + 185251, + 185253, + 185254, + 185257, + 185259, + 185263, + 185266, + 185268, + 185270, + 185273, + 185274, + 185276, + 185278, + 185280, + 185285, + 185286, + 185287, + 185290, + 185291, + 185292, + 185297, + 185302, + 185307, + 185308, + 185309, + 185313, + 185322, + 185323, + 185324, + 185327, + 185330, + 714314, + 185334, + 185352, + 185346, + 185347, + 185351, + 185369, + 185372, + 185371, + 185365, + 185364, + 185362, + 185360, + 185359, + 185374, + 185378, + 185388, + 185389, + 185399, + 437022, + 185383, + 185385, + 185468, + 185467, + 185471, + 185472, + 185473, + 185517, + 185566, + 185554, + 185568, + 185557, + 185582, + 185580, + 185578, + 528744, + 185401, + 185590, + 185593, + 185599, + 185596, + 185594, + 185602, + 185604, + 185621, + 185635, + 185652, + 185651, + 185649, + 185669, + 185674, + 186286, + 699191, + 618462, + 186293, + 186292, + 186283, + 185770, + 185781, + 185830, + 185832, + 185896, + 185900, + 185760, + 905695, + 904968, + 827284, + 825345, + 185967, + 185979, + 186008, + 186010, + 186012, + 186000, + 186006, + 185998, + 186058, + 186067, + 186062, + 186076, + 186091, + 186084, + 186094, + 186101, + 885360, + 885396, + 885400, + 885430, + 186121, + 186117, + 186115, + 186110, + 186135, + 186107, + 186108, + 186137, + 186139, + 186141, + 186145, + 186146, + 186148, + 186152, + 186154, + 186158, + 186160, + 186130, + 186129, + 186124, + 186122, + 828783, + 186168, + 186178, + 186176, + 186027, + 186047, + 186021, + 186019, + 186038, + 186025, + 351923, + 186173, + 185985, + 186055, + 186053, + 185990, + 186221, + 186214, + 186223, + 186215, + 186238, + 186237, + 186239, + 699095, + 186245, + 186257, + 186259, + 655502, + 199825, + 443494, + 655482, + 186210, + 186209, + 186211, + 186241, + 186243, + 186242, + 199959, + 444434, + 185954, + 185951, + 185946, + 185960, + 186330, + 933748, + 186337, + 186332, + 186374, + 186376, + 186365, + 186377, + 186346, + 186409, + 186380, + 186383, + 186386, + 186388, + 186390, + 186391, + 186392, + 186393, + 186396, + 186399, + 186400, + 186401, + 186404, + 186406, + 186407, + 186415, + 186418, + 186422, + 186423, + 186425, + 186426, + 186430, + 187541, + 851431, + 851437, + 851464, + 186356, + 810391, + 699677, + 186353, + 186369, + 186997, + ], +} diff --git a/backend/geonature/tests/benchmarks/benchmark_generator.py b/backend/geonature/tests/benchmarks/benchmark_generator.py new file mode 100644 index 0000000000..4f80e2f483 --- /dev/null +++ b/backend/geonature/tests/benchmarks/benchmark_generator.py @@ -0,0 +1,96 @@ +from typing import Any +from geonature.tests.utils import set_logged_user +from geonature.tests.fixtures import users + +import importlib +from geonature.tests.benchmarks import * + + +class CLater: + def __init__(self, value) -> None: + self.value = value + + +class BenchmarkTest: + """ + Class that allows to define a benchmark test and generate the pytest function to run the benchmark. + + Example, in a pytest file: + ```python + import pytest + bench = BenchmarkTest(print,"test_print",["Hello","World"],{}) + @pytest.mark.usefixtures("client_class", "temporary_transaction") + class TestBenchie: + pass + TestBenchie.test_print = bench() + ``` + + If a function or its argument depend on the pytest function context, use the GetLatter class : GetLatter("). For example, to use + the `url_for()` function, replace from `url_for(...)` to `GetLatter("url_for(...)")`. + + If the benchmark requires a user to be logged, use the `function_kwargs` with the "user_profile" key and the value corresponds to a key + available in the dictionary returned by the `user` fixture. + + + """ + + def __init__(self, function, function_args=[], function_kwargs={}) -> None: + """ + Constructor of BenchmarkTest + + Parameters + ---------- + function : Callable | GetLatter + function that will be benchmark + function_args : Sequence[Any | GetLatter] + args for the function + function_kwargs : Dict[str,Any] + kwargs for the function + """ + self.function = function + self.function_args = function_args + self.function_kwargs = function_kwargs + + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.generate_func_test() + + def generate_func_test(self): + """ + Return the pytest function to run the benchmark on the indicated function. + + Returns + ------- + Callable + test function + + Raises + ------ + KeyError + if the user_profile given do not exists + """ + + fixtures = self.function_kwargs.pop("fixtures", []) + user_profile = self.function_kwargs.pop("user_profile", None) + + func, args, kwargs = self.function, self.function_args, self.function_kwargs + + def function_to_include_fixture(*fixture): + + def final_test_function(self, benchmark, users): + + if user_profile: + if not user_profile in users: + raise KeyError(f"{user_profile} can't be found in the users fixture !") + set_logged_user(self.client, users[user_profile]) + benchmark( + eval(func.value) if isinstance(func, CLater) else func, + *[eval(arg.value) if isinstance(arg, CLater) else arg for arg in args], + **{ + key: eval(value.value) if isinstance(value, CLater) else value + for key, value in kwargs.items() + }, + ) + + return final_test_function + + return function_to_include_fixture(*fixtures) diff --git a/backend/geonature/tests/benchmarks/test_benchmark_gn_meta.py b/backend/geonature/tests/benchmarks/test_benchmark_gn_meta.py new file mode 100644 index 0000000000..aadf43f64c --- /dev/null +++ b/backend/geonature/tests/benchmarks/test_benchmark_gn_meta.py @@ -0,0 +1,29 @@ +import logging +import pytest +from geonature.tests.benchmarks import * +from geonature.tests.test_pr_occhab import stations + +from .benchmark_generator import BenchmarkTest, CLater +from .utils import activate_profiling_sql + +logging.basicConfig() +logger = logging.getLogger("logger-name") +logger.setLevel(logging.DEBUG) + +from .utils import CLIENT_GET, CLIENT_POST + + +@pytest.mark.benchmark(group="gn_meta") +@pytest.mark.usefixtures("client_class", "temporary_transaction", "activate_profiling_sql") +class TestBenchmarkGnMeta: + + test_list_acquisition_frameworks = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("gn_meta.get_acquisition_frameworks_list")""")], + dict(user_profile="admin_user", fixtures=[]), + )() + test_list_datasets = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("gn_meta.get_datasets")""")], + dict(user_profile="admin_user", fixtures=[]), + )() diff --git a/backend/geonature/tests/benchmarks/test_benchmark_occhab.py b/backend/geonature/tests/benchmarks/test_benchmark_occhab.py new file mode 100644 index 0000000000..0cee7edbd3 --- /dev/null +++ b/backend/geonature/tests/benchmarks/test_benchmark_occhab.py @@ -0,0 +1,48 @@ +import logging +import pytest +from geonature.tests.benchmarks import * +from geonature.tests.test_pr_occhab import stations + +from .benchmark_generator import BenchmarkTest, CLater +from .utils import activate_profiling_sql + +logging.basicConfig() +logger = logging.getLogger("logger-name") +logger.setLevel(logging.DEBUG) + +from .utils import CLIENT_GET, CLIENT_POST + + +@pytest.mark.benchmark(group="occhab") +@pytest.mark.usefixtures("client_class", "temporary_transaction", "activate_profiling_sql") +class TestBenchmarkOcchab: + + test_get_station = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("occhab.get_station", id_station=8)""")], + dict(user_profile="user", fixtures=[stations]), + )() + + test_list_stations = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("occhab.list_stations")""")], + dict(user_profile="admin_user", fixtures=[]), + )() + + test_list_stations_restricted = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("occhab.list_stations")""")], + dict(user_profile="user_restricted_occhab", fixtures=[]), + )() + + +for format_ in "csv geojson shapefile".split(): + setattr( + TestBenchmarkOcchab, + f"test_export_all_habitats_{format_}", + BenchmarkTest( + CLIENT_POST, + [CLater("""url_for("occhab.export_all_habitats",export_format="csv")""")], + dict(user_profile="admin_user", fixtures=[]), + )(), + ) diff --git a/backend/geonature/tests/benchmarks/test_benchmark_occtax.py b/backend/geonature/tests/benchmarks/test_benchmark_occtax.py new file mode 100644 index 0000000000..092604b3ec --- /dev/null +++ b/backend/geonature/tests/benchmarks/test_benchmark_occtax.py @@ -0,0 +1,30 @@ +import logging +import pytest +from geonature.tests.benchmarks import * +from geonature.tests.test_pr_occhab import stations + +from .benchmark_generator import BenchmarkTest, CLater + +from .utils import activate_profiling_sql + +logging.basicConfig() +logger = logging.getLogger("logger-name") +logger.setLevel(logging.DEBUG) + +from .utils import CLIENT_GET, CLIENT_POST + + +@pytest.mark.benchmark(group="occtax") +@pytest.mark.usefixtures("client_class", "temporary_transaction", "activate_profiling_sql") +class TestBenchmarkOcctax: + + test_list_releves_restricted = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("pr_occtax.getReleves")""")], + dict(user_profile="user_restricted_occhab", fixtures=[]), + )() + test_list_releves_unrestricted = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("pr_occtax.getReleves")""")], + dict(user_profile="admin_user", fixtures=[]), + )() diff --git a/backend/geonature/tests/benchmarks/test_benchmark_synthese.py b/backend/geonature/tests/benchmarks/test_benchmark_synthese.py new file mode 100644 index 0000000000..c757dbff0c --- /dev/null +++ b/backend/geonature/tests/benchmarks/test_benchmark_synthese.py @@ -0,0 +1,108 @@ +import logging + +import pytest +from geonature.tests.benchmarks import * +from geonature.tests.test_pr_occhab import stations +from geonature.core.gn_synthese.models import Synthese +from .utils import activate_profiling_sql + +from .benchmark_generator import BenchmarkTest, CLater + + +logging.basicConfig() +logger = logging.getLogger("logger-name") +logger.setLevel(logging.DEBUG) + +from .utils import CLIENT_GET, CLIENT_POST, add_bluring_to_benchmark_test_class + +SYNTHESE_GET_OBS_URL = """url_for("gn_synthese.get_observations_for_web")""" +SYNTHESE_EXPORT_OBS_URL = """url_for("gn_synthese.export_observations_web")""" +SYNTHESE_EXPORT_STATUS_URL = """url_for("gn_synthese.export_status")""" +SYNTHESE_EXPORT_TAXON_WEB_URL = """url_for("gn_synthese.export_taxon_web")""" + + +@pytest.mark.benchmark(group="synthese") +@pytest.mark.usefixtures("client_class", "temporary_transaction", "activate_profiling_sql") +class TestBenchmarkSynthese: + # GET NOMENCLATURE + test_get_default_nomenclatures = BenchmarkTest( + CLIENT_GET, + [CLater("""url_for("gn_synthese.getDefaultsNomenclatures")""")], + dict(user_profile="self_user"), + ) + + test_with_geometry_bbox = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=benchmark_synthese_intersection_data_test_bbox, + ), + ) + + test_with_geometry_complex_poly = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=benchmark_synthese_intersection_data_test_complex_polygon, + ), + ) + test_with_commune = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=CLater("benchmark_data.benchmark_synthese_intersection_data_test_commune()"), + ), + ) + + test_with_departement = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=CLater("benchmark_data.benchmark_synthese_intersection_data_test_departement()"), + ), + ) + test_with_region = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=CLater("benchmark_data.benchmark_synthese_intersection_data_test_region()"), + ), + ) + test_with_up_tree_taxon = BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_GET_OBS_URL)], + dict( + user_profile="admin_user", + json=benchmark_synthese_with_tree_taxon, + ), + ) + + +# EXPORT TESTING +for url, label in [ + (SYNTHESE_EXPORT_STATUS_URL, "status"), + (SYNTHESE_EXPORT_TAXON_WEB_URL, "taxons"), + (SYNTHESE_EXPORT_OBS_URL, "observations"), +]: + for n_obs in [1000, 10000, 100000, 1000000]: + setattr( + TestBenchmarkSynthese, + f"test_export_{label}_{n_obs}", + BenchmarkTest( + CLIENT_POST, + [CLater(SYNTHESE_EXPORT_OBS_URL)], + dict( + user_profile="admin_user", + json=CLater( + f"db.session.execute(select(Synthese.id_synthese).limit({n_obs})).all()" + ), + ), + ), + ) + +add_bluring_to_benchmark_test_class(TestBenchmarkSynthese) diff --git a/backend/geonature/tests/benchmarks/utils.py b/backend/geonature/tests/benchmarks/utils.py new file mode 100644 index 0000000000..eeb049b54e --- /dev/null +++ b/backend/geonature/tests/benchmarks/utils.py @@ -0,0 +1,105 @@ +import time +import logging +import os + +import pytest +import pandas +from sqlalchemy import event + +from geonature.utils.env import db +from .benchmark_generator import CLater, BenchmarkTest +from geonature.tests.test_synthese import blur_sensitive_observations +import traceback +from geonature.tests.fixtures import app + +logging.basicConfig() +logger = logging.getLogger("logger-name") +logger.setLevel(logging.DEBUG) + + +@pytest.fixture(scope="function") +def activate_profiling_sql(sqllogfilename: pytest.FixtureDef, app: pytest.FixtureDef): + """ + Fixture to activate profiling for SQL queries and store query's statements and execution times in a CSV file. + + This fixture takes a `sqllogfilename` parameter, which is the path to a CSV file where the query statements and + execution times will be stored. If no `sqllogfilename` is provided, the SQL profiling will not be activated. + + Parameters + ---------- + - sqllogfilename: pytest.FixtureDef + The path to the CSV file where the query statements and execution times will be stored. + + """ + columns = ["Endpoint", "Query", "Total Time [s.]"] + + if not sqllogfilename: + logger.debug("No SQL Log file provided. SQL Profiling will not be activated.") + return + + directory = os.path.dirname(sqllogfilename) + if directory and not os.path.exists(directory): + raise FileNotFoundError(f"Directory {directory} does not exists ! ") + + if not os.path.exists(sqllogfilename): + df = pandas.DataFrame([], columns=columns) + df.to_csv(sqllogfilename, header=True, index=None, sep=";") + + def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): + conn.info.setdefault("query_start_time", []).append(time.time()) + logger.debug("Start Query: %s" % statement) + + # @event.listens_for(Engine, "after_cursor_execute") + def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + total = time.time() - conn.info["query_start_time"].pop(-1) + logger.debug("Query Complete!") + logger.debug("Total Time: %f" % total) + if statement.startswith("SELECT"): + df = pandas.DataFrame([[pytest.endpoint, statement, total]], columns=columns) + df.to_csv(sqllogfilename, mode="a", header=False, index=None, sep=";") + + event.listen(db.engine, "before_cursor_execute", before_cursor_execute) + event.listen(db.engine, "after_cursor_execute", after_cursor_execute) + + +def add_bluring_to_benchmark_test_class(benchmark_cls: type): + """ + Add the blurring enabling fixture to all benchmark tests declared in the given class. + + Parameters + ---------- + benchmark_cls : type + benchmark test class + """ + for attr in dir(benchmark_cls): + if attr.startswith("test_"): + b_test = getattr(benchmark_cls, attr) + + # If attribute does not corresponds to a BenchmarkTest, skip + if not isinstance(b_test, BenchmarkTest): + continue + + # Include blurring fixture + kwargs = b_test.function_kwargs + kwargs["fixtures"] = ( + kwargs["fixtures"] + [blur_sensitive_observations] + if "fixtures" in kwargs + else [blur_sensitive_observations] + ) + # Recreate BenchmarkTest object including the blurring enabling fixture + setattr( + benchmark_cls, + f"{attr}_with_blurring", + BenchmarkTest( + b_test.function, b_test.function_args, kwargs + )(), # Run the test function generation while we're at it + ) + # Generate the test function from the orginal `BenchmarkTest`s + setattr( + benchmark_cls, + attr, + b_test(), + ) + + +CLIENT_GET, CLIENT_POST = CLater("self.client.get"), CLater("self.client.post") diff --git a/backend/geonature/tests/conftest.py b/backend/geonature/tests/conftest.py index ca9ffcb72b..1b05115836 100644 --- a/backend/geonature/tests/conftest.py +++ b/backend/geonature/tests/conftest.py @@ -1,3 +1,15 @@ # force discovery of some fixtures from .fixtures import app, users, _session from pypnusershub.tests.fixtures import teardown_logout_user +import pytest + +pytest.endpoint = "" + + +def pytest_addoption(parser): + parser.addoption("--sql-log-filename", action="store", default=None) + + +@pytest.fixture(scope="session") +def sqllogfilename(request): + return request.config.getoption("--sql-log-filename") diff --git a/backend/geonature/tests/fixtures.py b/backend/geonature/tests/fixtures.py index a4785b2f63..bffb671cc4 100644 --- a/backend/geonature/tests/fixtures.py +++ b/backend/geonature/tests/fixtures.py @@ -7,7 +7,7 @@ import pytest import sqlalchemy as sa -from flask import current_app, testing, url_for +from flask import current_app, testing, url_for, request from geoalchemy2.shape import from_shape from PIL import Image from shapely.geometry import Point @@ -97,6 +97,10 @@ def app(): app.test_client_class = GeoNatureClient app.config["SERVER_NAME"] = "test.geonature.fr" # required by url_for + @app.before_request + def get_endpoint(): + pytest.endpoint = request.endpoint + with app.app_context(): """ Note: This may seem redundant with 'temporary_transaction' fixture. @@ -334,10 +338,18 @@ def create_user( 2, False, [], - {"C": 2, "OCCHAB": {"R": 2, "U": 1, "E": 2, "D": 1}}, + { + "C": 2, + "OCCHAB": {"R": 2, "U": 1, "E": 2, "D": 1}, + "OCCTAX": {"R": 2, "U": 1, "E": 2, "D": 1}, + }, ), {}, ), + ( + ("user_with_blurring", organisme, 1, True, [], {}), + {}, + ), ] for (username, *args), kwargs in users_to_create: diff --git a/setup.py b/setup.py index a87c593671..9695ffdc23 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,15 @@ install_requires=list(open("backend/requirements-common.in", "r")) + list(open("backend/requirements-dependencies.in", "r")), extras_require={ - "tests": ["pytest", "pytest-flask", "pytest-cov", "jsonschema", "pandas"], + "tests": [ + "pandas", + "pytest", + "pytest-flask", + "pytest-benchmark", + "pytest-cov", + "jsonschema", + "pandas", + ], "doc": [ "sphinx", "sphinx_rtd_theme",