You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by cl...@apache.org on 2015/05/10 15:04:11 UTC

[42/50] [abbrv] jena git commit: Added initial contract tests added testing_framework

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestSuite.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestSuite.java b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestSuite.java
new file mode 100644
index 0000000..7e19122
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestSuite.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.testing_framework.manifest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import org.junit.internal.runners.ErrorReportingRunner;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.ParentRunner;
+import org.junit.runners.Suite;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.RunnerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.jena.shared.JenaException;
+
+/**
+ * Class that runs the Manifest annotated tests.
+ * 
+ * Used with <code>@RunWith( ManifestSuite.class )</code> this class loads a 
+ * manifest test suite file and adds the tests.
+ * <p>
+ * Tests annotated with <code>@RunWith( ManifestSuite.class )</code> must
+ * have a <code>ManifestFile</code> annotation specifying the path to the manifest file
+ * </p>
+ */
+public class ManifestSuite extends ParentRunner<Runner> {
+	private static final Logger LOG = LoggerFactory
+			.getLogger(ManifestSuite.class);
+	private final List<Runner> fRunners;
+	private final ManifestItemHandler itemHandler;
+	private final ManifestFile mf;
+
+	/**
+	 * Called reflectively on classes annotated with
+	 * <code>@RunWith(Suite.class)</code>
+	 * 
+	 * @param cls
+	 *            the root class
+	 * @param builder
+	 *            builds runners for classes in the suite
+	 * @throws Throwable
+	 */
+	public ManifestSuite(Class<? extends ManifestItemHandler> cls,
+			RunnerBuilder builder) throws Throwable {
+		super(cls);
+
+		List<Throwable> errors = new ArrayList<Throwable>();
+
+		mf = cls.getAnnotation(ManifestFile.class);
+		if (mf == null) {
+			throw new IllegalStateException(
+					"ManifestSuite requries ManifestFile annotation");
+		}
+		itemHandler = cls.newInstance();
+
+		Runner[] runner = new Runner[1];
+		try {
+			runner[0] = oneManifest(new Manifest(mf.value()),
+					new ArrayList<Runner>());
+		} catch (JenaException ex) {
+			runner[0] = new ErrorReportingRunner(null, ex);
+		}
+
+		if (!errors.isEmpty()) {
+			throw new InitializationError(errors);
+		}
+		fRunners = Collections.unmodifiableList(Arrays.asList(runner));
+	}
+
+	private Runner oneManifest(final Manifest manifest, List<Runner> r) {
+
+		// Recurse
+		for (Iterator<String> iter = manifest.includedManifests(); iter
+				.hasNext();) {
+			try {
+				r.add(oneManifest(new Manifest(iter.next()),
+						new ArrayList<Runner>()));
+			} catch (JenaException ex) {
+				r.add(new ErrorReportingRunner(null, ex));
+			}
+		}
+		itemHandler.setTestRunnerList(r);
+		manifest.apply(itemHandler);
+		try {
+			return new Suite((Class<?>) null, r) {
+
+				@Override
+				protected String getName() {
+					return manifest.getName();
+				}
+
+			};
+		} catch (InitializationError e) {
+			return new ErrorReportingRunner(null, e);
+		}
+	}
+
+	@Override
+	protected List<Runner> getChildren() {
+		return fRunners;
+	}
+
+	@Override
+	protected Description describeChild(Runner child) {
+		return child.getDescription();
+	}
+
+	@Override
+	protected void runChild(Runner child, RunNotifier notifier) {
+		child.run(notifier);
+	}
+
+	/**
+	 * Returns a name used to describe this Runner
+	 */
+	@Override
+	protected String getName() {
+		return String.format("%s - %s", super.getName(), mf.value());
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTest.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTest.java b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTest.java
new file mode 100644
index 0000000..cfde240
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTest.java
@@ -0,0 +1,13 @@
+package org.apache.jena.testing_framework.manifest;
+
+public abstract class ManifestTest {
+
+	protected ManifestItem manifestItem;
+
+	public final void setManifestItem(ManifestItem manifestItem) {
+		this.manifestItem = manifestItem;
+	}
+
+	abstract public void runTest();
+
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTestRunner.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTestRunner.java b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTestRunner.java
new file mode 100644
index 0000000..616b444
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/manifest/ManifestTestRunner.java
@@ -0,0 +1,75 @@
+package org.apache.jena.testing_framework.manifest;
+
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.runner.Description;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+public class ManifestTestRunner extends BlockJUnit4ClassRunner {
+
+	private ManifestItem manifestItem;
+
+	public ManifestTestRunner(ManifestItem manifestItem,
+			Class<? extends ManifestTest> cls) throws InitializationError {
+		super(cls);
+		this.manifestItem = manifestItem;
+	}
+
+	/**
+	 * Returns the name that describes {@code method} for {@link Description}s.
+	 * Default implementation is the method's name
+	 */
+	@Override
+	protected String testName(FrameworkMethod method) {
+		return manifestItem.getTestName();
+	}
+
+	/**
+	 * Returns the methods that run tests. Default implementation returns all
+	 * methods annotated with {@code @Test} on this class and superclasses that
+	 * are not overridden.
+	 */
+	@Override
+	protected List<FrameworkMethod> computeTestMethods() {
+		FrameworkMethod[] lst = new FrameworkMethod[1];
+
+		try {
+			lst[0] = new FrameworkMethod(getTestClass().getJavaClass()
+					.getMethod("runTest")) {
+
+				@Override
+				public String getName() {
+					return manifestItem.getTestName();
+				}
+			};
+		} catch (NoSuchMethodException e) {
+			throw new IllegalStateException(e);
+		} catch (SecurityException e) {
+			throw new IllegalStateException(e);
+		}
+		return Arrays.asList(lst);
+	}
+
+	@Override
+	public Description getDescription() {
+		return Description.createTestDescription(this.getTestClass()
+				.getJavaClass(), manifestItem.getTestName(), new Annotation[0]);
+	}
+
+	/**
+	 * Returns a new fixture for running a test. Default implementation executes
+	 * the test class's no-argument constructor (validation should have ensured
+	 * one exists).
+	 */
+	@Override
+	protected Object createTest() throws Exception {
+		ManifestTest instance = (ManifestTest) super.createTest();
+		instance.setManifestItem(manifestItem);
+		return instance;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/package-info.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/package-info.java b/jena-core/src/test/java/org/apache/jena/testing_framework/package-info.java
new file mode 100644
index 0000000..96f3e42
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/package-info.java
@@ -0,0 +1,109 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ */
+package org.apache.jena.testing_framework;
+
+/**
+ * Foo set of classes providing support for testing.
+ * <p>
+ * Testing guidelines/suggestions.
+ * </p><p>
+ * Interface tests are built so that developers may test that implementations meet the contract
+ * set out in the interface and accompanying documentation.
+ * </p>
+ * <h4>Producers</h4>
+ * <p>
+ * The test and suites use an instance of the [INTERFACE]ProducerInterface to create an instance
+ * of the the Object being tested.   
+ * </p>
+ * <h4>Tests</h4>
+ * <p>
+ * Interface tests are noted as Abstract[INTERFACE]Test.  Implementations of [INTERFACE] should
+ * create a concrete implementation of Abstract[INTERFACE]Test with an [INTERFACE]Producer to create
+ * instances of the Object.  Passing the test indicates a compliance with the base interface 
+ * definition.
+ * </p><p>
+ * In general to implement a test requires a few lines of code as is noted in the example below
+ * where the new Foo graph implementation is being tested.</p>
+ * <pre><code>
+ * public class FooGraphTest extends AbstractGraphTest {
+ * 
+ *   // the graph producer to use while running
+ *   GraphProducerInterface graphProducer = new FooGraphTest.GraphProducer();
+ * 
+ *   @Override
+ *   protected GraphProducerInterface getGraphProducer() {
+ *     return graphProducer;
+ *   }
+ * 
+ *   // the implementation of the graph producer.
+ *   public static class GraphProducer extends AbstractGraphProducer {
+ * 
+ *     @Override
+ *     protected Graph createNewGraph() {
+ *       return new FooGraph();
+ *     }
+ *   }
+ * }
+ * </code></pre>
+ * <h4>Suites</h4>
+ * <p>
+ * Test suites are named as Abstract[INTERFACE]Suite.  Suites contain several tests (see above)
+ * that exercise all of the tests for the components of the object under test.  For example the 
+ * graph suite includes tests for the graph itself, the reifier, finding literals, recursive 
+ * subgraph extraction, event manager, and transactions.  Running the suites is a bit more 
+ * complicated then running the tests.
+ * </p>
+ * Suites are created using the JUnit 4 <code>@RunWith(Suite.class)</code and 
+ * <code>@Suite.SuiteClasses({ })</code> annotations.  This has several effects that the developer 
+ * should know about:</p>
+ * <ul>
+ * <li>The suite class does not get instantiated during the run.</li>
+ * <li>The test class names must be known at coding time (not run time) as they are listed in the
+ * annotation.</li>
+ * <li>Configuration of the tests has to occur during the static initialization phase of class 
+ * loading.</li>
+ * </ul>
+ * <p>
+ * To meet these requirements the AbstractGraphSuite has a static variable that holds the instance
+ * of the GraphProducerInterface and a number of local static implementations of the Abstract tests
+ * that implement the "getGraphProducer()" method by returning the static instance.  The names of 
+ * the local graphs are then used in the @Suite.SuiteClasses annotation.  This makes creating an
+ * instance of the AbstractGraphSuite for a graph implementation fairly simple as is noted below.
+ * </p>
+ * <pre><code>
+ * public class FooGraphSuite extends AbstractGraphSuite {
+ *   @BeforeClass
+ *   public static void beforeClass() {
+ *     setGraphProducer(new GraphProducer());
+ *   }
+ *   
+ *   public static class GraphProducer extends AbstractGraphProducer {
+ *     @Override
+ *     protected Graph createNewGraph() {
+ *       return new FooGraph();
+ *     }
+ *   }
+ * }
+ * </code></pre>
+ * <p>
+ * <b>Note:</b> that the beforeClass() method is annotated with @BeforeClass.  the @BeforeClass 
+ * causes it to be run once before any of the test methods in the class. This will set the static
+ * instance of the graph producer before the suite is run so that it is provided to the enclosed
+ * tests.
+ * </p> 
+ */

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleItem.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleItem.java b/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleItem.java
new file mode 100644
index 0000000..65aff5b
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleItem.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.testing_framework.tuples;
+
+/**
+ * The unit found in a line of a tuple. Can be a string (quoted, possibly with
+ * the datatype, or unquoted) or a URI.
+ */
+public class TupleItem {
+	public static final int URI = 0;
+	public static final int STRING = 1;
+	public static final int UNKNOWN = 2;
+	public static final int UNQUOTED = 3;
+	public static final int ANON = 4;
+
+	String rep;
+	String datatype;
+	String asFound;
+	int itemType;
+
+	TupleItem(String value, String valAsFound, int type, String dt) {
+		rep = value;
+		asFound = valAsFound;
+		itemType = type;
+		datatype = dt;
+	}
+
+	public int getType() {
+		return itemType;
+	}
+
+	public boolean isURI() {
+		return itemType == URI;
+	}
+
+	public boolean isString() {
+		return itemType == STRING;
+	}
+
+	public boolean isUnknown() {
+		return itemType == UNKNOWN;
+	}
+
+	public boolean isUnquoted() {
+		return itemType == UNQUOTED;
+	}
+
+	public boolean isAnon() {
+		return itemType == ANON;
+	}
+
+	public String get() {
+		return rep;
+	}
+
+	public String getDT() {
+		return datatype;
+	}
+
+	public String asQuotedString() {
+		return asFound;
+	}
+
+	@Override
+	public String toString() {
+		return rep;
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/b293ee8a/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleSet.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleSet.java b/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleSet.java
new file mode 100644
index 0000000..8e78879
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/testing_framework/tuples/TupleSet.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.testing_framework.tuples;
+
+import java.io.*;
+import java.util.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TupleSet implements Iterator<List<TupleItem>> {
+	BufferedReader in;
+	public String line = null;
+	public int lineNumber = 0;
+
+	static final char COMMENTCHAR = '#';
+	List<TupleItem> current = null;
+	boolean finished = false;
+
+	protected static Logger logger = LoggerFactory.getLogger(TupleSet.class);
+
+	/** Creates new TupleSet */
+	public TupleSet(Reader r) {
+		if (!(r instanceof BufferedReader))
+			in = new BufferedReader(r);
+		else
+			in = (BufferedReader) r;
+	}
+
+	@Override
+	public boolean hasNext() {
+		if (finished)
+			return false;
+
+		if (current == null)
+			current = tuple();
+		return current != null;
+	}
+
+	@Override
+	public List<TupleItem> next() {
+		if (hasNext()) {
+			List<TupleItem> x = current;
+			current = null;
+			return x;
+		} else
+			return null;
+	}
+
+	@Override
+	public void remove() {
+		throw new java.lang.UnsupportedOperationException("TupleSet.remove");
+	}
+
+	private List<TupleItem> tuple() {
+
+		try {
+			lineNumber++;
+			line = in.readLine();
+		} catch (IOException e) {
+		}
+
+		if (line == null) {
+			finished = true;
+			return null;
+		}
+
+		// System.out.println("Line: "+line) ;
+		List<TupleItem> tuple = new ArrayList<TupleItem>();
+		int i = 0;
+		int j = 0;
+		boolean errorFound = false;
+
+		tupleLoop: for (;;) {
+			// Move to beginning of next item.
+			i = skipwhitespace(line, j);
+
+			if (i < 0)
+				break;
+
+			int iStart = -2; // Points to the beginning of the item as found
+			int jStart = -2; // Points to the item without quotes
+			int iFinish = -2; // Points after the end of the item as found
+			int jFinish = -2; // Points after the end of the item without quotes
+			int dtStart = -2; // Points to start of datatype (after < quote)
+			int dtFinish = -2; // Points to end of datatype
+			int type = TupleItem.UNKNOWN;
+
+			switch (line.charAt(i)) {
+			case COMMENTCHAR:
+				break tupleLoop;
+			case '<':
+				type = TupleItem.URI;
+				iStart = i;
+				jStart = i + 1;
+				int newPosn = parseURI(i, line);
+				if (newPosn < 0) {
+					errorFound = true;
+					break tupleLoop;
+				}
+				j = newPosn;
+
+				iFinish = j + 1;
+				jFinish = j;
+				break;
+			case '"':
+				type = TupleItem.STRING;
+				iStart = i;
+				jStart = i + 1;
+				boolean inEscape = false;
+				for (j = i + 1; j < line.length(); j++) {
+					char ch = line.charAt(j);
+					if (inEscape) {
+						// ToDo: escape
+						inEscape = false;
+						continue;
+					}
+					// Not an escape
+					if (ch == '"')
+						break;
+
+					if (ch == '\\')
+						inEscape = true;
+					if (ch == '\n' || ch == '\r') {
+						errorFound = true;
+						break tupleLoop;
+
+					}
+				}
+
+				// Malformed
+				if (j == line.length()) {
+					errorFound = true;
+					break tupleLoop;
+				}
+
+				iFinish = j + 1;
+				jFinish = j;
+				// RDF literals may be followed by their type.
+
+				if (j < line.length() - 3 && line.charAt(j + 1) == '^'
+						&& line.charAt(j + 2) == '^'
+						&& line.charAt(j + 3) == '<') {
+					dtFinish = parseURI(j + 3, line);
+					dtStart = j + 4;
+					if (dtFinish < 0) {
+						errorFound = true;
+						break tupleLoop;
+					}
+					j = dtFinish + 1;
+					// String dt = line.substring(dtStart, dtFinish) ;
+					// System.out.println("I see a datatype:"+dt) ;
+				}
+
+				break;
+			case '_':
+				type = TupleItem.ANON;
+				iStart = i;
+				for (j = i + 1; j < line.length(); j++) {
+					char ch = line.charAt(j);
+					if (ch == ' ' || ch == '\t' || ch == '.')
+						break;
+					if (!Character.isLetterOrDigit(ch) && !(ch == '_')
+							&& !(ch == ':')) {
+						errorFound = true;
+						break tupleLoop;
+					}
+				}
+				iFinish = j;
+				jStart = iStart;
+				jFinish = iFinish;
+				break;
+			case '.':
+			case '\n':
+			case '\r':
+				return tuple;
+			default:
+				type = TupleItem.UNQUOTED;
+				iStart = i;
+				jStart = i;
+				for (j = i + 1; j < line.length(); j++) {
+					char ch = line.charAt(j);
+					if (ch == ' ' || ch == '\t' || ch == '.')
+						break;
+
+					// if ( ! Character.isLetterOrDigit(line.charAt(i)) )
+					// {
+					// errorFound = true ;
+					// break tupleLoop;
+					// }
+				}
+				// Malformed
+				if (j == line.length() + 1) {
+					errorFound = true;
+					break tupleLoop;
+				}
+				iFinish = j;
+				jFinish = j;
+				break;
+			}
+			String item = line.substring(jStart, jFinish);
+			String literal = line.substring(iStart, iFinish);
+			String dt = null;
+			if (dtStart > 0)
+				dt = line.substring(dtStart, dtFinish);
+
+			tuple.add(new TupleItem(item, literal, type, dt));
+			j++;
+			// End of item.
+		}
+		// End of this line.
+		if (errorFound) {
+			logger.error("Error in TupleSet.tuple: " + line);
+
+			String s = "";
+			int k = 0;
+			for (; k < i; k++)
+				s = s + " ";
+			s = s + "^";
+			for (; k < j - 1; k++)
+				s = s + " ";
+			s = s + "^";
+			logger.error(s);
+			return null;
+		}
+
+		if (tuple.size() == 0) {
+			// Nothing found : loop by tail recursion
+			return tuple();
+		}
+		return tuple;
+	}
+
+	private int skipwhitespace(String s, int i) {
+		for (; i < s.length(); i++) {
+			char ch = s.charAt(i);
+			// Horizonal whitespace
+			if (ch != ' ' && ch != '\t')
+				return i;
+		}
+		return -1;
+	}
+
+	private int parseURI(int i, String line) {
+		int j;
+		for (j = i + 1; j < line.length(); j++) {
+			char ch = line.charAt(j);
+			if (ch == '>')
+				break;
+			if (ch == '\n' || ch == '\r')
+				return -1;
+		}
+		// Malformed
+		if (j == line.length())
+			return -2;
+		return j;
+	}
+}