SPARQLSyntaxComplianceTest.java

/*******************************************************************************
 * Copyright (c) 2020 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.testsuite.query.parser.sparql.manifest;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;

import org.eclipse.rdf4j.common.io.IOUtil;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.query.algebra.DeleteData;
import org.eclipse.rdf4j.query.algebra.InsertData;
import org.eclipse.rdf4j.query.algebra.UpdateExpr;
import org.eclipse.rdf4j.query.parser.ParsedOperation;
import org.eclipse.rdf4j.query.parser.ParsedUpdate;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.eclipse.rdf4j.repository.sail.helpers.SailUpdateExecutor;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFParseException;
import org.eclipse.rdf4j.sail.NotifyingSailConnection;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A test suite that runs SPARQL syntax tests.
 *
 * @author Jeen Broekstra
 */
public abstract class SPARQLSyntaxComplianceTest extends SPARQLComplianceTest {

	private static final Logger logger = LoggerFactory.getLogger(SPARQLSyntaxComplianceTest.class);

	private static final List<String> excludedSubdirs = List.of();

	public class DynamicSPARQLSyntaxComplianceTest extends DynamicSparqlComplianceTest {
		private final String queryFileURL;
		private final boolean positiveTest;

		public DynamicSPARQLSyntaxComplianceTest(String displayName, String testURI, String name, String queryFileURL,
				boolean positiveTest) {

			super(displayName, testURI, name);
			this.queryFileURL = queryFileURL;
			this.positiveTest = positiveTest;
		}

		@Override
		protected void runTest() throws Exception {
			InputStream stream = new URL(queryFileURL).openStream();
			String query = IOUtil.readString(new InputStreamReader(stream, StandardCharsets.UTF_8));
			stream.close();

			try {
				ParsedOperation operation = parseOperation(query, queryFileURL);

				assertThatNoException().isThrownBy(() -> {
					int hashCode = operation.hashCode();
					if (hashCode == System.identityHashCode(operation)) {
						throw new UnsupportedOperationException(
								"hashCode() result is the same as  the identityHashCode in "
										+ operation.getClass().getName());
					}
				});

				if (!positiveTest) {
					boolean dataBlockUpdate = false;
					if (operation instanceof ParsedUpdate) {
						for (UpdateExpr updateExpr : ((ParsedUpdate) operation).getUpdateExprs()) {
							if (updateExpr instanceof InsertData || updateExpr instanceof DeleteData) {
								// parsing for these operation happens during actual
								// execution, so try and execute.
								dataBlockUpdate = true;

								MemoryStore store = new MemoryStore();
								store.init();
								NotifyingSailConnection conn = store.getConnection();
								try {
									conn.begin();
									SailUpdateExecutor exec = new SailUpdateExecutor(conn, store.getValueFactory(),
											null);
									exec.executeUpdate(updateExpr, null, null, true, -1);
									conn.rollback();
									fail("Negative test case should have failed to parse");
								} catch (SailException e) {
									if (!(e.getCause() instanceof RDFParseException)) {
										logger.error("unexpected error in negative test case", e);
										fail("unexpected error in negative test case");
									}
									// fall through - a parse exception is expected for a
									// negative test case
									conn.rollback();
								} finally {
									conn.close();
								}
							}
						}
					}
					if (!dataBlockUpdate) {
						fail("Negative test case should have failed to parse");
					}
				}
			} catch (MalformedQueryException e) {
				if (positiveTest) {
					e.printStackTrace();
					fail("Positive test case failed: " + e.getMessage());
				}
			}
		}

		@Override
		protected Repository getDataRepository() {
			return null; // not needed in syntax tests
		}

		@Override
		public void tearDown() throws Exception {
			// not needed in syntax tests
		}

		@Override
		public void setUp() throws Exception {
			// not needed in syntax tests
		}
	}

	@TestFactory
	public Collection<DynamicTest> getTestData() {
		List<DynamicTest> tests = new ArrayList<>();

		Deque<String> manifests = new ArrayDeque<>();
		manifests.add(SPARQLSyntaxComplianceTest.class.getClassLoader()
				.getResource("testcases-sparql-1.1-w3c/manifest-all.ttl")
				.toExternalForm());
		while (!manifests.isEmpty()) {
			String pop = manifests.pop();
			SPARQLSyntaxManifest manifest = new SPARQLSyntaxManifest(pop);
			tests.addAll(manifest.tests);
			manifests.addAll(manifest.subManifests);
		}
		return tests;
	}

	class SPARQLSyntaxManifest {
		List<DynamicTest> tests = new ArrayList<>();
		List<String> subManifests = new ArrayList<>();

		public SPARQLSyntaxManifest(String filename) {
			SailRepository sailRepository = new SailRepository(new MemoryStore());
			try (SailRepositoryConnection connection = sailRepository.getConnection()) {
				connection.add(new URL(filename), filename, RDFFormat.TURTLE);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}

			try (SailRepositoryConnection connection = sailRepository.getConnection()) {

				String manifestQuery = " PREFIX qt: <http://www.w3.org/2001/sw/DataAccess/tests/test-query#> "
						+ "PREFIX mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> "
						+ "SELECT DISTINCT ?manifestFile "
						+ "WHERE { [] mf:include [ rdf:rest*/rdf:first ?manifestFile ] . }   ";

				try (TupleQueryResult manifestResults = connection
						.prepareTupleQuery(QueryLanguage.SPARQL, manifestQuery, filename)
						.evaluate()) {
					for (BindingSet bindingSet : manifestResults) {
						String subManifestFile = bindingSet.getValue("manifestFile").stringValue();
						if (includeSubManifest(subManifestFile, excludedSubdirs)) {
							subManifests.add(subManifestFile);
						}
					}
				}

				StringBuilder query = new StringBuilder(512);
				query.append("PREFIX mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> ");
				query.append("PREFIX qt: <http://www.w3.org/2001/sw/DataAccess/tests/test-query#> ");
				query.append("PREFIX dawgt: <http://www.w3.org/2001/sw/DataAccess/tests/test-dawg#> ");
				query.append("SELECT ?TestURI ?Name ?Action ?Type ");
				query.append("WHERE { [] rdf:first ?TestURI. ");
				query.append("        ?TestURI a ?Type ; ");
				query.append("                 mf:name ?Name ;");
				query.append("                 mf:action ?Action ;");
				query.append("                 dawgt:approval dawgt:Approved . ");
				query.append(
						"        FILTER(?Type IN (mf:PositiveSyntaxTest, mf:NegativeSyntaxTest, mf:PositiveSyntaxTest11, mf:NegativeSyntaxTest11, mf:PositiveUpdateSyntaxTest11, mf:NegativeUpdateSyntaxTest11)) ");
				query.append(" } ");

				try (TupleQueryResult result = connection.prepareTupleQuery(query.toString()).evaluate()) {
					for (BindingSet bs : result) {
						// FIXME I'm sure there's a neater way to do this
						String testName = bs.getValue("Name").stringValue();
						String displayName = filename
								.substring(filename.lastIndexOf("testcases-sparql-1.1-w3c/")
										+ "testcases-sparql-1.1-w3c/".length(), filename.lastIndexOf("/"))
								+ ": " + testName;

						IRI testURI = (IRI) bs.getValue("TestURI");
						Value action = bs.getValue("Action");
						String type = bs.getValue("Type").toString();
						boolean positiveTest = type
								.equals("http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#PositiveSyntaxTest11")
								|| type.equals(
										"http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#PositiveUpdateSyntaxTest11");

						DynamicSPARQLSyntaxComplianceTest ds11ut = new DynamicSPARQLSyntaxComplianceTest(displayName,
								testURI.stringValue(), testName, action.stringValue(),
								positiveTest);
						if (!shouldIgnoredTest(testName)) {
							tests.add(DynamicTest.dynamicTest(displayName, ds11ut::test));
						}
					}
				}

			}

		}

	}

	protected abstract ParsedOperation parseOperation(String operation, String fileURL) throws MalformedQueryException;
}