From 4a9695ac0620d22cc0399665dbc166b683bab03a Mon Sep 17 00:00:00 2001 From: Ilay Falach Date: Tue, 14 Jul 2026 11:03:26 +0300 Subject: [PATCH] test: add pytest markers (unit/integration) to 9 legacy test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classify all 267 tests in the legacy test files with @pytest.mark.unit or @pytest.mark.integration so they are selectable via `pytest -m unit` (no MongoDB needed) and `pytest -m integration` (CI with MongoDB). Markers only — no test logic changed (diff is pure decorator insertions). Closes #964 Co-Authored-By: Claude Fable 5 --- hera/tests/test_datalayer.py | 27 +++++++++++++++++++++++++++ hera/tests/test_demography.py | 11 +++++++++++ hera/tests/test_experiment.py | 12 ++++++++++++ hera/tests/test_highfreq.py | 10 ++++++++++ hera/tests/test_landcover.py | 4 ++++ hera/tests/test_lowfreq.py | 10 ++++++++++ hera/tests/test_repository.py | 6 ++++++ hera/tests/test_topography.py | 8 ++++++++ hera/tests/test_workflow_execution.py | 7 +++++++ 9 files changed, 95 insertions(+) diff --git a/hera/tests/test_datalayer.py b/hera/tests/test_datalayer.py index 78f1d15cf..0bdced382 100644 --- a/hera/tests/test_datalayer.py +++ b/hera/tests/test_datalayer.py @@ -103,6 +103,7 @@ def clean_proj(): # 1. Project CRUD — all three collections # =========================================================================== +@pytest.mark.integration @requires_mongo class TestMeasurementsCRUD: """Add, get, get-as-dict, delete for Measurements collection.""" @@ -154,6 +155,7 @@ def test_delete(self, proj): assert len(remaining) == 0 +@pytest.mark.integration @requires_mongo class TestSimulationsCRUD: """Add, get, get-as-dict, delete for Simulations collection.""" @@ -193,6 +195,7 @@ def test_delete(self, proj): assert len(remaining) == 0 +@pytest.mark.integration @requires_mongo class TestCacheCRUD: """Add, get, get-as-dict, delete for Cache collection.""" @@ -236,6 +239,7 @@ def test_delete(self, proj): # 2. Nested queries (dictToMongoQuery) # =========================================================================== +@pytest.mark.integration @requires_mongo class TestNestedQueries: """Test querying with nested desc fields.""" @@ -273,6 +277,7 @@ def test_multi_level_nested_query(self, clean_proj): # 3. getAllDocuments — cross-collection query # =========================================================================== +@pytest.mark.integration @requires_mongo class TestGetAllDocuments: """Test getAllDocuments aggregates across collections.""" @@ -295,6 +300,7 @@ def test_returns_from_all_collections(self, clean_proj): # 4. getDocumentByID # =========================================================================== +@pytest.mark.integration @requires_mongo class TestGetDocumentByID: """Test retrieving a document by its MongoDB ID.""" @@ -313,6 +319,7 @@ def test_round_trip(self, clean_proj): # 5. getMetadata # =========================================================================== +@pytest.mark.integration @requires_mongo class TestGetMetadata: """Test getMetadata returns DataFrame of document descriptions.""" @@ -335,6 +342,7 @@ def test_returns_dataframe(self, clean_proj): # 6. addDocumentFromDict # =========================================================================== +@pytest.mark.integration @requires_mongo class TestAddDocumentFromDict: """Test adding documents from a dictionary representation.""" @@ -358,6 +366,7 @@ def test_add_measurements_from_dict(self, clean_proj): # 7. Counters # =========================================================================== +@pytest.mark.integration @requires_mongo class TestCounters: """Test counter operations: define, set, get, getAndAdd.""" @@ -401,6 +410,7 @@ def test_set_counter_overwrites(self, clean_proj): # 8. Configuration # =========================================================================== +@pytest.mark.integration @requires_mongo class TestConfiguration: """Test initConfig, setConfig, getConfig.""" @@ -439,6 +449,7 @@ def test_init_config_does_not_overwrite(self, clean_proj): # 9. Project properties # =========================================================================== +@pytest.mark.integration @requires_mongo class TestProjectProperties: """Test Project properties.""" @@ -468,6 +479,7 @@ def test_files_directory(self, proj): # 10. saveData family — round-trip with real files # =========================================================================== +@pytest.mark.integration @requires_mongo class TestSaveData: """Test saveMeasurementData, saveCacheData, saveSimulationData with real data.""" @@ -575,6 +587,7 @@ def test_save_pickle(self, clean_proj, tmp_dir): # 11. DataHandler round-trips (unit tests, no DB needed) # =========================================================================== +@pytest.mark.unit class TestDataHandlers: """Test DataHandler saveData/getData round-trips for key formats.""" @@ -639,6 +652,7 @@ def test_handler_pickle(self, tmp_dir): # 12. datatypes class methods # =========================================================================== +@pytest.mark.unit class TestDatatypes: """Test datatypes introspection methods.""" @@ -684,6 +698,7 @@ def test_get_data_format_extension(self): # 13. Collection direct access # =========================================================================== +@pytest.mark.integration @requires_mongo class TestCollectionDirect: """Test AbstractCollection and subclasses directly.""" @@ -809,6 +824,7 @@ def test_get_project_list(self): # 14. MetadataFrame # =========================================================================== +@pytest.mark.integration @requires_mongo class TestMetadataFrame: """Test MetadataFrame.asDict and MetadataFrame.getData.""" @@ -851,6 +867,7 @@ def test_get_data_string(self, clean_proj): # 15. nonDBMetadataFrame # =========================================================================== +@pytest.mark.unit class TestNonDBMetadataFrame: """Test nonDBMetadataFrame wrapping.""" @@ -889,6 +906,7 @@ def test_get_data_with_numpy(self): # 16. autocache — @cacheFunction decorator # =========================================================================== +@pytest.mark.integration @requires_mongo class TestAutoCache: """Test cacheFunction decorator with real MongoDB caching.""" @@ -952,6 +970,7 @@ def func_b(): # 17. Export / Import round-trip # =========================================================================== +@pytest.mark.integration @requires_mongo class TestExportImport: """Test Project export and load round-trip.""" @@ -1004,6 +1023,7 @@ def test_export_and_load(self, tmp_dir): # 18. Module-level functions # =========================================================================== +@pytest.mark.integration @requires_mongo class TestModuleFunctionsDB: """Test module-level functions that require MongoDB.""" @@ -1018,6 +1038,7 @@ def test_get_project_list(self, proj): assert PROJECT_NAME in projects +@pytest.mark.unit class TestModuleFunctionsLocal: """Test module-level functions that do NOT require MongoDB.""" @@ -1037,6 +1058,7 @@ def test_create_project_directory(self, tmp_dir): # 19. parseConnectionString (no DB, no config file) # =========================================================================== +@pytest.mark.unit class TestParseConnectionString: """Test connection string parsing — pure function, no side effects.""" @@ -1068,6 +1090,7 @@ def test_special_chars_in_password_raises(self): # 20. Config file management (mocked — no real config file touched) # =========================================================================== +@pytest.mark.unit class TestConfigFileManagement: """Test getMongoJSON, addOrUpdateDatabase, removeConnection, getDBNamesFromJSON. @@ -1190,6 +1213,7 @@ def test_addOrUpdateDatabase_no_config_raises(self, tmp_dir, monkeypatch): # 21. saveData with auto-format detection (requires MongoDB) # =========================================================================== +@pytest.mark.integration @requires_mongo class TestSaveDataAutoDetect: """Test that saveData auto-detects the format when dataFormat is not specified.""" @@ -1239,6 +1263,7 @@ def test_auto_detect_numpy(self, clean_proj, tmp_dir): # 22. Simulations/Cache getDocumentsAsDict (requires MongoDB) # =========================================================================== +@pytest.mark.integration @requires_mongo class TestSimulationsCacheAsDict: """Test getDocumentsAsDict for Simulations and Cache (Measurements already tested).""" @@ -1266,6 +1291,7 @@ def test_cache_as_dict(self, clean_proj): # 23. Empty project edge cases (requires MongoDB) # =========================================================================== +@pytest.mark.integration @requires_mongo class TestEmptyProjectEdgeCases: """Test queries against a project with no documents.""" @@ -1296,6 +1322,7 @@ def test_get_metadata_empty(self, clean_proj): # 24. guessHandler (no DB) # =========================================================================== +@pytest.mark.unit class TestGuessHandler: """Test guessHandler with various Python objects.""" diff --git a/hera/tests/test_demography.py b/hera/tests/test_demography.py index b4891724b..35277678b 100644 --- a/hera/tests/test_demography.py +++ b/hera/tests/test_demography.py @@ -131,6 +131,7 @@ def population_gdf(demo_toolkit): # 1. Toolkit initialization and properties # =========================================================================== +@pytest.mark.integration class TestDemographyToolkitInit: """Test toolkit construction and properties (requires DB).""" @@ -155,6 +156,7 @@ def test_population_types(self, demo_toolkit): # 2. setDefaultDirectory # =========================================================================== +@pytest.mark.integration class TestSetDefaultDirectory: def test_creates_and_sets_path(self, demo_toolkit): with tempfile.TemporaryDirectory() as tmpdir: @@ -172,6 +174,7 @@ def test_raises_if_not_found_and_create_false(self, demo_toolkit): # 3. analysis.calculatePopulationInPolygon # =========================================================================== +@pytest.mark.integration class TestCalculatePopulationInPolygon: def test_basic(self, demo_toolkit, population_gdf): """Polygon enclosing a census area returns non-empty result.""" @@ -294,6 +297,7 @@ def test_full_coverage_sums_to_total(self, demo_toolkit, synthetic_gdf, large_po # 4. analysis.createNewArea # =========================================================================== +@pytest.mark.integration class TestCreateNewArea: def test_simple(self, demo_toolkit, population_gdf): """Create area covering all population data.""" @@ -369,6 +373,7 @@ def test_with_specific_population_types(self, demo_toolkit, synthetic_gdf, large # 5. presentation.plotPopulationDensity # =========================================================================== +@pytest.mark.integration class TestPlotPopulationDensity: """Test plotPopulationDensity with synthetic data (no DB needed).""" @@ -459,6 +464,7 @@ def test_wgs84_input_with_reprojection(self, demo_toolkit, synthetic_gdf_wgs84): # 6. presentation.plotPopulation # =========================================================================== +@pytest.mark.integration class TestPlotPopulation: """Test plotPopulation (absolute counts).""" @@ -503,6 +509,7 @@ def test_with_existing_axes(self, demo_toolkit, synthetic_gdf): # 7. presentation.plotPopulationByType # =========================================================================== +@pytest.mark.integration class TestPlotPopulationByType: """Test plotPopulationByType (subplot grid).""" @@ -545,6 +552,7 @@ def test_custom_figsize_and_cmap(self, demo_toolkit, synthetic_gdf): # 8. presentation.plotPopulationInPolygon # =========================================================================== +@pytest.mark.integration class TestPlotPopulationInPolygon: """Test plotPopulationInPolygon (intersection visualization).""" @@ -593,6 +601,7 @@ def test_with_all_options(self, demo_toolkit, intersection_result, # 9. presentation.plotArea # =========================================================================== +@pytest.mark.integration class TestPlotArea: """Test plotArea (custom area with population annotation).""" @@ -651,6 +660,7 @@ def test_custom_styling(self, demo_toolkit, area_result, synthetic_gdf): # 10. presentation.plotPopulationOnMap (mocked TilesToolkit) # =========================================================================== +@pytest.mark.integration class TestPlotPopulationOnMap: """Test plotPopulationOnMap with a mocked TilesToolkit.""" @@ -716,6 +726,7 @@ def test_custom_zoom_and_styling(self, demo_toolkit, synthetic_gdf): # 11. Edge cases # =========================================================================== +@pytest.mark.integration class TestEdgeCases: """Edge cases and error handling.""" diff --git a/hera/tests/test_experiment.py b/hera/tests/test_experiment.py index 5284f1088..b0de5e627 100644 --- a/hera/tests/test_experiment.py +++ b/hera/tests/test_experiment.py @@ -358,6 +358,7 @@ def experiment(exp_home): # 1. experimentHome # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestExperimentHome: @@ -393,6 +394,7 @@ def test_nonexistent_experiment(self, exp_home): # 2. experimentSetupWithData — properties # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestExperimentSetup: @@ -426,6 +428,7 @@ def test_get_experiment_data(self, experiment): # 3. TrialSetWithData and TrialWithdata # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestTrialAccess: @@ -469,6 +472,7 @@ def test_trial_get_data_trh(self, experiment): # 4. EntityTypeWithData # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestEntityType: @@ -501,6 +505,7 @@ def test_get_data_trial(self, experiment): # 5. EntityWithData # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestEntity: @@ -522,6 +527,7 @@ def test_entity_get_data(self, experiment): # 6. getDataFromDateRange # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestGetDataFromDateRange: @@ -542,6 +548,7 @@ def test_basic(self, experiment): # 7. dataEngineFactory # =========================================================================== +@pytest.mark.unit @pytest.mark.skipif(not _ENGINE_AVAILABLE, reason="dataEngine not importable") class TestDataEngineFactory: """Test engine type constants (no DB needed).""" @@ -561,6 +568,7 @@ def test_constants_are_strings(self): # 8. experimentAnalysis — synthetic (no DB) # =========================================================================== +@pytest.mark.unit @pytest.mark.skipif(not _ANALYSIS_AVAILABLE, reason="experimentAnalysis not importable") class TestAnalysisSynthetic: """Test analysis methods with synthetic DataFrames.""" @@ -608,6 +616,7 @@ def __init__(self): # 9. experimentPresentation — smoke test (no DB) # =========================================================================== +@pytest.mark.unit @pytest.mark.skipif(not _PRESENTATION_AVAILABLE, reason="experimentPresentation not importable") class TestPresentationInit: """Test presentation can be instantiated.""" @@ -632,6 +641,7 @@ class MockAnalysis: # 10. Parsers — synthetic (no DB) # =========================================================================== +@pytest.mark.unit class TestParsers: """Test parsers with synthetic data.""" @@ -648,6 +658,7 @@ def test_campbell_binary_parser_import(self): # 11. Argos zip parsing (no DB) # =========================================================================== +@pytest.mark.integration @requires_experiment class TestArgosZipParsing: """Test that Argos zip files are parsed correctly.""" @@ -717,6 +728,7 @@ def test_trial_entities_table(self, experiment_dir): # 12. Full integration: load → navigate → get data # =========================================================================== +@pytest.mark.integration @requires_mongo @requires_experiment class TestFullIntegration: diff --git a/hera/tests/test_highfreq.py b/hera/tests/test_highfreq.py index 472e3f8a1..f8f5f588a 100644 --- a/hera/tests/test_highfreq.py +++ b/hera/tests/test_highfreq.py @@ -75,6 +75,7 @@ def sonic_df_indexed(sonic_df): # Toolkit property tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestHighFreqToolkitInit: def test_docType_property(self, hf_toolkit): assert hf_toolkit.docType == "highFreqMeteorology_HighFreqData" @@ -84,6 +85,7 @@ def test_docType_property(self, hf_toolkit): # Data reading tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestReadData: def test_read_sonic_data(self, sonic_df): assert isinstance(sonic_df, pd.DataFrame) @@ -98,6 +100,7 @@ def test_read_nonexistent_datasource(self, hf_toolkit): assert result is None +@pytest.mark.integration class TestTimeRange: def _get_time_series(self, df_pd): """Return the time series — either from the index or a column.""" @@ -119,6 +122,7 @@ def test_trh_time_range(self, trh_df): assert ts.min() < ts.max() +@pytest.mark.integration class TestSpecificPoints: def test_sonic_first_row(self, sonic_df): first = sonic_df.iloc[0] @@ -133,6 +137,7 @@ def test_trh_first_row(self, trh_df): assert abs(first["RH"] - 70.2) < 0.01 +@pytest.mark.integration class TestErrorPaths: def test_campbelToParquet_nonexistent(self, hf_toolkit): with pytest.warns(DeprecationWarning), pytest.raises(ValueError): @@ -147,6 +152,7 @@ def test_asciiToParquet_nonexistent(self, hf_toolkit): # AbstractCalculator tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestAbstractCalculator: def test_init_basic(self, sonic_df): ac = AbstractCalculator( @@ -178,6 +184,7 @@ def test_set_save_properties(self, sonic_df): # MeanDataCalculator tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestMeanDataCalculator: @pytest.fixture() def calc(self, sonic_df_indexed): @@ -223,6 +230,7 @@ def test_compute_returns_dataframe(self, calc): assert isinstance(result, pd.DataFrame) +@pytest.mark.integration class TestMeanDataCalculatorAdvanced: def test_TKE(self, sonic_df_indexed): df = sonic_df_indexed.head(100).copy() @@ -259,6 +267,7 @@ def test_MOLength(self, sonic_df_indexed): # RawdataAnalysis tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestRawdataAnalysis: def test_singlePointTurbulenceStatistics_returns_instance(self, hf_toolkit, sonic_df_indexed): df = sonic_df_indexed.copy() @@ -301,6 +310,7 @@ def test_AveragingCalculator_raises_on_invalid(self, hf_toolkit): # singlePointTurbulenceStatistics unit tests (synthetic data) # --------------------------------------------------------------------------- +@pytest.mark.unit class TestSinglePointTurbulenceStatistics: @pytest.fixture() def turb_calc(self): diff --git a/hera/tests/test_landcover.py b/hera/tests/test_landcover.py index d719e8cdd..ea2f79e99 100644 --- a/hera/tests/test_landcover.py +++ b/hera/tests/test_landcover.py @@ -60,6 +60,7 @@ def test_coords(): # Tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestGetLandCoverAtPoint: def test_basic(self, lc_toolkit, test_coords): value = lc_toolkit.getLandCoverAtPoint(test_coords["lon"], test_coords["lat"]) @@ -75,6 +76,7 @@ def test_against_raster(self, lc_toolkit, landcover_file, test_coords): assert value_from_raster == value_from_toolkit +@pytest.mark.integration class TestGetLandCover: def test_basic(self, lc_toolkit, test_coords): lc = lc_toolkit.getLandCover( @@ -109,6 +111,7 @@ def test_map_vs_raster(self, lc_toolkit, landcover_file, test_coords): pass # Point outside raster extent +@pytest.mark.integration class TestGetRoughness: def test_at_point(self, lc_toolkit, test_coords): value = lc_toolkit.getRoughnessAtPoint(test_coords["lon"], test_coords["lat"]) @@ -133,6 +136,7 @@ def test_values_in_range(self, lc_toolkit, test_coords): assert np.all((z0 > 0) & (z0 < 2)), "Some roughness values out of expected range" +@pytest.mark.integration class TestRoughnessLength: def test_roughnesslength2sandgrainroughness(self, lc_toolkit): ks = lc_toolkit.roughnesslength2sandgrainroughness(0.1) diff --git a/hera/tests/test_lowfreq.py b/hera/tests/test_lowfreq.py index 4a363838f..61724800e 100644 --- a/hera/tests/test_lowfreq.py +++ b/hera/tests/test_lowfreq.py @@ -49,6 +49,7 @@ def lowfreq_df(lf_toolkit): # Toolkit structure tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestLowFreqToolkitInit: def test_has_analysis(self, lf_toolkit): assert lf_toolkit.analysis is not None @@ -69,6 +70,7 @@ def test_docType_value(self, lf_toolkit): # Analysis tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestAnalysisAddDatesColumns: def test_basic(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() @@ -77,6 +79,7 @@ def test_basic(self, lf_toolkit, lowfreq_df): assert len(enriched) > 0 +@pytest.mark.integration class TestAnalysisCalcHourlyDist: def test_max_normalized(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() @@ -97,6 +100,7 @@ def test_density(self, lf_toolkit, lowfreq_df): assert (M >= 0).all(), "Density matrix should not contain negative values" +@pytest.mark.unit class TestCalcDist2dYNormalized: def test_y_normalized_behaviour(self): x = np.array([0.5, 1.5, 2.5, 1.5]) @@ -119,6 +123,7 @@ def test_y_normalized_behaviour(self): assert np.count_nonzero(M[2]) == 1 +@pytest.mark.integration class TestResampleSecondMoments: def test_basic(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy().set_index("datetime") @@ -145,6 +150,7 @@ def test_basic(self, lf_toolkit, lowfreq_df): # Presentation tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestPresentationDailyPlots: def test_plotScatter(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() @@ -170,6 +176,7 @@ def test_plotProbContourf(self, lf_toolkit, lowfreq_df): assert ax is not None +@pytest.mark.integration class TestPresentationDataMatchPlots: def test_dateLinePlot_matches_data(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() @@ -207,6 +214,7 @@ def test_plotScatter_matches_data(self, lf_toolkit, lowfreq_df): assert matches > 0 +@pytest.mark.integration class TestPresentationEdgeCases: def test_scatter_empty_dataframe(self, lf_toolkit): df = pd.DataFrame(columns=["datetime", "RH"]) @@ -243,6 +251,7 @@ def test_scatter_WS_field(self, lf_toolkit, lowfreq_df): assert matches > 0 +@pytest.mark.integration class TestPresentationSeasonalPlots: def test_plotProbContourf_bySeason(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() @@ -266,6 +275,7 @@ def test_contourf_distribution_ranges(self, lf_toolkit, lowfreq_df): assert filtered.max() >= y_min +@pytest.mark.integration class TestPresentationSavePlot: def test_scatter_creates_non_empty_image(self, lf_toolkit, lowfreq_df): df = lowfreq_df.copy() diff --git a/hera/tests/test_repository.py b/hera/tests/test_repository.py index 6b0ac2261..bd573dbbb 100644 --- a/hera/tests/test_repository.py +++ b/hera/tests/test_repository.py @@ -68,6 +68,7 @@ def _ensure_repos_added(tk, repo_test_cases): # Tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestAddRepository: def test_add_repository(self, tk, repo_test_cases, _ensure_repos_added): """addRepository must succeed without raising.""" @@ -78,6 +79,7 @@ def test_add_repository(self, tk, repo_test_cases, _ensure_repos_added): assert table is not None +@pytest.mark.integration class TestGetRepository: def test_get_repository(self, tk, repo_test_cases, _ensure_repos_added): """getRepository must return a non-empty dict.""" @@ -87,6 +89,7 @@ def test_get_repository(self, tk, repo_test_cases, _ensure_repos_added): assert isinstance(repo, dict) +@pytest.mark.integration class TestLoadDatasourcesToProject: def test_load_datasources_to_project(self, tk, repo_test_cases, _ensure_repos_added): """Load repository into a project and verify document count.""" @@ -131,6 +134,7 @@ def test_load_datasources_to_project(self, tk, repo_test_cases, _ensure_repos_ad ) +@pytest.mark.unit class TestResolveDataSourcePaths: def test_resolve_relative_paths(self): """resolveDataSourcePaths must convert relative paths to absolute.""" @@ -173,6 +177,7 @@ def test_absolute_paths_unchanged(self): class TestLoadRepositoryFromPath: + @pytest.mark.integration def test_load_repository_from_path(self): """loadRepositoryFromPath must read JSON and resolve paths.""" if not os.path.isfile(REPO_TEST_01_JSON): @@ -182,6 +187,7 @@ def test_load_repository_from_path(self): assert isinstance(result, dict) assert len(result) > 0 + @pytest.mark.unit def test_load_repository_nonexistent(self): """loadRepositoryFromPath must raise FileNotFoundError for missing files.""" with pytest.raises(FileNotFoundError): diff --git a/hera/tests/test_topography.py b/hera/tests/test_topography.py index 0719ef4ad..befa5a157 100644 --- a/hera/tests/test_topography.py +++ b/hera/tests/test_topography.py @@ -100,6 +100,7 @@ def _safe_get(func, *args, **kwargs): # Tests # --------------------------------------------------------------------------- +@pytest.mark.integration class TestGetPointElevation: def test_basic(self, topo_toolkit): lat, lon = 33.85, 35.15 @@ -126,6 +127,7 @@ def test_matches_hgt_file(self, topo_toolkit, resource_folders): assert abs(toolkit_elev - file_elev) <= 1 +@pytest.mark.integration class TestGetPointListElevation: def test_basic(self, topo_toolkit): points = pd.DataFrame({"lat": [33.85, 33.9], "lon": [35.15, 36.05]}) @@ -160,6 +162,7 @@ def test_matches_hgt_files(self, topo_toolkit, resource_folders): pytest.skip("No valid elevation comparisons were possible") +@pytest.mark.integration class TestGetElevationOfXarray: def test_basic(self, topo_toolkit): lat_vals = np.array([[33.85, 33.85], [33.86, 33.86]]) @@ -207,6 +210,7 @@ def test_matches_hgt_file(self, topo_toolkit, resource_folders): pytest.skip("No valid comparison points found") +@pytest.mark.integration class TestGetElevation: def test_basic(self, topo_toolkit): # Per documented WSG84 contract: minx=latitude, miny=longitude. @@ -250,6 +254,7 @@ def test_matches_hgt_file(self, topo_toolkit, resource_folders): pytest.skip("No valid comparison points found") +@pytest.mark.integration class TestConvertPointsCRS: def test_basic(self, topo_toolkit): points = [(35.1, 33.85), (36.05, 33.9)] @@ -257,6 +262,7 @@ def test_basic(self, topo_toolkit): assert converted.shape[0] == 2 +@pytest.mark.integration class TestCreateElevationSTL: def test_basic(self, topo_toolkit): # Per documented WSG84 contract: minx=latitude, miny=longitude. @@ -269,6 +275,7 @@ def test_basic(self, topo_toolkit): assert stl_str.startswith("solid") +@pytest.mark.integration class TestGetElevationSTL: def test_basic(self, topo_toolkit): lat_vals = np.array([[33.85, 33.85], [33.86, 33.86]]) @@ -284,6 +291,7 @@ def test_basic(self, topo_toolkit): assert stl.startswith("solid SurfaceTest") +@pytest.mark.integration class TestCalculateStatistics: def test_basic(self, topo_toolkit): elevation = xr.Dataset({ diff --git a/hera/tests/test_workflow_execution.py b/hera/tests/test_workflow_execution.py index 58e753bc9..04fba592a 100644 --- a/hera/tests/test_workflow_execution.py +++ b/hera/tests/test_workflow_execution.py @@ -29,6 +29,7 @@ # buildLuigiExecutionCommand # --------------------------------------------------------------------------- +@pytest.mark.unit def test_local_scheduler_command_is_backward_compatible(): cmd = buildLuigiExecutionCommand("Flow", "abc123", scheduler=SCHEDULER_LOCAL) assert "--module Flow finalnode_xx_0" in cmd @@ -38,12 +39,14 @@ def test_local_scheduler_command_is_backward_compatible(): assert cmd.startswith("python3 -m luigi --module Flow finalnode_xx_0 --local-scheduler") +@pytest.mark.unit def test_central_scheduler_drops_local_flag(): cmd = buildLuigiExecutionCommand("Flow", "abc123", scheduler=SCHEDULER_CENTRAL) assert "--local-scheduler" not in cmd assert "--dispatch-id abc123" in cmd +@pytest.mark.unit def test_central_scheduler_host_and_port(): cmd = buildLuigiExecutionCommand("Flow", "id1", scheduler=SCHEDULER_CENTRAL, schedulerHost="myhost", schedulerPort=8082) @@ -52,12 +55,14 @@ def test_central_scheduler_host_and_port(): assert "--local-scheduler" not in cmd +@pytest.mark.unit def test_central_scheduler_omits_address_when_not_given(): cmd = buildLuigiExecutionCommand("Flow", "id1", scheduler=SCHEDULER_CENTRAL) assert "--scheduler-host" not in cmd assert "--scheduler-port" not in cmd +@pytest.mark.unit def test_local_scheduler_ignores_host_and_port(): cmd = buildLuigiExecutionCommand("Flow", "id1", scheduler=SCHEDULER_LOCAL, schedulerHost="myhost", schedulerPort=8082) @@ -66,6 +71,7 @@ def test_local_scheduler_ignores_host_and_port(): assert "--local-scheduler" in cmd +@pytest.mark.unit def test_custom_target_task(): cmd = buildLuigiExecutionCommand("Flow", "id1", targetTask="otherNode_0") assert "otherNode_0" in cmd @@ -120,6 +126,7 @@ def test_custom_target_task(): ) +@pytest.mark.integration def test_run_hermes_workflow_isolates_outputs_per_dispatch(tmp_path): """Build and execute a real hermes workflow using hera's buildLuigiExecutionCommand and confirm the run completes and writes its targets under the dispatch_id subdir."""