From 86ad906efa98c1e324166ee7c32f3d8627c08c01 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:24:56 +0000 Subject: [PATCH 1/6] First commit of test dispatch branch. --- .../tests/binpreds/binpred_test_dispatch.py | 520 ++++++++++++++++++ ...summarize_binpred_test_dispatch_results.py | 11 + .../binpreds/test_binpred_test_dispatch.py | 110 ++++ python/cuspatial/cuspatial/tests/conftest.py | 24 + 4 files changed, 665 insertions(+) create mode 100644 python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py new file mode 100644 index 000000000..68e9b5a60 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -0,0 +1,520 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +import pytest +from shapely.geometry import LineString, Point, Polygon + +import cuspatial + +"""Test Dispatch""" + +"""This file is used to generate tests for all possible combinations +of geometry types and binary predicates. The tests are generated +using the fixtures defined in this file. The fixtures are combined +in the test function in `test_binpreds_test_dispatch.py` to make +a Tuple: (predicate, feature-name, feature-lhs, feature-rhs). The +feature-name is not used in the tests but is useful for debugging. +""" + + +"""The collection of all supported binary predicates""" + + +@pytest.fixture( + params=[ + "contains", + "geom_equals", + "intersects", + "covers", + "crosses", + "disjoint", + "overlaps", + "touches", + "within", + ] +) +def predicate(request): + return request.param + + +"""The fundamental set of tests. This section is dispatched based +on the feature type. Each feature pairing has a specific set of +comparisons that need to be performed to cover the entire test +space. This section will be contains specific feature representations +that cover all possible geometric combinations.""" + + +point_polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]) +features = { + "point-point-disjoint": ( + """Two points apart.""", + Point(0.0, 0.0), + Point(1.0, 0.0), + ), + "point-point-equal": ( + """Two points together.""", + Point(0.0, 0.0), + Point(0.0, 0.0), + ), + "point-linestring-disjoint": ( + """Point and linestring are disjoint.""", + Point(0.0, 0.0), + LineString([(1.0, 0.0), (2.0, 0.0)]), + ), + "point-linestring-point": ( + """Point and linestring share a point.""", + Point(0.0, 0.0), + LineString([(0.0, 0.0), (2.0, 0.0)]), + ), + "point-linestring-edge": ( + """Point and linestring intersect.""", + Point(0.5, 0.0), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "point-polygon-disjoint": ( + """Point and polygon are disjoint.""", + Point(-0.5, 0.5), + point_polygon, + ), + "point-polygon-point": ( + """Point and polygon share a point.""", + Point(0.0, 0.0), + point_polygon, + ), + "point-polygon-edge": ( + """Point and polygon intersect.""", + Point(0.5, 0.0), + point_polygon, + ), + "point-polygon-in": ( + """Point is in polygon interior.""", + Point(0.5, 0.5), + point_polygon, + ), + "linestring-linestring-disjoint": ( + """ + x---x + + x---x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ), + "linestring-linestring-same": ( + """ + x---x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "linestring-linestring-touches": ( + """ + x + | + | + | + x---x + """, + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "linestring-linestring-touch-edge": ( + """ + x + | + | + | + x-x-x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.5, 0.0), (0.5, 1.0)]), + ), + "linestring-linestring-crosses": ( + """ + x + | + x-|-x + | + x + """, + LineString([(0.5, 0.0), (0.5, 1.0)]), + LineString([(0.0, 0.5), (1.0, 0.5)]), + ), + "linestring-polygon-disjoint": ( + """ + point_polygon above is drawn as + ----- + | | + | | + | | + ----- + and the corresponding linestring is drawn as + x---x + or + x + | + | + | + x + """ + """ + x ----- + | | | + | | | + | | | + x ----- + """, + LineString([(-0.5, 0.0), (-0.5, 1.0)]), + point_polygon, + ), + "linestring-polygon-touch-point": ( + """ + x---x---- + | | + | | + | | + ----- + """, + LineString([(-1.0, 0.0), (0.0, 0.0)]), + point_polygon, + ), + "linestring-polygon-touch-edge": ( + """ + ----- + | | + x---x | + | | + ----- + """, + LineString([(-1.0, 0.5), (0.0, 0.5)]), + point_polygon, + ), + "linestring-polygon-overlap-edge": ( + """ + x---- + | | + | | + | | + x---- + """, + LineString([(0.0, 0.0), (0.0, 1.0)]), + point_polygon, + ), + "linestring-polygon-intersect-edge": ( + """ + ----- + | | + | | + | | + x---x-- + """, + LineString([(-0.5, 0.0), (0.5, 0.0)]), + point_polygon, + ), + "linestring-polygon-intersect-inner-edge": ( + """ + ----- + x | + | | + x | + ----- + + The linestring in this case is shorter than the corners of the polygon. + """, + LineString([(0.25, 0.0), (0.75, 0.0)]), + point_polygon, + ), + "linestring-polygon-point-interior": ( + """ + ----x + | /| + | / | + |/ | + x---- + """, + LineString([(0.0, 0.0), (1.0, 1.0)]), + point_polygon, + ), + "linestring-polygon-edge-interior": ( + """ + --x-- + | | | + | | | + | | | + --x-- + """, + LineString([(0.5, 0.0), (0.5, 1.0)]), + point_polygon, + ), + "linestring-polygon-in": ( + """ + ----- + | x | + | | | + | x | + ----- + """, + LineString([(0.5, 0.25), (0.5, 0.75)]), + point_polygon, + ), + "linestring-polygon-in-out": ( + """ + ----- + | | + | x | + | | | + --|-- + | + x + """, + LineString([(0.5, 0.5), (0.5, -0.5)]), + point_polygon, + ), + "linestring-polygon-crosses": ( + """ + x + --|-- + | | | + | | | + | | | + --|-- + x + """, + LineString([(0.5, 1.25), (0.5, -0.25)]), + point_polygon, + ), + "polygon-polygon-disjoint": ( + """ + Polygon polygon tests use a triangle for the lhs and a square for the rhs. + The triangle is drawn as + x---x + | / + | / + |/ + x + + The square is drawn as + + ----- + | | + | | + | | + ----- + """, + Polygon([(0.0, 2.0), (0.0, 3.0), (1.0, 3.0)]), + point_polygon, + ), + "polygon-polygon-touch-point": ( + """ + x---x + | / + | / + |/ + x---- + | | + | | + | | + ----- + """, + Polygon([(0.0, 1.0), (0.0, 2.0), (1.0, 2.0)]), + point_polygon, + ), + "polygon-polygon-touch-edge": ( + """ + x---x + | / + | / + |/ + -x--x + | | + | | + | | + ----- + """, + Polygon([(0.25, 1.0), (0.25, 2.0), (1.25, 2.0)]), + point_polygon, + ), + "polygon-polygon-overlap-edge": ( + """ + x + |\\ + | \\ + | \\ + x---x + | | + | | + | | + ----- + """, + Polygon([(0.0, 1.0), (0.0, 2.0), (1.0, 2.0)]), + point_polygon, + ), + "polygon-polygon-point-inside": ( + """ + x---x + | / + | / + --|/- + | x | + | | + | | + ----- + """, + Polygon([(0.5, 0.5), (0.5, 1.5), (1.5, 1.5)]), + point_polygon, + ), + "polygon-polygon-point-outside": ( + """ + x + -|\\-- + |x-x| + | | + | | + ----- + """, + Polygon([(0.25, 0.75), (0.25, 1.25), (0.75, 0.75)]), + point_polygon, + ), + "polygon-polygon-in-out-point": ( + """ + x + |\\ + --|-x + | |/| + | x | + | | + x---- + """, + Polygon([(0.5, 0.5), (0.5, 1.5), (1.0, 1.0)]), + point_polygon, + ), + "polygon-polygon-in-point-point": ( + """ + x---- + |\\ | + | x | + |/ | + x---- + """, + Polygon([(0.0, 0.0), (0.0, 1.0), (0.5, 0.5)]), + point_polygon, + ), + "polygon-polygon-contained": ( + """ + ----- + | x| + | /|| + |x-x| + ----- + """, + Polygon([(0.25, 0.25), (0.75, 0.75), (0.75, 0.25)]), + point_polygon, + ), + "polygon-polygon-same": ( + """ + x---x + | | + | | + | | + x---x + """, + point_polygon, + point_polygon, + ), +} + +point_point_dispatch_list = [ + "point-point-disjoint", + "point-point-equal", +] + +point_linestring_dispatch_list = [ + "point-linestring-disjoint", + "point-linestring-point", + "point-linestring-edge", +] + +point_polygon_dispatch_list = [ + "point-polygon-disjoint", + "point-polygon-point", + "point-polygon-edge", + "point-polygon-in", +] + +linestring_linestring_dispatch_list = [ + "linestring-linestring-disjoint", + "linestring-linestring-same", + "linestring-linestring-touches", + "linestring-linestring-crosses", +] + +linestring_polygon_dispatch_list = [ + "linestring-polygon-disjoint", + "linestring-polygon-touch-point", + "linestring-polygon-touch-edge", + "linestring-polygon-overlap-edge", + "linestring-polygon-intersect-edge", + "linestring-polygon-intersect-inner-edge", + "linestring-polygon-point-interior", + "linestring-polygon-edge-interior", + "linestring-polygon-in", + "linestring-polygon-crosses", +] + +polygon_polygon_dispatch_list = [ + "polygon-polygon-disjoint", + "polygon-polygon-touch-point", + "polygon-polygon-touch-edge", + "polygon-polygon-overlap-edge", + "polygon-polygon-point-inside", + "polygon-polygon-point-outside", + "polygon-polygon-in-out-point", + "polygon-polygon-in-point-point", + "polygon-polygon-contained", + "polygon-polygon-same", +] + + +def object_dispatch(name_list): + for name in name_list: + yield (name, features[name][0], features[name][1], features[name][2]) + + +type_dispatch = { + (Point, Point): object_dispatch(point_point_dispatch_list), + (Point, LineString): object_dispatch(point_linestring_dispatch_list), + (Point, Polygon): object_dispatch(point_polygon_dispatch_list), + (LineString, LineString): object_dispatch( + linestring_linestring_dispatch_list + ), + (LineString, Polygon): object_dispatch(linestring_polygon_dispatch_list), + (Polygon, Polygon): object_dispatch(polygon_polygon_dispatch_list), +} + + +def simple_test_dispatch(): + for types in type_dispatch: + generator = type_dispatch[types] + for test_name, test_description, lhs, rhs in generator: + yield ( + test_name, + test_description, + cuspatial.GeoSeries( + [ + lhs, + rhs if types[0] == types[1] else lhs, + lhs, + ] + ), + cuspatial.GeoSeries( + [ + rhs, + rhs, + rhs, + ] + ), + ) + + +@pytest.fixture(params=simple_test_dispatch()) +def simple_test(request): + return request.param diff --git a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py new file mode 100644 index 000000000..5efa9493d --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py @@ -0,0 +1,11 @@ +import pandas as pd + +pp = pd.read_csv("predicate_passes.csv") +pf = pd.read_csv("predicate_fails.csv") +fp = pd.read_csv("feature_passes.csv") +ff = pd.read_csv("feature_fails.csv") + +print(pp) +print(pf) +print(fp) +print(ff) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py new file mode 100644 index 000000000..83efa1c99 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from functools import wraps + +import pandas as pd +import pytest +from binpred_test_dispatch import predicate, simple_test # noqa: F401 + +"""Decorator function that xfails a test if an exception is throw +by the test function. Will be removed when all tests are passing.""" + + +def xfail_on_exception(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + pytest.xfail(f"Xfailling due to an exception: {e}") + + return wrapper + + +"""Parameterized test fixture that runs a binary predicate test +for each combination of geometry types and binary predicates.""" + +out_file = open("test_binpred_test_dispatch.log", "w") + + +# @xfail_on_exception # TODO: Remove when all tests are passing +def test_simple_features( + predicate, # noqa: F811 + simple_test, # noqa: F811 + predicate_passes, + predicate_fails, + feature_passes, + feature_fails, + request, +): + try: + (lhs, rhs) = simple_test[2], simple_test[3] + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + pred_fn = getattr(lhs, predicate) + got = pred_fn(rhs) + gpd_pred_fn = getattr(gpdlhs, predicate) + expected = gpd_pred_fn(gpdrhs) + assert (got.values_host == expected.values).all() + try: + predicate_passes[predicate] = ( + 1 + if predicate not in predicate_passes + else predicate_passes[predicate] + 1 + ) + feature_passes[(lhs.column_type, rhs.column_type)] = ( + 1 + if (lhs.column_type, rhs.column_type) not in feature_passes + else feature_passes[(lhs.column_type, rhs.column_type)] + 1 + ) + passes_df = pd.DataFrame( + { + "predicate": list(predicate_passes.keys()), + "predicate_passes": list(predicate_passes.values()), + } + ) + passes_df.to_csv("predicate_passes.csv", index=False) + passes_df = pd.DataFrame( + { + "feature": list(feature_passes.keys()), + "feature_passes": list(feature_passes.values()), + } + ) + passes_df.to_csv("feature_passes.csv", index=False) + print(passes_df) + except Exception as e: + raise ValueError(e) + except Exception as e: + out_file.write( + f"""{predicate}, +------------ +{simple_test[0]}\n{simple_test[1]}\nfailed +test: {request.node.name}\n\n""" + ) + predicate_fails[predicate] = ( + 1 + if predicate not in predicate_fails + else predicate_fails[predicate] + 1 + ) + feature_fails[(lhs.column_type, rhs.column_type)] = ( + 1 + if (lhs.column_type, rhs.column_type) not in feature_fails + else feature_fails[(lhs.column_type, rhs.column_type)] + 1 + ) + # TODO: Uncomment when all tests are passing + predicate_fails_df = pd.DataFrame( + { + "predicate": list(predicate_fails.keys()), + "predicate_fails": list(predicate_fails.values()), + } + ) + predicate_fails_df.to_csv("predicate_fails.csv", index=False) + feature_fails_df = pd.DataFrame( + { + "feature": list(feature_fails.keys()), + "feature_fails": list(feature_fails.values()), + } + ) + feature_fails_df.to_csv("feature_fails.csv", index=False) + raise e # TODO: Remove when all tests are passing. + # pytest.fail(f"Assertion failed: {e}") diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 1c37cee77..ac4fd111a 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -315,3 +315,27 @@ def naturalearth_cities(): @pytest.fixture def naturalearth_lowres(): return gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + + +@pytest.fixture(scope="session") +def predicate_passes(): + data = {} + return data + + +@pytest.fixture(scope="session") +def predicate_fails(): + data = {} + return data + + +@pytest.fixture(scope="session") +def feature_passes(): + data = {} + return data + + +@pytest.fixture(scope="session") +def feature_fails(): + data = {} + return data From 1a0072c1678669516ae574af416d0942f7c19345 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:36:19 +0000 Subject: [PATCH 2/6] Enable xfail. --- .../cuspatial/tests/binpreds/test_binpred_test_dispatch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 83efa1c99..b88fd7fc1 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -27,7 +27,7 @@ def wrapper(*args, **kwargs): out_file = open("test_binpred_test_dispatch.log", "w") -# @xfail_on_exception # TODO: Remove when all tests are passing +@xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 @@ -91,7 +91,6 @@ def test_simple_features( if (lhs.column_type, rhs.column_type) not in feature_fails else feature_fails[(lhs.column_type, rhs.column_type)] + 1 ) - # TODO: Uncomment when all tests are passing predicate_fails_df = pd.DataFrame( { "predicate": list(predicate_fails.keys()), @@ -107,4 +106,3 @@ def test_simple_features( ) feature_fails_df.to_csv("feature_fails.csv", index=False) raise e # TODO: Remove when all tests are passing. - # pytest.fail(f"Assertion failed: {e}") From b2ea9303c20affa4f631fb9f3abacefc11fd2e20 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:49:12 +0000 Subject: [PATCH 3/6] Binpred test dispatch documented and ready for review. --- .../tests/binpreds/binpred_test_dispatch.py | 32 ++++++++++--- ...summarize_binpred_test_dispatch_results.py | 5 ++ .../binpreds/test_binpred_test_dispatch.py | 47 +++++++++++++++++-- python/cuspatial/cuspatial/tests/conftest.py | 4 ++ 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 68e9b5a60..40c77b2a7 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -11,14 +11,12 @@ of geometry types and binary predicates. The tests are generated using the fixtures defined in this file. The fixtures are combined in the test function in `test_binpreds_test_dispatch.py` to make -a Tuple: (predicate, feature-name, feature-lhs, feature-rhs). The -feature-name is not used in the tests but is useful for debugging. +a Tuple: (feature-name, feature-description, feature-lhs, +feature-rhs). The feature-name and feature-descriptions are not used +in the test but are used for development and debugging. """ -"""The collection of all supported binary predicates""" - - @pytest.fixture( params=[ "contains", @@ -33,13 +31,14 @@ ] ) def predicate(request): + """The collection of all supported binary predicates""" return request.param """The fundamental set of tests. This section is dispatched based on the feature type. Each feature pairing has a specific set of comparisons that need to be performed to cover the entire test -space. This section will be contains specific feature representations +space. This section contains specific feature representations that cover all possible geometric combinations.""" @@ -117,6 +116,17 @@ def predicate(request): LineString([(0.0, 0.0), (0.0, 1.0)]), LineString([(0.0, 0.0), (1.0, 0.0)]), ), + "linestring-linestring-touch-interior": ( + """ + x x + | / + | / + |/ + x---x + """, + LineString([(0.0, 1.0), (0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 0.0), (1.0, 1.0)]), + ), "linestring-linestring-touch-edge": ( """ x @@ -475,12 +485,18 @@ def predicate(request): def object_dispatch(name_list): + """Generate a list of test cases for a given set of test names.""" for name in name_list: yield (name, features[name][0], features[name][1], features[name][2]) type_dispatch = { - (Point, Point): object_dispatch(point_point_dispatch_list), + """A dictionary of test cases for each geometry type combination. + Still needs MultiPoint."""( + Point, Point + ): object_dispatch( + point_point_dispatch_list + ), (Point, LineString): object_dispatch(point_linestring_dispatch_list), (Point, Polygon): object_dispatch(point_polygon_dispatch_list), (LineString, LineString): object_dispatch( @@ -492,6 +508,7 @@ def object_dispatch(name_list): def simple_test_dispatch(): + """Generates a list of test cases for each geometry type combination.""" for types in type_dispatch: generator = type_dispatch[types] for test_name, test_description, lhs, rhs in generator: @@ -517,4 +534,5 @@ def simple_test_dispatch(): @pytest.fixture(params=simple_test_dispatch()) def simple_test(request): + """Generates a unique test case for each geometry type combination.""" return request.param diff --git a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py index 5efa9493d..38742c7ba 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py +++ b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py @@ -1,3 +1,8 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +"""Prints a summary of the results of the binary predicate test dispatch. +""" + import pandas as pd pp = pd.read_csv("predicate_passes.csv") diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index b88fd7fc1..e176b9240 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -21,9 +21,7 @@ def wrapper(*args, **kwargs): return wrapper -"""Parameterized test fixture that runs a binary predicate test -for each combination of geometry types and binary predicates.""" - +# In the below file, all failing tests are recorded with visualizations. out_file = open("test_binpred_test_dispatch.log", "w") @@ -37,6 +35,42 @@ def test_simple_features( feature_fails, request, ): + """Parameterized test fixture that runs a binary predicate test + for each combination of geometry types and binary predicates. + + Uses four fixtures from `conftest.py` to store the number of times + each binary predicate has passed and failed, and the number of times + each combination of geometry types has passed and failed. These + results are saved to CSV files after each test. + + Uses the @xfail_on_exception decorator to mark a test as xfailed + if an exception is thrown. This is a temporary measure to allow + the test suite to run to completion while we work on fixing the + failing tests. + + Parameters + ---------- + predicate : str + The name of the binary predicate to test. + simple_test : tuple + A tuple containing the name of the test, a docstring that + describes the test, and the left and right geometry objects. + predicate_passes : dict + A dictionary fixture containing the number of times each binary + predicate has passed. + predicate_fails : dict + A dictionary fixture containing the number of times each binary + predicate has failed. + feature_passes : dict + A dictionary fixture containing the number of times each combination + of geometry types has passed. + feature_fails : dict + A dictionary fixture containing the number of times each combination + of geometry types has failed. + request : pytest.FixtureRequest + The pytest request object. Used to print the test name in + diagnostic output. + """ try: (lhs, rhs) = simple_test[2], simple_test[3] gpdlhs = lhs.to_geopandas() @@ -46,7 +80,10 @@ def test_simple_features( gpd_pred_fn = getattr(gpdlhs, predicate) expected = gpd_pred_fn(gpdrhs) assert (got.values_host == expected.values).all() + + # The test is complete, the rest is just logging. try: + # The test passed, store the results. predicate_passes[predicate] = ( 1 if predicate not in predicate_passes @@ -71,10 +108,10 @@ def test_simple_features( } ) passes_df.to_csv("feature_passes.csv", index=False) - print(passes_df) except Exception as e: raise ValueError(e) except Exception as e: + # The test failed, store the results. out_file.write( f"""{predicate}, ------------ @@ -105,4 +142,4 @@ def test_simple_features( } ) feature_fails_df.to_csv("feature_fails.csv", index=False) - raise e # TODO: Remove when all tests are passing. + raise e diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index ac4fd111a..b204efde2 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -319,23 +319,27 @@ def naturalearth_lowres(): @pytest.fixture(scope="session") def predicate_passes(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def predicate_fails(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def feature_passes(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def feature_fails(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data From dfcd58339858971af206efa20ca05f4bd75ba1c8 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 19:01:49 +0000 Subject: [PATCH 4/6] Fix bug in test list. --- .../cuspatial/tests/binpreds/binpred_test_dispatch.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 40c77b2a7..ed8ca4236 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -491,12 +491,9 @@ def object_dispatch(name_list): type_dispatch = { - """A dictionary of test cases for each geometry type combination. - Still needs MultiPoint."""( - Point, Point - ): object_dispatch( - point_point_dispatch_list - ), + # A dictionary of test cases for each geometry type combination. + # Still needs MultiPoint. + (Point, Point): object_dispatch(point_point_dispatch_list), (Point, LineString): object_dispatch(point_linestring_dispatch_list), (Point, Polygon): object_dispatch(point_polygon_dispatch_list), (LineString, LineString): object_dispatch( From 1aefaa24b03045a9306b7c9a2a1883f54cc04a66 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Apr 2023 09:39:28 -0500 Subject: [PATCH 5/6] Update python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- .../cuspatial/tests/binpreds/test_binpred_test_dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index e176b9240..914e2e88e 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -16,7 +16,7 @@ def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: - pytest.xfail(f"Xfailling due to an exception: {e}") + pytest.xfail(f"Xfailing due to an exception: {e}") return wrapper From c9e3bf3a9cabbf5db3a0fc8d3b8c9fbb6164a2c3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 26 Apr 2023 19:38:10 +0000 Subject: [PATCH 6/6] Add description to why every test returns a GeoSeries of length 3. --- .../tests/binpreds/binpred_test_dispatch.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index ed8ca4236..03f6e3ab0 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -505,7 +505,43 @@ def object_dispatch(name_list): def simple_test_dispatch(): - """Generates a list of test cases for each geometry type combination.""" + """Generates a list of test cases for each geometry type combination. + + Each dispatched test case is a tuple of the form: + (test_name, test_description, lhs, rhs) + which is run in `test_binpred_test_dispatch.py`. + + The test_name is a unique identifier for the test case. + The test_description is a string representation of the test case. + The lhs and rhs are GeoSeries of the left and right geometries. + + lhs and rhs are always constructed as a list of 3 geometries since + the binpred function is designed to operate primarily on groups of + geometries. The first and third feature in the list always match + the first geometry specified in `test_description`, and the rhs is always + a group of three of the second geometry specified in `test_description`. + The second feature in the lhs varies. + + When the types of the lhs and rhs are equal, the second geometry + from `test_description` is substituted for the second geometry in the lhs. + This produces a test form of: + lhs rhs + A B + B B + A B + + This decision has two primary benefits: + 1. It causes the test to produce varied results (meaning results of the + form (True, False, True) or (False, True, False), greatly reducing the + likelihood of an "all-False" or "all-True" predicate producing + false-positive results. + 2. It tests every binary predicate against self, such as A.touches(A) + for every predicate and geometry combination. + + When the types of lhs and rhs are not equal this variation is not + performed, since we cannot currently use predicate operations on mixed + geometry types. + """ for types in type_dispatch: generator = type_dispatch[types] for test_name, test_description, lhs, rhs in generator: