Trac #120: New attribute 'line' for methods in XML report.
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index eeb3d01..05dd9cb 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -22,7 +22,12 @@
 

 <h3>New Features</h3>

 <ul>

-  <li>Optional locale attribute for HTML reports (Track #122).</li>

+  <li>New attribute <code>line</code> for <code>method</code> elements in the

+      XML report containing the first source line number of the method.

+      (Track #120).</li>

+  <li>Optional <code>locale</code> attribute for number rendering HTML reports,

+      also available as an attribute of the <code>html</code> tag of the

+      <code>report</code> Ant task (Track #122).</li>

   <li>Coverage tables in HTML report are now sortable (Track #98).</li>

   <li>The <code>report</code> Ant task issues a warning if source files are

       provided but class files do not contain debug information to collect line

diff --git a/org.jacoco.report.test/src/org/jacoco/report/xml/XMLReportNodeHandlerTest.java b/org.jacoco.report.test/src/org/jacoco/report/xml/XMLReportNodeHandlerTest.java
new file mode 100644
index 0000000..02d4b55
--- /dev/null
+++ b/org.jacoco.report.test/src/org/jacoco/report/xml/XMLReportNodeHandlerTest.java
@@ -0,0 +1,190 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2010 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *    
+ *******************************************************************************/
+package org.jacoco.report.xml;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.jacoco.core.analysis.CounterImpl;
+import org.jacoco.core.analysis.CoverageNodeImpl;
+import org.jacoco.core.analysis.ICoverageNode.ElementType;
+import org.jacoco.core.analysis.MethodCoverage;
+import org.jacoco.report.IReportVisitor;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+/**
+ * Unit tests for {@link XMLReportNodeHandler}.
+ * 
+ * @author Marc R. Hoffmann
+ * @version $qualified.bundle.version$
+ */
+public class XMLReportNodeHandlerTest {
+
+	private XMLElement root;
+
+	private StringWriter buffer;
+
+	private XMLSupport support;
+
+	private XMLReportNodeHandler handler;
+
+	@Before
+	public void setup() throws Exception {
+		buffer = new StringWriter();
+		support = new XMLSupport(XMLReportNodeHandler.class);
+		root = new XMLDocument("report", "-//JACOCO//DTD Report 1.0//EN",
+				"report.dtd", "UTF-8", true, buffer);
+		handler = new XMLReportNodeHandler(root, new CoverageNodeImpl(
+				ElementType.GROUP, "Sample", false));
+	}
+
+	@Test
+	public void testRoot() throws Exception {
+		final Document doc = getDocument();
+		assertEquals("Sample", support.findStr(doc, "//report/@name"));
+	}
+
+	@Test
+	public void testGroup() throws Exception {
+		handler.visitChild(
+				new CoverageNodeImpl(ElementType.GROUP, "Group1", false))
+				.visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("Group1", support.findStr(doc, "//report/group/@name"));
+	}
+
+	@Test
+	public void testCounters() throws Exception {
+		final CoverageNodeImpl node = new CoverageNodeImpl(ElementType.GROUP,
+				"Group1", false) {
+			{
+				classCounter = CounterImpl.getInstance(10, 1);
+				methodCounter = CounterImpl.getInstance(20, 2);
+				blockCounter = CounterImpl.getInstance(30, 3);
+				instructionCounter = CounterImpl.getInstance(40, 4);
+				lineCounter = CounterImpl.getInstance(50, 5);
+			}
+		};
+		handler.visitChild(node).visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("1", support.findStr(doc,
+				"//report/group/counter[@type='CLASS']/@covered"));
+		assertEquals("9", support.findStr(doc,
+				"//report/group/counter[@type='CLASS']/@missed"));
+		assertEquals("2", support.findStr(doc,
+				"//report/group/counter[@type='METHOD']/@covered"));
+		assertEquals("18", support.findStr(doc,
+				"//report/group/counter[@type='METHOD']/@missed"));
+		assertEquals("3", support.findStr(doc,
+				"//report/group/counter[@type='BLOCK']/@covered"));
+		assertEquals("27", support.findStr(doc,
+				"//report/group/counter[@type='BLOCK']/@missed"));
+		assertEquals("4", support.findStr(doc,
+				"//report/group/counter[@type='INSTRUCTION']/@covered"));
+		assertEquals("36", support.findStr(doc,
+				"//report/group/counter[@type='INSTRUCTION']/@missed"));
+		assertEquals("5", support.findStr(doc,
+				"//report/group/counter[@type='LINE']/@covered"));
+		assertEquals("45", support.findStr(doc,
+				"//report/group/counter[@type='LINE']/@missed"));
+	}
+
+	@Test
+	public void testPackage() throws Exception {
+		handler.visitChild(
+				new CoverageNodeImpl(ElementType.PACKAGE, "org.jacoco.example",
+						false)).visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("org.jacoco.example",
+				support.findStr(doc, "//report/package/@name"));
+	}
+
+	@Test
+	public void testClass() throws Exception {
+		final IReportVisitor packageHandler = handler
+				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
+						"org.jacoco.example", false));
+		packageHandler.visitChild(
+				new CoverageNodeImpl(ElementType.CLASS, "Foo", true)).visitEnd(
+				null);
+		packageHandler.visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("Foo",
+				support.findStr(doc, "//report/package/class/@name"));
+	}
+
+	@Test
+	public void testMethod() throws Exception {
+		final IReportVisitor packageHandler = handler
+				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
+						"org.jacoco.example", false));
+		final IReportVisitor classHandler = packageHandler
+				.visitChild(new CoverageNodeImpl(ElementType.CLASS, "Foo", true));
+		MethodCoverage node = new MethodCoverage("doit", "()V", null);
+		node.addBlock(5, new int[] { 15, 16, 17 }, false);
+		classHandler.visitChild(node).visitEnd(null);
+		classHandler.visitEnd(null);
+		packageHandler.visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("doit",
+				support.findStr(doc, "//report/package/class/method/@name"));
+		assertEquals("()V",
+				support.findStr(doc, "//report/package/class/method/@desc"));
+		assertEquals("15",
+				support.findStr(doc, "//report/package/class/method/@line"));
+	}
+
+	@Test
+	public void testSourcefile() throws Exception {
+		final IReportVisitor packageHandler = handler
+				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
+						"org.jacoco.example", false));
+		final CoverageNodeImpl node = new CoverageNodeImpl(
+				ElementType.SOURCEFILE, "Foo.java", true) {
+			{
+				lines.increment(new int[] { 11, 13 }, false);
+				lines.increment(new int[] { 13, 14 }, true);
+			}
+		};
+		packageHandler.visitChild(node).visitEnd(null);
+		packageHandler.visitEnd(null);
+		final Document doc = getDocument();
+		assertEquals("Foo.java",
+				support.findStr(doc, "//report/package/sourcefile/@name"));
+		assertEquals("11",
+				support.findStr(doc, "//report/package/sourcefile/line[1]/@nr"));
+		assertEquals("N", support.findStr(doc,
+				"//report/package/sourcefile/line[1]/@status"));
+		assertEquals("13",
+				support.findStr(doc, "//report/package/sourcefile/line[2]/@nr"));
+		assertEquals("P", support.findStr(doc,
+				"//report/package/sourcefile/line[2]/@status"));
+		assertEquals("14",
+				support.findStr(doc, "//report/package/sourcefile/line[3]/@nr"));
+		assertEquals("F", support.findStr(doc,
+				"//report/package/sourcefile/line[3]/@status"));
+	}
+
+	private Document getDocument() throws SAXException, IOException,
+			ParserConfigurationException {
+		handler.visitEnd(null);
+		return support.parse(buffer.toString());
+	}
+
+}
diff --git a/org.jacoco.report/src/org/jacoco/report/xml/XMLReportNodeHandler.java b/org.jacoco.report/src/org/jacoco/report/xml/XMLReportNodeHandler.java
index 0fedb88..cab19e6 100644
--- a/org.jacoco.report/src/org/jacoco/report/xml/XMLReportNodeHandler.java
+++ b/org.jacoco.report/src/org/jacoco/report/xml/XMLReportNodeHandler.java
@@ -16,10 +16,10 @@
 

 import org.jacoco.core.analysis.ICounter;

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ILines;

-import org.jacoco.core.analysis.MethodCoverage;

 import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

 import org.jacoco.core.analysis.ICoverageNode.ElementType;

+import org.jacoco.core.analysis.ILines;

+import org.jacoco.core.analysis.MethodCoverage;

 import org.jacoco.report.IReportVisitor;

 import org.jacoco.report.ISourceFileLocator;

 

@@ -61,8 +61,7 @@
 	 *            this element

 	 * @throws IOException

 	 */

-	protected void insertElementsBefore(final XMLElement element)

-			throws IOException {

+	void insertElementsBefore(final XMLElement element) throws IOException {

 	}

 

 	/**

@@ -72,8 +71,7 @@
 	 *            this element

 	 * @throws IOException

 	 */

-	protected void insertElementsAfter(final XMLElement element)

-			throws IOException {

+	void insertElementsAfter(final XMLElement element) throws IOException {

 	}

 

 	public IReportVisitor visitChild(final ICoverageNode node)

@@ -89,7 +87,12 @@
 			return new XMLReportNodeHandler(element.element("class"), node);

 		case METHOD:

 			final XMLElement methodChild = element.element("method");

-			methodChild.attr("desc", ((MethodCoverage) node).getDesc());

+			final MethodCoverage methodNode = (MethodCoverage) node;

+			methodChild.attr("desc", methodNode.getDesc());

+			final int line = methodNode.getLines().getFirstLine();

+			if (line != -1) {

+				methodChild.attr("line", line);

+			}

 			return new XMLReportNodeHandler(methodChild, node);

 		case SOURCEFILE:

 			return new XMLReportNodeHandler(element.element("sourcefile"), node) {

@@ -99,16 +102,17 @@
 					writeLines(node.getLines(), element);

 				}

 			};

+		default:

+			throw new AssertionError(type);

 		}

-		return IReportVisitor.NOP;

 	}

 

 	public final void visitEnd(final ISourceFileLocator sourceFileLocator)

 			throws IOException {

+		insertElementsAfter(element);

 		for (final CounterEntity counterEntity : CounterEntity.values()) {

 			createCounterElement(counterEntity);

 		}

-		insertElementsAfter(element);

 		element.close();

 	}

 

diff --git a/org.jacoco.report/src/org/jacoco/report/xml/report.dtd b/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
index 0196301..cb72c70 100644
--- a/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
+++ b/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
@@ -44,9 +44,10 @@
 <!ELEMENT method (counter*)>

 <!ATTLIST method

   name CDATA #REQUIRED

-  desc CDATA #REQUIRED>

+  desc CDATA #REQUIRED

+  line CDATA #IMPLIED>

   

-<!ELEMENT sourcefile (line*)>

+<!ELEMENT sourcefile (line*, counter*)>

 <!ATTLIST sourcefile

   name CDATA #REQUIRED>

 

@@ -57,6 +58,6 @@
 

 <!ELEMENT counter EMPTY>

 <!ATTLIST counter

-  type    CDATA #REQUIRED

+  type    (INSTRUCTION|BLOCK|LINE|METHOD|CLASS) #REQUIRED

   covered CDATA #REQUIRED

   missed  CDATA #REQUIRED>
\ No newline at end of file