Skip to content
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ dependency-reduced-pom.xml
# OS specific files
.DS_Store

plans/
1 change: 1 addition & 0 deletions .java-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
21
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,20 @@ protected AbstractTestRunner(final Path path, final boolean parallel) {
this.parallel = parallel;
}

/**
* Returns the path to the test file (XQuery or XML). Used for hang reporting and diagnostics.
*
* @return the source path of the test file
*/
public Path getSourcePath() {
return path;
}

protected static Sequence executeQuery(final BrokerPool brokerPool, final Source query, final List<Function<XQueryContext, Tuple2<String, Object>>> externalVariableBindings) throws EXistException, PermissionDeniedException, XPathException, IOException, DatabaseConfigurationException {
return executeQuery(brokerPool, query, externalVariableBindings, null);
}

protected static Sequence executeQuery(final BrokerPool brokerPool, final Source query, final List<Function<XQueryContext, Tuple2<String, Object>>> externalVariableBindings, @javax.annotation.Nullable final Path moduleLoadPath) throws EXistException, PermissionDeniedException, XPathException, IOException, DatabaseConfigurationException {
final SecurityManager securityManager = requireNonNull(brokerPool.getSecurityManager(), "securityManager is null");
try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) {
final XQueryPool queryPool = brokerPool.getXQueryPool();
Expand All @@ -82,9 +95,11 @@ protected static Sequence executeQuery(final BrokerPool brokerPool, final Source

// setup misc. context
context.setBaseURI(new AnyURIValue("/db"));
if(query instanceof FileSource) {
if (moduleLoadPath != null) {
context.setModuleLoadPath(moduleLoadPath.toAbsolutePath().toString());
} else if (query instanceof FileSource) {
final Path queryPath = Paths.get(((FileSource) query).getPath().toAbsolutePath().toString());
if(Files.isDirectory(queryPath)) {
if (Files.isDirectory(queryPath)) {
context.setModuleLoadPath(queryPath.toString());
} else {
context.setModuleLoadPath(queryPath.getParent().toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr

// notify JUnit
try {
final XPathException errorReason = errorMapAsXPathException(error);
final XPathException errorReason = errorMapAsXPathException(name, error);
logOneLineIfXQueryError(name, error);
notifier.fireTestFailure(new Failure(description, errorReason));
} catch (final XPathException e) {
//signal internal failure
Expand All @@ -70,51 +71,105 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
return Sequence.EMPTY_SEQUENCE;
}

private XPathException errorMapAsXPathException(final MapType errorMap) throws XPathException {
final Sequence seqDescription = errorMap.get(new StringValue(this, "description"));
final String description;
if(seqDescription != null && !seqDescription.isEmpty()) {
description = seqDescription.itemAt(0).getStringValue();
} else {
description = "";
private void logOneLineIfXQueryError(final String testName, @Nullable final MapType errorMap) {
if (errorMap == null) {
return;
}
try {
final Sequence seqModule = errorMap.get(new StringValue(this, "module"));
final String modulePath = (seqModule != null && !seqModule.isEmpty())
? seqModule.itemAt(0).getStringValue() : null;
final String file = modulePath != null ? modulePath.replaceFirst("^.*[/\\\\]", "") : "xquery";
final Sequence seqLine = errorMap.get(new StringValue(this, "line-number"));
final int line = (seqLine != null && !seqLine.isEmpty())
? seqLine.itemAt(0).toJavaObject(int.class) : 0;
final String oneLine = "XQuery failure: " + file + (line > 0 ? ":" + line + " " : " ") + testName;
XQueryFailureLog.log(oneLine);
} catch (final Exception ignored) {
// do not affect test execution
}
}

final Sequence seqErrorCode = errorMap.get(new StringValue(this, "code"));
final ErrorCodes.ErrorCode errorCode;
if(seqErrorCode != null && !seqErrorCode.isEmpty()) {
errorCode = new ErrorCodes.ErrorCode(((QNameValue)seqErrorCode.itemAt(0)).getQName(), description);
} else {
errorCode = ErrorCodes.ERROR;
private XPathException errorMapAsXPathException(final String testName, final MapType errorMap) throws XPathException {
if (errorMap == null) {
final XPathException xpe = new XPathException(-1, -1, ErrorCodes.ERROR, "unknown error");
xpe.setStackTrace(new StackTraceElement[]{
new StackTraceElement(suiteName, testName != null ? testName : "eval", "xquery", 0)
});
return xpe;
}
final String description = getStringFromErrorMap(errorMap, "description", "");
final ErrorCodes.ErrorCode errorCode = getErrorCodeFromErrorMap(errorMap, description);
final int lineNumber = getIntFromErrorMap(errorMap, "line-number", -1);
final int columnNumber = getIntFromErrorMap(errorMap, "column-number", -1);
final XPathException xpe = new XPathException(lineNumber, columnNumber, errorCode, description);
final String moduleFileName = getModuleFileNameFromErrorMap(errorMap);
final StackTraceElement[] javaStack = getJavaStackFromErrorMap(errorMap);
setStackTraceOnException(xpe, testName, moduleFileName, lineNumber, javaStack);
return xpe;
}

final Sequence seqLineNumber = errorMap.get(new StringValue(this, "line-number"));
final int lineNumber;
if(seqLineNumber != null && !seqLineNumber.isEmpty()) {
lineNumber = seqLineNumber.itemAt(0).toJavaObject(int.class);
} else {
lineNumber = -1;
private String getStringFromErrorMap(final MapType errorMap, final String key, final String defaultVal) throws XPathException {
final Sequence seq = errorMap.get(new StringValue(this, key));
if (seq != null && !seq.isEmpty()) {
return seq.itemAt(0).getStringValue();
}
return defaultVal;
}

final Sequence seqColumnNumber = errorMap.get(new StringValue(this, "column-number"));
final int columnNumber;
if(seqColumnNumber != null && !seqColumnNumber.isEmpty()) {
columnNumber = seqColumnNumber.itemAt(0).toJavaObject(int.class);
} else {
columnNumber = -1;
private ErrorCodes.ErrorCode getErrorCodeFromErrorMap(final MapType errorMap, final String description) throws XPathException {
final Sequence seq = errorMap.get(new StringValue(this, "code"));
if (seq != null && !seq.isEmpty()) {
return new ErrorCodes.ErrorCode(((QNameValue) seq.itemAt(0)).getQName(), description);
}
return ErrorCodes.ERROR;
}

final XPathException xpe = new XPathException(lineNumber, columnNumber, errorCode, description);
private int getIntFromErrorMap(final MapType errorMap, final String key, final int defaultVal) throws XPathException {
final Sequence seq = errorMap.get(new StringValue(this, key));
if (seq != null && !seq.isEmpty()) {
return seq.itemAt(0).toJavaObject(int.class);
}
return defaultVal;
}

final Sequence seqJavaStackTrace = errorMap.get(new StringValue(this, "java-stack-trace"));
if (seqJavaStackTrace != null && !seqJavaStackTrace.isEmpty()) {
try {
xpe.setStackTrace(convertStackTraceElements(seqJavaStackTrace));
} catch (final NullPointerException e) {
e.printStackTrace();
}
private String getModuleFileNameFromErrorMap(final MapType errorMap) throws XPathException {
final Sequence seq = errorMap.get(new StringValue(this, "module"));
if (seq == null || seq.isEmpty()) {
return null;
}
final String path = seq.itemAt(0).getStringValue();
return path != null ? path.replaceFirst("^.*[/\\\\]", "") : null;
}

return xpe;
private StackTraceElement[] getJavaStackFromErrorMap(final MapType errorMap) throws XPathException {
final Sequence seq = errorMap.get(new StringValue(this, "java-stack-trace"));
if (seq == null || seq.isEmpty()) {
return null;
}
try {
return convertStackTraceElements(seq);
} catch (final NullPointerException e) {
e.printStackTrace();
return null;
}
}

private void setStackTraceOnException(final XPathException xpe, final String testName,
final String moduleFileName, final int lineNumber, final StackTraceElement[] javaStack) {
final StackTraceElement xqueryFrame = new StackTraceElement(
suiteName,
testName != null ? testName : "eval",
moduleFileName != null ? moduleFileName : "xquery",
lineNumber > 0 ? lineNumber : 0);
if (javaStack != null && javaStack.length > 0) {
final StackTraceElement[] fullStack = new StackTraceElement[1 + javaStack.length];
fullStack[0] = xqueryFrame;
System.arraycopy(javaStack, 0, fullStack, 1, javaStack.length);
xpe.setStackTrace(fullStack);
} else {
xpe.setStackTrace(new StackTraceElement[] { xqueryFrame });
}
}

private static final Pattern PTN_CAUSED_BY = Pattern.compile("Caused by:\\s([a-zA-Z0-9_$\\.]+)(?::\\s(.+))?");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.IntegerValue;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.StringValue;
Expand All @@ -36,23 +37,34 @@
import org.junit.runner.notification.RunNotifier;
import org.xml.sax.SAXException;

import javax.annotation.Nullable;
import javax.xml.transform.OutputKeys;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

import static org.exist.xquery.FunctionDSL.param;
import static org.exist.xquery.FunctionDSL.params;

public class ExtTestFailureFunction extends JUnitIntegrationFunction {

@Nullable
private final Path sourcePath;

public ExtTestFailureFunction(final XQueryContext context, final String parentName, final RunNotifier notifier) {
this(context, parentName, notifier, null);
}

public ExtTestFailureFunction(final XQueryContext context, final String parentName, final RunNotifier notifier, @Nullable final Path sourcePath) {
super("ext-test-failure-function",
params(
param("name", Type.STRING, "name of the test"),
param("expected", Type.MAP_ITEM, "expected result of the test"),
param("actual", Type.MAP_ITEM, "actual result of the test")
), context, parentName, notifier);
this.sourcePath = sourcePath;
}

@Override
Expand All @@ -70,10 +82,27 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr

// notify JUnit
try {
final AssertionError failureReason = new ComparisonFailure("", expectedToString(expected), actualToString(actual));

// NOTE: We remove the StackTrace, because it is not useful to have a Java Stack Trace pointing into the XML XQuery Test Suite code
failureReason.setStackTrace(new StackTraceElement[0]);
final String fileName = getFileNameFromActual(actual);
final int lineNumber = getLineFromActual(actual);
// Short one-line for logs (filename only)
final String shortFileName = fileName != null ? lastPathSegment(fileName) : null;
final String shortLocation = shortFileName != null ? shortFileName + (lineNumber > 0 ? ":" + lineNumber : "") : null;
String oneLine = "XQuery failure: " + (shortLocation != null ? shortLocation + " " : "") + name;
if (shortFileName != null && lineNumber > 0) {
oneLine += "\n\tat (" + shortFileName + ":" + lineNumber + ")";
}
XQueryFailureLog.log(oneLine);
final AssertionError failureReason = new ComparisonFailure(oneLine, expectedToString(expected), actualToString(actual));

// Stack trace for IDE navigation. IntelliJ linkifies short "filename:line" in stack traces
// but not absolute paths; use short filename so the stack line becomes clickable.
if (shortFileName != null) {
failureReason.setStackTrace(new StackTraceElement[]{
new StackTraceElement(" ", " ", shortFileName, lineNumber > 0 ? lineNumber : 1)
});
} else {
failureReason.setStackTrace(new StackTraceElement[0]);
}

notifier.fireTestFailure(new Failure(description, failureReason));
} catch (final XPathException | SAXException | IOException | IllegalStateException e) {
Expand All @@ -84,6 +113,52 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
return Sequence.EMPTY_SEQUENCE;
}

/**
* Last path segment for short display in failure message and stack trace (short name makes IDE stack trace link clickable).
*/
private static String lastPathSegment(final String path) {
if (path == null || path.isEmpty()) {
return path;
}
try {
final Path p = Paths.get(path);
return p.getFileName() != null ? p.getFileName().toString() : path;
} catch (final Exception ignored) {
return path;
}
}

/** Source from inspect can be full path or short identifier (implementation-dependent); we use last segment for IDE links. */
private String getFileNameFromActual(final MapType actual) throws XPathException {
final Sequence seqSource = actual.get(new StringValue(this, "source"));
if (!seqSource.isEmpty()) {
final String s = seqSource.itemAt(0).getStringValue();
if (s != null && !s.isEmpty()) {
return s;
}
}
if (sourcePath != null) {
return sourcePath.getFileName() != null ? sourcePath.getFileName().toString() : sourcePath.toString();
}
return null;
}

private int getLineFromActual(final MapType actual) throws XPathException {
final Sequence seqLine = actual.get(new StringValue(this, "line"));
if (!seqLine.isEmpty()) {
final Item item = seqLine.itemAt(0);
if (item instanceof IntegerValue) {
return (int) ((IntegerValue) item).getLong();
}
try {
return Integer.parseInt(item.getStringValue());
} catch (final NumberFormatException ignored) {
// fall through to 0
}
}
return 0;
}

private String expectedToString(final MapType expected) throws XPathException, SAXException, IOException {
final Sequence seqExpectedValue = expected.get(new StringValue(this, "value"));
if(!seqExpectedValue.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public void run(final RunNotifier notifier) {
// set callback functions for notifying junit!
context -> new Tuple2<>("test-ignored-function", new FunctionReference(new FunctionCall(context, new ExtTestIgnoredFunction(context, getSuiteName(), notifier)))),
context -> new Tuple2<>("test-started-function", new FunctionReference(new FunctionCall(context, new ExtTestStartedFunction(context, getSuiteName(), notifier)))),
context -> new Tuple2<>("test-failure-function", new FunctionReference(new FunctionCall(context, new ExtTestFailureFunction(context, getSuiteName(), notifier)))),
context -> new Tuple2<>("test-failure-function", new FunctionReference(new FunctionCall(context, new ExtTestFailureFunction(context, getSuiteName(), notifier, path)))),
context -> new Tuple2<>("test-assumption-failed-function", new FunctionReference(new FunctionCall(context, new ExtTestAssumptionFailedFunction(context, getSuiteName(), notifier)))),
context -> new Tuple2<>("test-error-function", new FunctionReference(new FunctionCall(context, new ExtTestErrorFunction(context, getSuiteName(), notifier)))),
context -> new Tuple2<>("test-finished-function", new FunctionReference(new FunctionCall(context, new ExtTestFinishedFunction(context, getSuiteName(), notifier))))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

package org.exist.test.runner;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
* Appends one line per XQuery test failure/error to a log file for CI and grep.
* Writes to {@code target/surefire-reports/xquery-failures.log} when running under Maven;
* failures to write are ignored so tests are never affected.
*/
public final class XQueryFailureLog {

private static final String LOG_NAME = "xquery-failures.log";

private XQueryFailureLog() {
}

/**
* Append a single line to the XQuery failures log (one line per failure for CI/grep).
* If the message contains newlines, only the first line is written.
* Safe to call from any thread; IO errors are swallowed.
*
* @param message failure message (e.g. "XQuery failure: file.xq:34 testName"; may contain newlines)
*/
public static void log(final String message) {
if (message == null || message.isEmpty()) {
return;
}
final String firstLine = message.contains("\n") ? message.substring(0, message.indexOf('\n')) : message;
try {
final Path dir = Paths.get(System.getProperty("user.dir", "."), "target", "surefire-reports");
if (!Files.isDirectory(dir)) {
Files.createDirectories(dir);
}
final Path file = dir.resolve(LOG_NAME);
Files.write(file, (firstLine + "\n").getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (final IOException ignored) {
// do not affect test execution
}
}
}
Loading
Loading