SPARQLComplianceTest.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.fail;
import static org.assertj.core.api.Assumptions.assumeThat;

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.rdf4j.common.text.StringUtil;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.util.Models;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.Dataset;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.util.RDFInserter;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFParser;
import org.eclipse.rdf4j.rio.Rio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base functionality for SPARQL compliance test suites using a W3C-style Manifest.
 *
 * @author Jeen Broekstra
 */
public abstract class SPARQLComplianceTest {

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

	private List<String> ignoredTests = new ArrayList<>();

	public SPARQLComplianceTest() {
	}

	/**
	 * @param displayName
	 * @param testURI
	 * @param name
	 */
	protected abstract class DynamicSparqlComplianceTest {
		private final String displayName;
		private final String testURI;
		private final String name;

		public DynamicSparqlComplianceTest(String displayName, String testURI, String name) {
			this.displayName = displayName;
			this.testURI = testURI;
			this.name = name;
		}

		public void test() throws Exception {
			assumeThat(getIgnoredTests().contains(getName())).withFailMessage("test case '%s' is ignored", getName())
					.isFalse();
			try {
				setUp();
				runTest();
			} finally {
				tearDown();
			}
		}

		/**
		 * @return the displayName
		 */
		public String getDisplayName() {
			return displayName;
		}

		/**
		 * @return the testURI
		 */
		public String getTestURI() {
			return testURI;
		}

		/**
		 * @return the name
		 */
		public String getName() {
			return name;
		}

		protected void uploadDataset(Dataset dataset) throws Exception {
			try (RepositoryConnection con = getDataRepository().getConnection()) {
				// Merge default and named graphs to filter duplicates
				Set<IRI> graphURIs = new HashSet<>();
				graphURIs.addAll(dataset.getDefaultGraphs());
				graphURIs.addAll(dataset.getNamedGraphs());

				for (Resource graphURI : graphURIs) {
					upload(((IRI) graphURI), graphURI);
				}
			}
		}

		protected abstract Repository getDataRepository();

		protected void upload(IRI graphURI, Resource context) throws Exception {

			RepositoryConnection con = getDataRepository().getConnection();
			try {
				con.begin();
				RDFFormat rdfFormat = Rio.getParserFormatForFileName(graphURI.toString()).orElse(RDFFormat.TURTLE);
				RDFParser rdfParser = Rio.createParser(rdfFormat, getDataRepository().getValueFactory());
				// rdfParser.setPreserveBNodeIDs(true);

				RDFInserter rdfInserter = new RDFInserter(con);
				rdfInserter.enforceContext(context);
				rdfParser.setRDFHandler(rdfInserter);

				URL graphURL = new URL(graphURI.toString());
				try (InputStream in = graphURL.openStream()) {
					rdfParser.parse(in, graphURI.toString());
				}

				con.commit();
			} catch (Exception e) {
				if (con.isActive()) {
					con.rollback();
				}
				throw e;
			} finally {
				con.close();
			}
		}

		protected void compareGraphs(Iterable<Statement> queryResult, Iterable<Statement> expectedResult)
				throws Exception {
			if (!Models.isomorphic(expectedResult, queryResult)) {
				StringBuilder message = new StringBuilder(128);
				message.append("\n============ ");
				message.append(getName());
				message.append(" =======================\n");
				message.append("Expected result: \n");
				for (Statement st : expectedResult) {
					message.append(st.toString());
					message.append("\n");
				}
				message.append("=============");
				StringUtil.appendN('=', getName().length(), message);
				message.append("========================\n");

				message.append("Query result: \n");
				for (Statement st : queryResult) {
					message.append(st.toString());
					message.append("\n");
				}
				message.append("=============");
				StringUtil.appendN('=', getName().length(), message);
				message.append("========================\n");

				logger.error(message.toString());
				fail(message.toString());
			}
		}

		protected abstract void runTest() throws Exception;

		public abstract void tearDown() throws Exception;

		public abstract void setUp() throws Exception;

		protected void clear(Repository repo) {
			try (RepositoryConnection con = repo.getConnection()) {
				con.clear();
				con.clearNamespaces();
			}
		}
	}

	protected static final void printBindingSet(BindingSet bs, StringBuilder appendable) {
		List<String> names = new ArrayList<>(bs.getBindingNames());
		Collections.sort(names);

		for (String name : names) {
			if (bs.hasBinding(name)) {
				appendable.append(bs.getBinding(name));
				appendable.append(' ');
			}
		}
		appendable.append("\n");
	}

	/**
	 * Verifies if the selected subManifest occurs in the supplied list of excluded subdirs.
	 *
	 * @param subManifestFile the url of a sub-manifest
	 * @param excludedSubdirs an array of directory names. May be null.
	 * @return <code>false</code> if the supplied list of excluded subdirs is not empty and contains a match for the
	 *         supplied sub-manifest, <code>true</code> otherwise.
	 */
	protected static boolean includeSubManifest(String subManifestFile, List<String> excludedSubdirs) {
		boolean result = true;

		if (excludedSubdirs != null && !excludedSubdirs.isEmpty()) {
			int index = subManifestFile.lastIndexOf('/');
			String path = subManifestFile.substring(0, index);
			String sd = path.substring(path.lastIndexOf('/') + 1);

			for (String subdir : excludedSubdirs) {
				if (sd.equals(subdir)) {
					result = false;
					break;
				}
			}
		}
		return result;
	}

	/**
	 * @return the ignoredTests
	 */
	protected List<String> getIgnoredTests() {
		return ignoredTests;
	}

	protected void addIgnoredTest(String ignoredTest) {
		this.ignoredTests.add(ignoredTest);
	}

	protected boolean shouldIgnoredTest(String ignoredTest) {
		return this.ignoredTests.contains(ignoredTest);
	}

	/**
	 * @param ignoredTests the ignoredTests to set
	 */
	protected void setIgnoredTests(List<String> ignoredTests) {
		this.ignoredTests = ignoredTests;
	}

}