#!/usr/bin/env python3
# Copyright(c) 2007,2013 Progress Software Corporation (PSC). All rights
# Copyright (c) 2013, 2015-2016, 2018-2021, 2023 Software AG, Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA, and/or its subsidiaries and/or its affiliates and/or their licensors.
# Use, reproduction, transfer, publication or disclosure is prohibited except as specifically provided for in your License Agreement with Software AG
""" Contains support for EPL code coverage reporting.
"""
import sys, os, logging
import io
import pysys
from pysys.constants import *
from pysys.writer.testoutput import CollectTestOutputWriter
from pysys.utils.fileutils import deletedir, mkdir, pathexists
log = logging.getLogger('pysys.apama.coverage')
[docs]class EPLCoverageWriter(CollectTestOutputWriter):
"""Writer that collects Apama EPL code coverage files during test execution, and writes an HTML report showing
line coverage for each ``.mon`` file after testing has completed.
To configure this writer, add it to your ``pysysproject.xml`` and set the required ``destDir`` property
(a typical value would be ``__coverage_epl.${outDirName}``), and optionally other configuration properties.
To enable it at runtime, run pysys with ``-XcodeCoverage`` (or alternatively, ``-XeplCoverage``).
If coverage is generated, the directory containing all coverage files is published as an artifact named
"EPLCoverageDir". Optionally an archive of this directory can be generated by setting the
``destArchive`` property (see `pysys.writer.testoutput.CollectTestOutputWriter`), and published as
"EPLCoverageArchive".
.. versionadded:: 10.7.0
The following property can be set in the project configuration for this writer (and see also
`pysys.writer.testoutput.CollectTestOutputWriter` for inherited properties such as ``destDir`` (required) and ``destArchive``):
"""
srcIncludes = []
"""
List of EPL source file patterns to include. Default is ``**``.
"""
srcExcludes = []
"""
List of EPL source file patterns to exclude, e.g. ``**/foo/Bar*.mon, **/baz/*.mon``.
In addition to the specified exclusions, files under APAMA_HOME, the test root dir, any test input dirs (``Input/*.mon``) and
any temporary (``*.tmp.mon``) files are excluded.
"""
srcSearchDirs = []
"""
List of directories that will be searched to locate source files that were injected without an absolute path.
"""
args = []
"""
Additional command line arguments to pass to ``epl_coverage``.
"""
title = ''
"""
Title for the HTML coverage report.
"""
# override CollectTestOutputWriter property values
fileIncludesRegex = u'.*[.]eplcoverage$'
outputPattern = u'@TESTID@_@FILENAME@.@UNIQUE@.eplcoverage'
publishArtifactDirCategory = u'EPLCoverageDir'
publishArtifactArchiveCategory = u'EPLCoverageArchive'
@staticmethod
def _isEPLCoverageEnabled(runner):
return (
runner.getBoolProperty('eplCoverage', default=runner.getBoolProperty('codeCoverage'))
or runner.getBoolProperty('eplcoverage')) # old pre-10.7 name
def isEnabled(self, record=False, **kwargs):
"""
:meta private: Does not need to be displayed in the API docs.
"""
return self.destDir and self._isEPLCoverageEnabled(self.runner)
def cleanup(self, **kwargs):
"""
:meta private: Does not need to be displayed in the API docs.
"""
if not pathexists(self.destDir):
log.info('No tests generated any EPL code coverage files (hint: check your correlators were shutdown cleanly).')
return
log.info('Preparing Apama EPL coverage report in: %s', os.path.normpath(self.destDir))
covfiles = os.listdir(self.destDir)
with io.open(self.destDir+'/coverage_files.txt', 'w', encoding='utf-8') as f:
# Writes Generated EPL Coverage file names to coverage_file.txt - always utf-8
for cov in covfiles:
f.write(cov)
f.write(u'\n')
del cov
arguments=[
'--output', self.destDir,
]
defaultExcludes = [
'**/Input/**.mon', # test files aren't interesting (though note that modern PySys tests may not have an input dir)
'**/*.tmp.mon', # temporary files aren't interesting
# exclude all files from Apama monitor dir.
self.runner.project.APAMA_HOME+'/monitors/**',
# not useful
'**/*.qry.mon',
]
# it's common to have a test root dir under the source dir in which case excluding it is important (+for compat since prior to 10.15.2 we always did that)
# but sometimes people have srcdir=testroot (or less commonly a dynamically generated srcdir is located under testroot) and in those cases the exclude would break everything
if not any(
os.path.normcase(os.path.normpath(srcdir)).startswith( os.path.normcase(os.path.normpath(self.runner.project.testRootDir))
) for srcdir in self.srcSearchDirs):
defaultExcludes.append(self.runner.project.testRootDir+'/**')
for s in self.srcIncludes:
arguments.extend(['--include', s])
for s in self.srcExcludes:
arguments.extend(['--exclude', s])
# putting the defaults after the user-specified ones makes most sense for coverage tool debug log messages
for e in defaultExcludes:
# since excluding+including the same string no sense, this condition gives a reasonable (though low profile/undoc'd) way for user to override our defaults
if e not in self.srcIncludes:
arguments.extend(['--exclude', e])
for s in self.srcSearchDirs:
arguments.extend(['--source', s])
arguments.extend(self.args)
if self.title:
arguments.extend(['--title', self.title])
arguments.append('@%s/coverage_files.txt'%self.destDir)
proc = self.runner.startProcess(self.runner.project.APAMA_HOME+'/bin/epl_coverage',
arguments=arguments,
state=FOREGROUND,
environs=dict(os.environ), # need this so we have APAMA_JRE/JAVA_HOME etc available (on Windows, or from Unix core edition)
displayName='epl_coverage (with %d coverage files)'%(len(covfiles)),
stdouterr=self.destDir+'/epl_coverage', # both the out and err files may contain useful logging
workingDir=self.destDir,
abortOnError=True,
)
self.runner.logFileContents(proc.stderr, mappers=[lambda line: None if 'JAVA_OPTIONS' in line else line]) # in case there are any (non-fatal) errors, print them
# if it succeeded the individual cov files can be deleted, but in case we excluded everything, leave them around
if os.path.getsize(self.destDir+os.sep+'merged.eplcoverage') > 12:
for p in covfiles: os.remove(os.path.join(self.destDir, p))
self.archiveAndPublish()