# GNAThub (GNATdashboard)
# Copyright (C) 2013-2020, AdaCore
#
# This is free software; you can redistribute it and/or modify it under
# terms of the GNU General Public License as published by the Free Soft-
# ware Foundation; either version 3, or (at your option) any later ver-
# sion. This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN-
# TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details. You should have received a copy of the GNU
# General Public License distributed with this software; see file
# COPYING3. If not, go to http://www.gnu.org/licenses for a complete copy
# of the license.
"""GNAThub plug-in for the GNATmetric and LALmetric command-line tool.
It exports the GNATmetric class which implements the :class:`GNAThub.Plugin`
interface. This allows GNAThub's plug-in scanner to automatically find this
module and load it as part of the GNAThub default execution.
"""
import os
import GNAThub
from GNAThub import Console, Plugin, Reporter, Runner
import shutil
from xml.etree import ElementTree
from xml.etree.ElementTree import ParseError
[docs]class GNATmetric(Plugin, Runner, Reporter):
"""GNATmetric & LALmetric plugin for GNAThub."""
# GNATmetric exits with an error code of 1 even on a successful run
VALID_EXIT_CODES = (0, 1)
RANKING = GNAThub.RANKING_INFO
[docs] def __init__(self):
super(GNATmetric, self).__init__()
if GNAThub.dry_run_without_project():
return
self.tool = None
self.output = os.path.join(
GNAThub.Project.artifacts_dir(), 'metrix.xml')
self.rules = {}
self.display_names = {}
self.messages = {}
self.firstunit = False
def __cmd_exists(self, cmd):
return shutil.which(cmd) is not None
@property
def name(self):
return 'lalmetric' if self._use_libadalang_tools else 'gnatmetric'
@property
def _use_libadalang_tools(self):
"""Whether to use GNATmetric or LALmetric.
:return: `True` if we should use LALmetric, `False` for GNATmetric
:rtype: boolean
"""
return 'USE_LIBADALANG_TOOLS' in os.environ
def __cmd_line(self):
"""Create GNATmetric command line arguments list.
:return: the GNATmetric command line
:rtype: collections.Iterable[str]
"""
cmd_line = [
self.name, '-ox', self.output, '-P', GNAThub.Project.path()]
if GNAThub.u_process_all():
cmd_line.extend(['-U'])
# Keeping this for later implementation of -U main switch
# if GNAThub.u_main():
# cmd_line.extend(['-U'])
# cmd_line.extend([GNAThub.u_main()])
cmd_line = cmd_line + GNAThub.Project.scenario_switches()
if GNAThub.Project.target():
cmd = '{}-{}'.format(GNAThub.Project.target(), cmd_line[0])
if self.__cmd_exists(cmd):
cmd_line[0] = cmd
else:
cmd_line.extend(['--target', GNAThub.Project.target()])
if GNAThub.Project.runtime():
cmd_line.extend(('--RTS', GNAThub.Project.runtime()))
if GNAThub.subdirs():
cmd_line.extend(['--subdirs=' + GNAThub.subdirs()])
return cmd_line
[docs] def run(self):
"""Execute GNATmetric.
Returns according to the success of the execution of the tool:
* ``GNAThub.EXEC_SUCCESS``: on successful execution
* ``GNAThub.EXEC_FAILURE``: on any error
"""
status = GNAThub.Run(self.name, self.__cmd_line()).status
return GNAThub.EXEC_SUCCESS if status == 0 else GNAThub.EXEC_FAILURE
[docs] def parse_metrics(self, node, entity=False):
"""Parse the xml *node* returns a list of metrics"""
message_data = []
for metric in node.findall('./metric'):
name = metric.attrib.get('name')
if name in self.rules:
rule = self.rules[name]
else:
rule = GNAThub.Rule(
self.display_names[name], name,
GNAThub.METRIC_KIND, self.tool)
self.rules[name] = rule
if (rule, metric.text, GNATmetric.RANKING) in self.messages:
msg = self.messages[(rule, metric.text, GNATmetric.RANKING)]
else:
msg = GNAThub.Message(rule, metric.text, GNATmetric.RANKING)
self.messages[(rule, metric.text, GNATmetric.RANKING)] = msg
message_data.append([msg, 0, 1, 1])
return message_data
[docs] def parse_units(self, node, resource):
"""Recursively parse the unit node until all of them are found"""
# Map of entities for a ressource
entities_messages = []
if not node.findall('./unit'):
return []
for unit in node.findall('./unit'):
ename = unit.attrib.get('name')
ekind = unit.attrib.get('kind')
if self.firstunit:
ekind = "compilation unit"
self.firstunit = False
else:
if ekind.startswith('procedure'):
ekind = ekind.replace("procedure", "action")
elif ekind.startswith('function'):
ekind = ekind.replace("function", "action")
eline = unit.attrib.get('line')
ecol = unit.attrib.get('col')
# A resource can have multiple entities with the same name
entity = GNAThub.Entity(ename, ekind, int(eline),
int(ecol), int(ecol),
resource)
entities_messages.append([entity, self.parse_metrics(unit, True)])
entities_messages += self.parse_units(unit, resource)
return entities_messages
[docs] def parse_config(self, tree):
"""
Parse the config block to retrieve the names to be displayed for
each rule.
"""
config_node = tree.find('./config')
if config_node is None:
return
self.info('retrieving metrics configuration')
for metric in config_node.findall('./metric'):
name = metric.attrib.get('name')
display_name = metric.attrib.get('display_name')
if not display_name:
display_name = name
if name not in self.display_names:
self.display_names[name] = display_name
[docs] def report(self):
"""Parse GNATmetric XML report and save data to the database.
Returns according to the success of the analysis:
* ``GNAThub.EXEC_SUCCESS``: transactions committed to database
* ``GNAThub.EXEC_FAILURE``: error while parsing the xml report
"""
# Clear existing references only if not incremental run
if not GNAThub.incremental():
self.info('clear existing results if any')
GNAThub.Tool.clear_references(self.name)
if not os.path.isfile(self.output):
self.error('no XML report found')
return GNAThub.EXEC_FAILURE
self.info('analyse report')
self.tool = GNAThub.Tool(self.name)
self.log.debug('parse XML report: %s', self.output)
try:
tree = ElementTree.parse(self.output)
# Parse the config first to create the GNAThub rules
self.parse_config(tree)
coupling = tree.find('./coupling')
if coupling:
list_coupling = coupling.findall('./file')
else:
list_coupling = []
# Fetch all files
files = tree.findall('./file') + list_coupling
total = len(files)
# List of resource messages suitable for tool level bulk insertion
resources_messages = []
for index, node in enumerate(files, start=1):
resource = GNAThub.Resource.get(node.attrib.get('name'))
# Save file level metrics
if not resource:
self.warn('skip "%s" message (file not found)' %
node.attrib.get('name'))
continue
self.firstunit = True
resources_messages.append([resource, self.parse_metrics(node)])
self.tool.add_messages([], self.parse_units(node, resource))
Console.progress(index, total, new_line=(index == total))
# Retrieve the project metrics
resource = GNAThub.Resource(GNAThub.Project.name(),
GNAThub.PROJECT_KIND)
resources_messages.append([resource, self.parse_metrics(tree)])
self.tool.add_messages(resources_messages, [])
except ParseError as why:
self.log.exception('failed to parse XML report')
self.error('%s (%s:%s)' % (why, why.filename, why.lineno))
return GNAThub.EXEC_FAILURE
else:
return GNAThub.EXEC_SUCCESS