From d0ec3a01a61f259768d31fc20791780807e1788e Mon Sep 17 00:00:00 2001
From: Patrick Reynolds <patrick.reynolds@kitware.com>
Date: Fri, 27 Sep 2013 10:42:39 -0400
Subject: Adding support for the Python coverage.py tool.

This assumes that coverage.py has been run in such a way to produce its
standard XML output. This uses the Cobertura schema and should be somewhat
generalizable.
---
 Source/CMakeLists.txt                         |   1 +
 Source/CTest/cmCTestCoverageHandler.cxx       |  34 ++++++++
 Source/CTest/cmCTestCoverageHandler.h         |   4 +
 Source/CTest/cmParsePythonCoverage.cxx        | 113 ++++++++++++++++++++++++++
 Source/CTest/cmParsePythonCoverage.h          |  48 +++++++++++
 Tests/CMakeLists.txt                          |  19 +++++
 Tests/PythonCoverage/DartConfiguration.tcl.in |   8 ++
 Tests/PythonCoverage/coverage.xml.in          |  35 ++++++++
 Tests/PythonCoverage/coveragetest/foo.py      |   8 ++
 Tests/PythonCoverage/coveragetest/test_foo.py |  11 +++
 10 files changed, 281 insertions(+)
 create mode 100644 Source/CTest/cmParsePythonCoverage.cxx
 create mode 100644 Source/CTest/cmParsePythonCoverage.h
 create mode 100644 Tests/PythonCoverage/DartConfiguration.tcl.in
 create mode 100644 Tests/PythonCoverage/coverage.xml.in
 create mode 100644 Tests/PythonCoverage/coveragetest/foo.py
 create mode 100644 Tests/PythonCoverage/coveragetest/test_foo.py

diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 8412e3e..50e9d2b 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -439,6 +439,7 @@ set(CTEST_SRCS cmCTest.cxx
   CTest/cmParseCacheCoverage.cxx
   CTest/cmParseGTMCoverage.cxx
   CTest/cmParsePHPCoverage.cxx
+  CTest/cmParsePythonCoverage.cxx
   CTest/cmCTestEmptyBinaryDirectoryCommand.cxx
   CTest/cmCTestGenericHandler.cxx
   CTest/cmCTestHandlerCommand.cxx
diff --git a/Source/CTest/cmCTestCoverageHandler.cxx b/Source/CTest/cmCTestCoverageHandler.cxx
index 20aded2..ef071b9 100644
--- a/Source/CTest/cmCTestCoverageHandler.cxx
+++ b/Source/CTest/cmCTestCoverageHandler.cxx
@@ -11,6 +11,7 @@
 ============================================================================*/
 #include "cmCTestCoverageHandler.h"
 #include "cmParsePHPCoverage.h"
+#include "cmParsePythonCoverage.h"
 #include "cmParseGTMCoverage.h"
 #include "cmParseCacheCoverage.h"
 #include "cmCTest.h"
@@ -392,6 +393,13 @@ int cmCTestCoverageHandler::ProcessHandler()
     {
     return error;
     }
+  file_count += this->HandlePythonCoverage(&cont);
+  error = cont.Error;
+  if ( file_count < 0 )
+    {
+    return error;
+    }
+
   file_count += this->HandleMumpsCoverage(&cont);
   error = cont.Error;
   if ( file_count < 0 )
@@ -761,6 +769,32 @@ int cmCTestCoverageHandler::HandlePHPCoverage(
     }
   return static_cast<int>(cont->TotalCoverage.size());
 }
+
+//----------------------------------------------------------------------
+int cmCTestCoverageHandler::HandlePythonCoverage(
+  cmCTestCoverageHandlerContainer* cont)
+{
+  cmParsePythonCoverage cov(*cont, this->CTest);
+
+  // Assume the coverage.xml is in the source directory
+  std::string coverageXMLFile = this->CTest->GetBinaryDir() + "/coverage.xml";
+
+  if(cmSystemTools::FileExists(coverageXMLFile.c_str()))
+    {
+    cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
+               "Parsing coverage.py XML file: " << coverageXMLFile
+               << std::endl);
+    cov.ReadCoverageXML(coverageXMLFile.c_str());
+    }
+  else
+    {
+    cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
+               "Cannot find coverage.py XML file: " << coverageXMLFile
+               << std::endl);
+    }
+  return static_cast<int>(cont->TotalCoverage.size());
+}
+
 //----------------------------------------------------------------------
 int cmCTestCoverageHandler::HandleMumpsCoverage(
   cmCTestCoverageHandlerContainer* cont)
diff --git a/Source/CTest/cmCTestCoverageHandler.h b/Source/CTest/cmCTestCoverageHandler.h
index 92b0b22..3506928 100644
--- a/Source/CTest/cmCTestCoverageHandler.h
+++ b/Source/CTest/cmCTestCoverageHandler.h
@@ -70,6 +70,10 @@ private:
 
   //! Handle coverage using xdebug php coverage
   int HandlePHPCoverage(cmCTestCoverageHandlerContainer* cont);
+
+  //! Handle coverage for Python with coverage.py
+  int HandlePythonCoverage(cmCTestCoverageHandlerContainer* cont);
+
   //! Handle coverage for mumps
   int HandleMumpsCoverage(cmCTestCoverageHandlerContainer* cont);
 
diff --git a/Source/CTest/cmParsePythonCoverage.cxx b/Source/CTest/cmParsePythonCoverage.cxx
new file mode 100644
index 0000000..a086f13
--- /dev/null
+++ b/Source/CTest/cmParsePythonCoverage.cxx
@@ -0,0 +1,113 @@
+#include "cmStandardIncludes.h"
+#include "cmSystemTools.h"
+#include "cmXMLParser.h"
+#include "cmParsePythonCoverage.h"
+#include <cmsys/Directory.hxx>
+
+
+//----------------------------------------------------------------------------
+class cmParsePythonCoverage::XMLParser: public cmXMLParser
+{
+public:
+  XMLParser(cmCTest* ctest, cmCTestCoverageHandlerContainer& cont)
+    : CTest(ctest), Coverage(cont)
+  {
+  }
+
+  virtual ~XMLParser()
+  {
+  }
+
+protected:
+
+  virtual void StartElement(const char* name, const char** atts)
+  {
+    if(strcmp(name, "class") == 0)
+    {
+      int tagCount = 0;
+      while(true)
+      {
+        if(strcmp(atts[tagCount], "filename") == 0)
+        {
+          cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Reading file: "
+                     << atts[tagCount+1] << std::endl);
+          this->CurFileName = this->Coverage.SourceDir + "/" +
+                                 atts[tagCount+1];
+          FileLinesType& curFileLines =
+            this->Coverage.TotalCoverage[this->CurFileName];
+          std::ifstream fin(this->CurFileName.c_str());
+          if(!fin)
+          {
+            cmCTestLog(this->CTest, ERROR_MESSAGE,
+                       "Python Coverage: Error opening " << this->CurFileName
+                       << std::endl);
+            this->Coverage.Error++;
+            break;
+          }
+
+          std::string line;
+          curFileLines.push_back(-1);
+          while(cmSystemTools::GetLineFromStream(fin, line))
+          {
+            curFileLines.push_back(-1);
+          }
+
+          break;
+        }
+        ++tagCount;
+      }
+    }
+    else if(strcmp(name, "line") == 0)
+    {
+      int tagCount = 0;
+      int curNumber = -1;
+      int curHits = -1;
+      while(true)
+      {
+        if(strcmp(atts[tagCount], "hits") == 0)
+        {
+          curHits = atoi(atts[tagCount+1]);
+        }
+        else if(strcmp(atts[tagCount], "number") == 0)
+        {
+          curNumber = atoi(atts[tagCount+1]);
+        }
+
+        if(curHits > -1 && curNumber > -1)
+        {
+          FileLinesType& curFileLines =
+            this->Coverage.TotalCoverage[this->CurFileName];
+          curFileLines[curNumber] = curHits;
+          break;
+        }
+        ++tagCount;
+      }
+    }
+  }
+
+  virtual void EndElement(const char*) {}
+
+private:
+
+  typedef cmCTestCoverageHandlerContainer::SingleFileCoverageVector
+     FileLinesType;
+  cmCTest* CTest;
+  cmCTestCoverageHandlerContainer& Coverage;
+  std::string CurFileName;
+
+};
+
+
+cmParsePythonCoverage::cmParsePythonCoverage(
+    cmCTestCoverageHandlerContainer& cont,
+    cmCTest* ctest)
+    :Coverage(cont), CTest(ctest)
+{
+}
+
+bool cmParsePythonCoverage::ReadCoverageXML(const char* xmlFile)
+{
+  cmParsePythonCoverage::XMLParser parser(this->CTest, this->Coverage);
+  parser.ParseFile(xmlFile);
+  return true;
+}
diff --git a/Source/CTest/cmParsePythonCoverage.h b/Source/CTest/cmParsePythonCoverage.h
new file mode 100644
index 0000000..668c7f9
--- /dev/null
+++ b/Source/CTest/cmParsePythonCoverage.h
@@ -0,0 +1,48 @@
+/*============================================================================
+  CMake - Cross Platform Makefile Generator
+  Copyright 2000-2009 Kitware, Inc.
+
+  Distributed under the OSI-approved BSD License (the "License");
+  see accompanying file Copyright.txt for details.
+
+  This software is distributed WITHOUT ANY WARRANTY; without even the
+  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the License for more information.
+============================================================================*/
+
+#ifndef cmParsePythonCoverage_h
+#define cmParsePythonCoverage_h
+
+#include "cmStandardIncludes.h"
+#include "cmCTestCoverageHandler.h"
+
+/** \class cmParsePythonCoverage
+ * \brief Parse coverage.py Python coverage information
+ *
+ * This class is used to parse the output of the coverage.py tool that
+ * is currently maintained by Ned Batchelder. That tool has a command
+ * that produces xml output in the format typically output by the common
+ * Java-based Cobertura coverage application. This helper class parses
+ * that XML file to fill the coverage-handler container.
+ */
+class cmParsePythonCoverage
+{
+public:
+
+  //! Create the coverage parser by passing in the coverage handler
+  //! container and the cmCTest object
+  cmParsePythonCoverage(cmCTestCoverageHandlerContainer& cont,
+    cmCTest* ctest);
+
+  //! Read the XML produced by running `coverage xml`
+  bool ReadCoverageXML(const char* xmlFile);
+
+private:
+
+  class XMLParser;
+  cmCTestCoverageHandlerContainer& Coverage;
+  cmCTest* CTest;
+  std::string CurFileName;
+};
+
+#endif
diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index 9c3ed59..030137c 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -1959,6 +1959,25 @@ ${CMake_BINARY_DIR}/bin/cmake -DVERSION=master -P ${CMake_SOURCE_DIR}/Utilities/
       PASS_REGULAR_EXPRESSION
       "Process file.*XINDEX.m.*Total LOC:.*125.*Percentage Coverage: 85.60.*"
       ENVIRONMENT COVFILE=)
+
+  # Adding a test case for Python Coverage
+  configure_file(
+     "${CMake_SOURCE_DIR}/Tests/PythonCoverage/coverage.xml.in"
+     "${CMake_BINARY_DIR}/Testing/PythonCoverage/coverage.xml")
+  configure_file(
+     "${CMake_SOURCE_DIR}/Tests/PythonCoverage/DartConfiguration.tcl.in"
+     "${CMake_BINARY_DIR}/Testing/PythonCoverage/DartConfiguration.tcl")
+  file(COPY "${CMake_SOURCE_DIR}/Tests/PythonCoverage/coveragetest"
+    DESTINATION "${CMake_BINARY_DIR}/Testing/PythonCoverage")
+  add_test(NAME CTestPythonCoverage
+    COMMAND cmake -E chdir
+    ${CMake_BINARY_DIR}/Testing/PythonCoverage
+    $<TARGET_FILE:ctest> -T Coverage --debug)
+  set_tests_properties(CTestPythonCoverage PROPERTIES
+      PASS_REGULAR_EXPRESSION
+      "Process file.*foo.py.*Total LOC:.*13.*Percentage Coverage: 84.62.*"
+      ENVIRONMENT COVFILE=)
+
   # Use macro, not function so that build can still be driven by CMake 2.4.
   # After 2.6 is required, this could be a function without the extra 'set'
   # calls.
diff --git a/Tests/PythonCoverage/DartConfiguration.tcl.in b/Tests/PythonCoverage/DartConfiguration.tcl.in
new file mode 100644
index 0000000..e29cffe
--- /dev/null
+++ b/Tests/PythonCoverage/DartConfiguration.tcl.in
@@ -0,0 +1,8 @@
+# This file is configured by CMake automatically as DartConfiguration.tcl
+# If you choose not to use CMake, this file may be hand configured, by
+# filling in the required variables.
+
+
+# Configuration directories and files
+SourceDirectory: ${CMake_BINARY_DIR}/Testing/PythonCoverage/coveragetest
+BuildDirectory: ${CMake_BINARY_DIR}/Testing/PythonCoverage
diff --git a/Tests/PythonCoverage/coverage.xml.in b/Tests/PythonCoverage/coverage.xml.in
new file mode 100644
index 0000000..fcc1b1c
--- /dev/null
+++ b/Tests/PythonCoverage/coverage.xml.in
@@ -0,0 +1,35 @@
+<?xml version="1.0" ?>
+<!DOCTYPE coverage
+  SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
+<coverage branch-rate="0" line-rate="0.8462" timestamp="1380469411433" version="3.6">
+	<!-- Generated by coverage.py: http://nedbatchelder.com/code/coverage -->
+	<packages>
+		<package branch-rate="0" complexity="0" line-rate="0.8462" name="">
+			<classes>
+				<class branch-rate="0" complexity="0" filename="foo.py" line-rate="0.6667" name="foo">
+					<methods/>
+					<lines>
+						<line hits="1" number="2"/>
+						<line hits="1" number="3"/>
+						<line hits="1" number="4"/>
+						<line hits="1" number="6"/>
+						<line hits="0" number="7"/>
+						<line hits="0" number="8"/>
+					</lines>
+				</class>
+				<class branch-rate="0" complexity="0" filename="test_foo.py" line-rate="1" name="test_foo">
+					<methods/>
+					<lines>
+						<line hits="1" number="2"/>
+						<line hits="1" number="3"/>
+						<line hits="1" number="5"/>
+						<line hits="1" number="7"/>
+						<line hits="1" number="8"/>
+						<line hits="1" number="10"/>
+						<line hits="1" number="11"/>
+					</lines>
+				</class>
+			</classes>
+		</package>
+	</packages>
+</coverage>
diff --git a/Tests/PythonCoverage/coveragetest/foo.py b/Tests/PythonCoverage/coveragetest/foo.py
new file mode 100644
index 0000000..97b5a41
--- /dev/null
+++ b/Tests/PythonCoverage/coveragetest/foo.py
@@ -0,0 +1,8 @@
+
+def foo():
+    x = 3 + 3
+    return x
+
+def bar():
+    y = 2 + 2
+    return y
diff --git a/Tests/PythonCoverage/coveragetest/test_foo.py b/Tests/PythonCoverage/coveragetest/test_foo.py
new file mode 100644
index 0000000..51a69d8
--- /dev/null
+++ b/Tests/PythonCoverage/coveragetest/test_foo.py
@@ -0,0 +1,11 @@
+
+import foo
+import unittest
+
+class TestFoo(unittest.TestCase):
+
+    def testFoo(self):
+        self.assertEquals(foo.foo(), 6, 'foo() == 6')
+
+if __name__ == '__main__':
+    unittest.main()
-- 
cgit v0.12