Skip to content

Commit

Permalink
implements the ATS conformance testsuite
Browse files Browse the repository at this point in the history
See geoserver/geoserver-ogcapi#10
for the motivation.
  • Loading branch information
pmauduit committed Nov 5, 2024
1 parent 96940ce commit 595d601
Show file tree
Hide file tree
Showing 6 changed files with 909 additions and 0 deletions.
11 changes: 11 additions & 0 deletions modules/unsupported/cql2-text/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geopkg</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<!-- ==================================================== -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.geotools.filter.text.cql_2.conformance;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.io.FileUtils;
import org.geotools.api.data.DataStore;
import org.geotools.api.data.DataStoreFinder;
import org.geotools.http.HTTPResponse;
import org.geotools.http.SimpleHttpClient;
import org.junit.BeforeClass;

/**
* Base class for tests issued from the Abstract Test Suite (ATS). See
* https://docs.ogc.org/is/21-065r2/21-065r2.html#ats for the context.
*
* <p>This class will download the official Natural Earth dataset and store it into the default
* temporary directory.
*/
public abstract class ATSOnlineTest {

private static String NE_DATA_URL =
"https://github.com/opengeospatial/ogcapi-features/raw/refs/heads/master/cql2/standard/data/ne110m4cql2.gpkg";

protected final File neGpkg = Path.of(System.getProperty("java.io.tmpdir"), "ne.gpkg").toFile();

@BeforeClass
public static void forceGMT() {
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
}

private void downloadNaturalEarthData() throws IOException {
if (neGpkg.exists()) {
return;
}
neGpkg.createNewFile();
SimpleHttpClient client = new SimpleHttpClient();
HTTPResponse r = client.get(new URL(NE_DATA_URL));
FileUtils.copyInputStreamToFile(r.getResponseStream(), neGpkg);
}

protected DataStore naturalEarthData() throws IOException {
downloadNaturalEarthData();
Map params = new HashMap();
params.put("dbtype", "geopkg");
params.put("database", neGpkg.getAbsolutePath());
params.put("read-only", true);

DataStore datastore = DataStoreFinder.getDataStore(params);

return datastore;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.geotools.filter.text.cql_2.conformance;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import org.geootols.filter.text.cql_2.CQL2;
import org.geotools.api.data.DataStore;
import org.geotools.api.filter.Filter;
import org.geotools.filter.text.cql2.CQLException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/** See table 9 from section A.4.4 Conformance test 13. */
@RunWith(Parameterized.class)
public class ConformanceTest13OnlineTest extends ATSOnlineTest {

private final String criteria;
private final int expectedFeatures;

public ConformanceTest13OnlineTest(String criteria, int expectedFeatures) {
this.criteria = criteria;
this.expectedFeatures = expectedFeatures;
}

@Parameterized.Parameters(name = "{index} {0}")
public static Collection<Object[]> params() {
return Arrays.asList(
new Object[][] {
{"name LIKE 'B_r%'", 3},
{"name NOT LIKE 'B_r%'", 240},
{"pop_other between 1000000 and 3000000", 75},
{"pop_other not between 1000000 and 3000000", 168},
{"name IN ('Kiev','kobenhavn','Berlin','athens','foo')", 2},
{"name NOT IN ('Kiev','kobenhavn','Berlin','athens','foo')", 241},
{"pop_other in (1038288,1611692,3013258,3013257,3013259)", 3},
{"pop_other not in (1038288,1611692,3013258,3013257,3013259)", 240},
{"\"date\" in (DATE('2021-04-16'),DATE('2022-04-16'),DATE('2022-04-18'))", 2},
{
"\"date\" not in (DATE('2021-04-16'),DATE('2022-04-16'),DATE('2022-04-18'))",
1
},
{"start in (TIMESTAMP('2022-04-16T10:13:19Z'))", 1},
{"start not in (TIMESTAMP('2022-04-16T10:13:19Z'))", 2},
{"boolean in (true)", 2},
{"boolean not in (false)", 2}
});
}

public @Test void testConformance() throws IOException, CQLException {
DataStore ds = naturalEarthData();
int feat = featuresReturned(ds);
ds.dispose();

assertEquals(this.expectedFeatures, feat);
}

private int featuresReturned(DataStore ds) throws CQLException, IOException {
Filter filter = CQL2.toFilter(this.criteria);

return ds.getFeatureSource("ne_110m_populated_places_simple").getFeatures(filter).size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.geotools.filter.text.cql_2.conformance;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import org.geootols.filter.text.cql_2.CQL2;
import org.geotools.api.data.DataStore;
import org.geotools.api.filter.Filter;
import org.geotools.filter.text.cql2.CQLException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/** See table 12 from section A.7.2 Conformance test 26. */
@RunWith(Parameterized.class)
public class ConformanceTest26OnlineTest extends ATSOnlineTest {

private final String dataset;
private final String criteria;
private final int expectedFeatures;

public ConformanceTest26OnlineTest(String dataset, String criteria, int expectedFeatures) {
this.dataset = dataset;
this.criteria = criteria;
this.expectedFeatures = expectedFeatures;
}

@Parameterized.Parameters(name = "{index} {0} {1}")
public static Collection<Object[]> params() {
return Arrays.asList(
new Object[][] {
{"ne_110m_admin_0_countries", "S_INTERSECTS(geom,BBOX(0,40,10,50))", 8},
{"ne_110m_admin_0_countries", "S_INTERSECTS(geom,BBOX(150,-90,-150,90))", 10},
{"ne_110m_admin_0_countries", "S_INTERSECTS(geom,POINT(7.02 49.92))", 1},
{
"ne_110m_admin_0_countries",
"S_INTERSECTS(geom,BBOX(0,40,10,50)) and S_INTERSECTS(geom,BBOX(5,50,10,60))",
3
},
{
"ne_110m_admin_0_countries",
"S_INTERSECTS(geom,BBOX(0,40,10,50)) and not S_INTERSECTS(geom,BBOX(5,50,10,60))",
5
},
{
"ne_110m_admin_0_countries",
"S_INTERSECTS(geom,BBOX(0,40,10,50)) or S_INTERSECTS(geom,BBOX(-90,40,-60,50))",
10
},
{"ne_110m_populated_places_simple", "S_INTERSECTS(geom,BBOX(0,40,10,50))", 7},
{"ne_110m_rivers_lake_centerlines", "S_INTERSECTS(geom,BBOX(-180,-90,0,90))", 4}
});
}

public @Test void testConformance() throws CQLException, IOException {
DataStore ds = naturalEarthData();
int feat = featuresReturned(ds);
ds.dispose();

assertEquals(this.expectedFeatures, feat);
}

private int featuresReturned(DataStore ds) throws CQLException, IOException {
Filter filter = CQL2.toFilter(this.criteria);
return ds.getFeatureSource(this.dataset).getFeatures(filter).size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.geotools.filter.text.cql_2.conformance;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import org.geootols.filter.text.cql_2.CQL2;
import org.geotools.api.data.DataStore;
import org.geotools.api.filter.Filter;
import org.geotools.filter.text.cql2.CQLException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/** See table 7 from section A.3.5 Conformance test 8. */
@RunWith(Parameterized.class)
public class ConformanceTest8OnlineTest extends ATSOnlineTest {

private final String dataset;
private final String criteria;
private final int expectedFeatures;

public ConformanceTest8OnlineTest(String dataset, String criteria, int expectedFeatures) {
this.dataset = dataset;
this.criteria = criteria;
this.expectedFeatures = expectedFeatures;
}

@Parameterized.Parameters(name = "{index} {0} {1}")
public static Collection<Object[]> params() {
return Arrays.asList(
new Object[][] {
{"ne_110m_admin_0_countries", "NAME='Luxembourg'", 1},
{"ne_110m_admin_0_countries", "NAME>='Luxembourg'", 84},
{"ne_110m_admin_0_countries", "NAME>'Luxembourg'", 83},
{"ne_110m_admin_0_countries", "NAME<='Luxembourg'", 94},
{"ne_110m_admin_0_countries", "NAME<'Luxembourg'", 93},
{"ne_110m_admin_0_countries", "NAME<>'Luxembourg'", 176},
{"ne_110m_admin_0_countries", "POP_EST=37589262", 1},
{"ne_110m_admin_0_countries", "POP_EST>=37589262", 39},
{"ne_110m_admin_0_countries", "POP_EST>37589262", 38},
{"ne_110m_admin_0_countries", "POP_EST<=37589262", 139},
{"ne_110m_admin_0_countries", "POP_EST<37589262", 138},
{"ne_110m_admin_0_countries", "POP_EST<>37589262", 176},
{"ne_110m_populated_places_simple", "name IS NOT NULL", 243},
{"ne_110m_populated_places_simple", "name IS NULL", 0},
{"ne_110m_populated_places_simple", "name='København'", 1},
{"ne_110m_populated_places_simple", "name>='København'", 137},
{"ne_110m_populated_places_simple", "name>'København'", 136},
{"ne_110m_populated_places_simple", "name<='København'", 107},
{"ne_110m_populated_places_simple", "name<'København'", 106},
{"ne_110m_populated_places_simple", "name<>'København'", 242},
{"ne_110m_populated_places_simple", "pop_other IS NOT NULL", 243},
{"ne_110m_populated_places_simple", "pop_other IS NULL", 0},
{"ne_110m_populated_places_simple", "pop_other=1038288", 1},
{"ne_110m_populated_places_simple", "pop_other>=1038288", 123},
{"ne_110m_populated_places_simple", "pop_other>1038288", 122},
{"ne_110m_populated_places_simple", "pop_other<=1038288", 121},
{"ne_110m_populated_places_simple", "pop_other<1038288", 120},
{"ne_110m_populated_places_simple", "pop_other<>1038288", 242},
{"ne_110m_populated_places_simple", "\"date\" IS NOT NULL", 3},
{"ne_110m_populated_places_simple", "\"date\" IS NULL", 240},
{"ne_110m_populated_places_simple", "\"date\"=DATE('2022-04-16')", 1},
{"ne_110m_populated_places_simple", "\"date\">=DATE('2022-04-16')", 2},
{"ne_110m_populated_places_simple", "\"date\">DATE('2022-04-16')", 1},
{"ne_110m_populated_places_simple", "\"date\"<=DATE('2022-04-16')", 2},
{"ne_110m_populated_places_simple", "\"date\"<DATE('2022-04-16')", 1},
{"ne_110m_populated_places_simple", "\"date\"<>DATE('2022-04-16')", 2},
{"ne_110m_populated_places_simple", "start IS NOT NULL", 3},
{"ne_110m_populated_places_simple", "start IS NULL", 240},
{
"ne_110m_populated_places_simple",
"start=TIMESTAMP('2022-04-16T10:13:19Z')",
1
},
{
"ne_110m_populated_places_simple",
"start<=TIMESTAMP('2022-04-16T10:13:19Z')",
2
},
{
"ne_110m_populated_places_simple",
"start<TIMESTAMP('2022-04-16T10:13:19Z')",
1
},
{
"ne_110m_populated_places_simple",
"start>=TIMESTAMP('2022-04-16T10:13:19Z')",
2
},
{
"ne_110m_populated_places_simple",
"start>TIMESTAMP('2022-04-16T10:13:19Z')",
1
},
{
"ne_110m_populated_places_simple",
"start<>TIMESTAMP('2022-04-16T10:13:19Z')",
2
},
{"ne_110m_populated_places_simple", "boolean IS NOT NULL", 3},
{"ne_110m_populated_places_simple", "boolean IS NULL", 240},
{"ne_110m_populated_places_simple", "boolean=true", 2},
{"ne_110m_populated_places_simple", "boolean=false", 1}
});
}

public @Test void testConformance() throws CQLException, IOException {
DataStore ds = naturalEarthData();
int feat = featuresReturned(ds);
ds.dispose();

assertEquals(this.expectedFeatures, feat);
}

private int featuresReturned(DataStore ds) throws CQLException, IOException {
Filter filter = CQL2.toFilter(this.criteria);
return ds.getFeatureSource(this.dataset).getFeatures(filter).size();
}
}
Loading

0 comments on commit 595d601

Please sign in to comment.