Skip to content

Commit

Permalink
Add unit test for API documentation coverage
Browse files Browse the repository at this point in the history
This test checks that the coverage of the API docs does not drop
below a preset threshold (initially set at the current doc coverage)

If new members are added without documentation, then the coverage
will drop and this test will fail. Hopefully over time we can slowly
increase this threshold until documentation coverage reaches an
acceptable level.
  • Loading branch information
nyalldawson committed Feb 11, 2015
1 parent 1f5b6fd commit 47488f2
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -23,7 +23,7 @@ install:
- mkdir build
- cd build
- cmake -DWITH_SERVER=ON -DWITH_STAGED_PLUGINS=OFF -DWITH_GRASS=OFF \
-DSUPPRESS_QT_WARNINGS=ON -DENABLE_MODELTEST=ON -DWITH_QWTPOLAR=OFF ..
-DSUPPRESS_QT_WARNINGS=ON -DENABLE_MODELTEST=ON -DWITH_QWTPOLAR=OFF -DWITH_APIDOC=ON ..

script: xvfb-run ctest -V -E 'Atlas|atlas|PyQgsPalLabelingCanvas|PyQgsPalLabelingServer|qgis_wcsprovidertest' -S ../qgis-test-travis.ctest --output-on-failure

2 changes: 1 addition & 1 deletion cmake_templates/Doxyfile.in
Expand Up @@ -1248,7 +1248,7 @@ MAN_LINKS = NO
# generate an XML file that captures the structure of
# the code including all documentation.

GENERATE_XML = NO
GENERATE_XML = YES

# The XML_OUTPUT tag is used to specify where the XML pages will be put.
# If a relative path is entered the value of OUTPUT_DIRECTORY will be
Expand Down
3 changes: 3 additions & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -41,3 +41,6 @@ ADD_PYTHON_TEST(PyQgsZonalStatistics test_qgszonalstatistics.py)
ADD_PYTHON_TEST(PyQgsAppStartup test_qgsappstartup.py)
ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py)
ADD_PYTHON_TEST(PyQgsGraduatedSymbolRendererV2 test_qgsgraduatedsymbolrendererv2.py)
IF (WITH_APIDOC)
ADD_PYTHON_TEST(PyQgsDocCoverage test_qgsdoccoverage.py)
ENDIF (WITH_APIDOC)
123 changes: 123 additions & 0 deletions tests/src/python/test_qgsdoccoverage.py
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for API documentation coverage.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '01/02/2015'
__copyright__ = 'Copyright 2015, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import os
import glob

from utilities import (TestCase,
unittest)

try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET

from PyQt4.QtCore import qDebug

# DOCUMENTATION THRESHOLD
#
# The minimum coverage of public/protected member functions in QGIS api
#
# DON'T LOWER THIS THRESHOLD UNLESS MEMBERS HAVE BEEN REMOVED FROM THE API
# (changes which raise this threshold are welcomed though!)

ACCEPTABLE_COVERAGE = 52.673


def elemIsDocumentableClass(elem):
if not elem.get('kind') == 'class':
return False

#public or protected classes should be documented
return elem.get('prot') in ('public','protected')

def elemIsDocumentableMember(elem):
if elem.get('kind') == 'variable':
return False

#only public or protected members should be documented
if not elem.get('prot') in ('public','protected'):
return False

#ignore reimplemented methods
#use two different tests, as doxygen will not detect reimplemented qt methods
if elem.find('reimplements') is not None:
return False
args = elem.find('argsstring')
if args is not None and args.text and ' override' in args.text:
return False

#ignore destructor
name = elem.find('name')
if name is not None and name.text and name.text.startswith('~'):
return False

return True


def memberIsDocumented(m):
for doc_type in ('inbodydescription','briefdescription','detaileddescription'):
doc = m.find(doc_type)
if doc is not None and list(doc):
return True
return False

def parseClassElem(e):
documentable_members = 0
documented_members = 0
for m in e.getiterator('memberdef'):
if elemIsDocumentableMember(m):
documentable_members += 1
if memberIsDocumented(m):
documented_members += 1
return documentable_members, documented_members

def parseFile(f):
documentable_members = 0
documented_members = 0
for event, elem in ET.iterparse(f):
if event == 'end' and elem.tag == 'compounddef':
if elemIsDocumentableClass(elem):
members, documented = parseClassElem(elem)
documentable_members += members
documented_members += documented
elem.clear()
return documentable_members, documented_members


def parseDocs(path):
documentable_members = 0
documented_members = 0
for f in glob.glob(os.path.join(path,'*.xml')):
members, documented = parseFile( f )
documentable_members += members
documented_members += documented

return 100.0 * documented_members / documentable_members

class TestQgsDocCoverage(TestCase):

def testCoverage(self):
prefixPath = os.environ['QGIS_PREFIX_PATH']
docPath = os.path.join(prefixPath, '..', 'doc', 'api', 'xml' )

coverage = parseDocs(docPath)
print "Documentation coverage {}".format(coverage)

assert coverage >= ACCEPTABLE_COVERAGE, 'Minimum coverage: %f\nActual coverage: %f\n' % (ACCEPTABLE_COVERAGE, coverage)


if __name__ == '__main__':
unittest.main()

0 comments on commit 47488f2

Please sign in to comment.