|
@@ -0,0 +1,892 @@
|
|
|
|
+<!DOCTYPE html>
|
|
|
|
+<!--
|
|
|
|
+
|
|
|
|
+ Metrix++, Copyright 2009-2013, Metrix++ Project
|
|
|
|
+ Link: http://metrixplusplus.sourceforge.net
|
|
|
|
+
|
|
|
|
+ This file is part of Metrix++ Tool.
|
|
|
|
+
|
|
|
|
+ Metrix++ 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, version 3 of the License.
|
|
|
|
+
|
|
|
|
+ Metrix++ is distributed in the hope that it will be useful,
|
|
|
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
+ MERCHANTABILITY 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
|
|
|
|
+ along with Metrix++. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+
|
|
|
|
+-->
|
|
|
|
+<html lang="en">
|
|
|
|
+ <head>
|
|
|
|
+ <meta charset="utf-8">
|
|
|
|
+ <title>Metrix++ Project</title>
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
+ <meta name="description" content="">
|
|
|
|
+ <meta name="author" content="">
|
|
|
|
+
|
|
|
|
+ <!-- Le styles -->
|
|
|
|
+ <!--
|
|
|
|
+ <link href="../../style.css" rel="stylesheet">
|
|
|
|
+ -->
|
|
|
|
+ <link href="assets/css/bootstrap.css" rel="stylesheet">
|
|
|
|
+ <link href="assets/css/bootstrap-responsive.css" rel="stylesheet">
|
|
|
|
+ <link href="assets/css/docs.css" rel="stylesheet">
|
|
|
|
+ <link href="assets/js/google-code-prettify/prettify.css" rel="stylesheet">
|
|
|
|
+
|
|
|
|
+ <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
|
|
|
+ <!--[if lt IE 9]>
|
|
|
|
+ <script src="assets/js/html5shiv.js"></script>
|
|
|
|
+ <![endif]-->
|
|
|
|
+
|
|
|
|
+ <!-- Le fav and touch icons -->
|
|
|
|
+ <link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/ico/apple-touch-icon-144-precomposed.png">
|
|
|
|
+ <link rel="apple-touch-icon-precomposed" sizes="114x114" href="assets/ico/apple-touch-icon-114-precomposed.png">
|
|
|
|
+ <link rel="apple-touch-icon-precomposed" sizes="72x72" href="assets/ico/apple-touch-icon-72-precomposed.png">
|
|
|
|
+ <link rel="apple-touch-icon-precomposed" href="assets/ico/apple-touch-icon-57-precomposed.png">
|
|
|
|
+ <link rel="shortcut icon" href="assets/ico/favicon.png">
|
|
|
|
+
|
|
|
|
+ <!--
|
|
|
|
+ <script type="text/javascript">
|
|
|
|
+ var _gaq = _gaq || [];
|
|
|
|
+ _gaq.push(['_setAccount', 'UA-146052-10']);
|
|
|
|
+ _gaq.push(['_trackPageview']);
|
|
|
|
+ (function() {
|
|
|
|
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
|
|
|
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
|
|
|
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
|
|
|
+ })();
|
|
|
|
+ </script>
|
|
|
|
+ -->
|
|
|
|
+ </head>
|
|
|
|
+
|
|
|
|
+ <body data-spy="scroll" data-target=".bs-docs-sidebar">
|
|
|
|
+
|
|
|
|
+ <!-- Navbar
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <div class="navbar navbar-link navbar-fixed-top">
|
|
|
|
+ <div class="navbar-inner">
|
|
|
|
+ <div class="container">
|
|
|
|
+ <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
|
|
|
+ <span class="icon-bar"></span>
|
|
|
|
+ <span class="icon-bar"></span>
|
|
|
|
+ <span class="icon-bar"></span>
|
|
|
|
+ </button>
|
|
|
|
+ <a class="brand" href="./index.html">Metrix++</a>
|
|
|
|
+ <div class="nav-collapse collapse">
|
|
|
|
+ <ul class="nav">
|
|
|
|
+ <li class="">
|
|
|
|
+ <a href="./index.html">Home</a>
|
|
|
|
+ </li>
|
|
|
|
+ <li class="">
|
|
|
|
+ <a href="./workflow.html">Workflow</a>
|
|
|
|
+ </li>
|
|
|
|
+ <li class="">
|
|
|
|
+ <a href="./extend.html">Create plugin</a>
|
|
|
|
+ </li>
|
|
|
|
+ </ul>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- Subhead
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <header class="jumbotron" id="overview">
|
|
|
|
+ <div class="container">
|
|
|
|
+ <div class="row">
|
|
|
|
+ <div class="span3"></div>
|
|
|
|
+ <div class="span9">
|
|
|
|
+ <h5 class="text-right">Management of source code quality is possible.</h5>
|
|
|
|
+ <p class="text-right">
|
|
|
|
+ <a href="https://sourceforge.net/projects/metrixplusplus/files/latest/download"
|
|
|
|
+ ><button type="button"class="btn btn-danger">Download</button></a>
|
|
|
|
+ <!--
|
|
|
|
+ <button type="button"class="btn btn-warning">Donate</button>
|
|
|
|
+ -->
|
|
|
|
+ </p>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </header>
|
|
|
|
+
|
|
|
|
+ <div class="container"><div class="row">
|
|
|
|
+
|
|
|
|
+ <!-- Docs nav
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <div class="span3 bs-docs-sidebar">
|
|
|
|
+ <ul class="nav nav-list bs-docs-sidenav">
|
|
|
|
+ <li><a href="#metric_plugin"><i class="icon-chevron-right"></i> Metric plugin</a></li>
|
|
|
|
+ <li><a href="#tool_plugin"><i class="icon-chevron-right"></i> Post-analysis tool</a></li>
|
|
|
|
+ <li><a href="#language_plugin"><i class="icon-chevron-right"></i> Language parser</a></li>
|
|
|
|
+ </ul>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- Sections
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <div class="span9">
|
|
|
|
+ <div class="page-header">
|
|
|
|
+ <h1>Create plugin</h1>
|
|
|
|
+ </div>
|
|
|
|
+ <p>There are 3 types of plugins considered in this chapter:</p>
|
|
|
|
+ <ul>
|
|
|
|
+ <li>Metric plugin</li>
|
|
|
|
+ <li>Language parser</li>
|
|
|
|
+ <li>Post-processing / Analysis tool</li>
|
|
|
|
+ </ul>
|
|
|
|
+ <p>Tutorial for metric plugin is generic at the beginning and large portion of this is applied to
|
|
|
|
+ all other plugins. You need to know python and python regular expressions library to write Metrix++ extensions.</p>
|
|
|
|
+ <section id="metric_plugin">
|
|
|
|
+ <h2>Metric plugin</h2>
|
|
|
|
+ <p>The tutorial will explain how to create a plugin to count magic numbers in source code.
|
|
|
|
+ It will be relatively simple at first and will be extended with additional configuration
|
|
|
|
+ options and smarter counting logic.</p>
|
|
|
|
+ <h4>Create placeholder for new plugin</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>All plugins are loaded by Metrix++ from standard places within the tool installation directory and
|
|
|
|
+ from custom places specified in the METRIXPLUSPLUS_PATH environment variable.
|
|
|
|
+ METRIXPLUSPLUS_PATH has got the same format as system PATH environment variable.
|
|
|
|
+ So, the first step in plugin development is to set the METRIXPLUSPLUS_PATH to point out to
|
|
|
|
+ the directory (or directories) where plugin is located.</li>
|
|
|
|
+ <li>Create new python package 'myext', python lib 'magic.py' and 'magic.ini' file.</li>
|
|
|
|
+ <pre>
|
|
|
|
++ working_directory (set in METRIXPLUSPLUS_PATH variable)
|
|
|
|
+\--+ myext
|
|
|
|
+ \--- __init__.py
|
|
|
|
+ \--- magic.py
|
|
|
|
+ \--- magic.ini
|
|
|
|
+</pre>
|
|
|
|
+ <li>__init__.py is empty file to make myext considered by python as a package.</li>
|
|
|
|
+ <li>Edit magic.py to have the following content:
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin):
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ print "Hello world"
|
|
|
|
+</pre>
|
|
|
|
+ mpp.api package include Metrix++ API classes. mpp.api.Plugin is the base class, which can be loaded
|
|
|
|
+ by Metrix++ engine and does nothing by default. In the code sample above it is extended to print
|
|
|
|
+ "Hello world" on initialization.</li>
|
|
|
|
+ <li>Edit magic.ini to have the following content:
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+[Plugin]
|
|
|
|
+version: 1.0
|
|
|
|
+package: myext
|
|
|
|
+module: magic
|
|
|
|
+class: Plugin
|
|
|
|
+depends: None
|
|
|
|
+actions: collect
|
|
|
|
+enabled: True
|
|
|
|
+</pre>
|
|
|
|
+ This file is a manifest for Metrix++ plugin loader. The fields in Plugin section are:
|
|
|
|
+ <dl class="dl-horizontal">
|
|
|
|
+ <dt>version</dt>
|
|
|
|
+ <dd>a string representing the version, step up it every time when behaviour of a plugin
|
|
|
|
+ or backward compatibility in api or data scope is changed</dd>
|
|
|
|
+ <dt>package</dt>
|
|
|
|
+ <dd>python package name where to load from</dd>
|
|
|
|
+ <dt>module</dt>
|
|
|
|
+ <dd>python module name (filename of *.py file) to load</dd>
|
|
|
|
+ <dt>class</dt>
|
|
|
|
+ <dd>name of a plugin class to instanciate</dd>
|
|
|
|
+ <dt>depends</dt>
|
|
|
|
+ <dd>list of plugin names to load, if it this plugin is loaded</dd>
|
|
|
|
+ <dt>actions</dt>
|
|
|
|
+ <dd>list of Metrix++ actions affected by this plugin</dd>
|
|
|
|
+ <dt>enabled</dt>
|
|
|
|
+ <dd>True or False, working status of a plugin</dd>
|
|
|
|
+ </dl>
|
|
|
|
+ </li>
|
|
|
|
+ <li>Now run Metrix++ to see how this new plugin works:</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect</pre>
|
|
|
|
+ <pre>Hello world</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Toogle option for the plugin</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>It is recommended to follow the convention for all plugins: 'run only if enabled'.
|
|
|
|
+ So, let's extend the magic.py file to make it configurable
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ # make this instance configurable...
|
|
|
|
+ mpp.api.IConfigurable):
|
|
|
|
+ # ... and implement 2 interfaces
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ # use configuration option here
|
|
|
|
+ if self.is_active_numbers == True:
|
|
|
|
+ print "Hello world"
|
|
|
|
+</pre>
|
|
|
|
+ parser argument is an instance of optparse.OptionParser class. It has got an extension to
|
|
|
|
+ accept multiple options of the same argument. Check std.tools.limit to see how to declare multiopt options, if you need.</li>
|
|
|
|
+ <li>Now run Metrix++ to see how this works:</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>Hello world</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Subscribe to notifications from parent plugins (or code parsers)</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>Every plugin works in a callback functions called by parent plugins.
|
|
|
|
+ Callback receives a reference to parent plugin, data object where to store metrics data,
|
|
|
|
+ and a flag indicating if there are changes in file or parent's settings since the previous collection.</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ # declare that it can subscribe on notifications
|
|
|
|
+ mpp.api.Child):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ if self.is_active_numbers == True:
|
|
|
|
+ # subscribe to notifications from all code parsers
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode, 'callback')
|
|
|
|
+
|
|
|
|
+ # parents (code parsers) will call the callback declared
|
|
|
|
+ def callback(self, parent, data, is_updated):
|
|
|
|
+ print parent.get_name(), data.get_path(), is_updated
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to see how this works. Try to do iterative scans (--db-file-prev option) to see how the
|
|
|
|
+ state of arguments is changed</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>std.code.cpp ./test.cpp True</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Implement simple metric based on regular expression pattern</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>Callback may execute counting, searcing and additional parsing and store results, using data argument.
|
|
|
|
+ 'data' argument is an instance of mpp.api.FileData class.
|
|
|
|
+ However, most metrics can be implemented
|
|
|
|
+ simplier, if mpp.api.MetricPluginMixin routines are used. MetricPluginMixin implements
|
|
|
|
+ declarative style for metrics based on searches by regular expression. It
|
|
|
|
+ cares about initialisation of database fields and properties.
|
|
|
|
+ It implements default callback which counts number of matches by regular expression for all
|
|
|
|
+ active declared metrics. So, let's utilise that:
|
|
|
|
+ </li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import re
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ mpp.api.Child,
|
|
|
|
+ # reuse by inheriting standard metric facilities
|
|
|
|
+ mpp.api.MetricPluginMixin):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ # declare metric rules
|
|
|
|
+ self.declare_metric(
|
|
|
|
+ self.is_active_numbers, # to count if active in callback
|
|
|
|
+ self.Field('numbers', int), # field name and type in the database
|
|
|
|
+ re.compile(r'''\b[0-9]+\b'''), # pattern to search
|
|
|
|
+ marker_type_mask=mpp.api.Marker.T.CODE, # search in code
|
|
|
|
+ region_type_mask=mpp.api.Region.T.ANY) # search in all types of regions
|
|
|
|
+
|
|
|
|
+ # use superclass facilities to initialize everything from declared fields
|
|
|
|
+ super(Plugin, self).initialize(fields=self.get_fields())
|
|
|
|
+
|
|
|
|
+ # subscribe to all code parsers if at least one metric is active
|
|
|
|
+ if self.is_active() == True:
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode)
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to count numbers in code files.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <li>Now view the results. At this stage it is fully working simple metric.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" view</pre>
|
|
|
|
+ <pre>
|
|
|
|
+:: info: Overall metrics for 'myext.magic:numbers' metric
|
|
|
|
+ Average : 2.75
|
|
|
|
+ Minimum : 0
|
|
|
|
+ Maximum : 7
|
|
|
|
+ Total : 11.0
|
|
|
|
+ Distribution : 4 regions in total (including 0 suppressed)
|
|
|
|
+ Metric value : Ratio : R-sum : Number of regions
|
|
|
|
+ 0 : 0.250 : 0.250 : 1 |||||||||||||||||||||||||
|
|
|
|
+ 1 : 0.250 : 0.500 : 1 |||||||||||||||||||||||||
|
|
|
|
+ 3 : 0.250 : 0.750 : 1 |||||||||||||||||||||||||
|
|
|
|
+ 7 : 0.250 : 1.000 : 1 |||||||||||||||||||||||||
|
|
|
|
+
|
|
|
|
+:: info: Directory content:
|
|
|
|
+ Directory : .
|
|
|
|
+</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Extend regular expression incremental counting by smarter logic</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>At this stage the metric counts every number in source code.
|
|
|
|
+ However, we indent to spot only 'magic' numbers. Declared constant
|
|
|
|
+ is not a magic number, so it is better to exclude constants from counting.
|
|
|
|
+ It is easy to change default counter behaviour by implementing
|
|
|
|
+ a function with name '_<metric_name>_count'. </li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import re
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ mpp.api.Child,
|
|
|
|
+ mpp.api.MetricPluginMixin):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ # improve pattern to find declarations of constants
|
|
|
|
+ pattern_to_search = re.compile(
|
|
|
|
+ r'''((const\s+([_a-zA-Z][_a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ self.declare_metric(self.is_active_numbers,
|
|
|
|
+ self.Field('numbers', int),
|
|
|
|
+ # give a pair of pattern + custom counter logic class
|
|
|
|
+ (pattern_to_search, self.NumbersCounter),
|
|
|
|
+ marker_type_mask=mpp.api.Marker.T.CODE,
|
|
|
|
+ region_type_mask=mpp.api.Region.T.ANY)
|
|
|
|
+
|
|
|
|
+ super(Plugin, self).initialize(fields=self.get_fields())
|
|
|
|
+
|
|
|
|
+ if self.is_active() == True:
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode)
|
|
|
|
+
|
|
|
|
+ # implement custom counter behavior:
|
|
|
|
+ # increments counter by 1 only if single number spotted,
|
|
|
|
+ # but not declaration of a constant
|
|
|
|
+ class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
|
|
|
|
+ def increment(self, match):
|
|
|
|
+ if match.group(0).startswith('const'):
|
|
|
|
+ return 0
|
|
|
|
+ return 1
|
|
|
|
+</pre>
|
|
|
|
+ <li>Initialy counter is initialized by zero, but it is possible to
|
|
|
|
+ change it as well by implementing a function with name '_<metric_name>_count_initialize'.
|
|
|
|
+ Plugin we are implementing does not require this.</li>
|
|
|
|
+ <li>Now run Metrix++ to collect and view the results.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" view</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Language specific regular expressions</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>In the previous step we added matching of constants assuming that identifiers
|
|
|
|
+ may have symbols '_', 'a-z', 'A-Z' and '0-9'. It is true for C++ but it is not complete for Java.
|
|
|
|
+ Java identifier may have '$' symbol in the identifier. So, let's add language specific pattern
|
|
|
|
+ in the declaration of the metric:</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import re
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ mpp.api.Child,
|
|
|
|
+ mpp.api.MetricPluginMixin):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ # specialized pattern for java
|
|
|
|
+ pattern_to_search_java = re.compile(
|
|
|
|
+ r'''((const\s+([_$a-zA-Z][_$a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ # pattern for C++ and C# languages
|
|
|
|
+ pattern_to_search_cpp_cs = re.compile(
|
|
|
|
+ r'''((const\s+([_a-zA-Z][_a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ # pattern for all other languages
|
|
|
|
+ pattern_to_search = re.compile(
|
|
|
|
+ r'''\b[0-9]+\b''')
|
|
|
|
+ self.declare_metric(self.is_active_numbers,
|
|
|
|
+ self.Field('numbers', int),
|
|
|
|
+ # dictionary of pairs instead of a single pair
|
|
|
|
+ {
|
|
|
|
+ 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
|
|
|
|
+ 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ '*': pattern_to_search
|
|
|
|
+ },
|
|
|
|
+ marker_type_mask=mpp.api.Marker.T.CODE,
|
|
|
|
+ region_type_mask=mpp.api.Region.T.ANY)
|
|
|
|
+
|
|
|
|
+ super(Plugin, self).initialize(fields=self.get_fields())
|
|
|
|
+
|
|
|
|
+ if self.is_active() == True:
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode)
|
|
|
|
+
|
|
|
|
+ class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
|
|
|
|
+ def increment(self, match):
|
|
|
|
+ if match.group(0).startswith('const'):
|
|
|
|
+ return 0
|
|
|
|
+ return 1
|
|
|
|
+</pre>
|
|
|
|
+ <li>Keys in the dictionary of patterns are names of parent plugins (references to code parsers).
|
|
|
|
+ The key '*' refers to any parser.</li>
|
|
|
|
+ <li>Now run Metrix++ to collect and view the results.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" view</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Store only non-zero metric values</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>Most functions have the metric, which we are implemneting, equal to zero.
|
|
|
|
+ However, we are interested in finding code blocks having this metric greater than zero.
|
|
|
|
+ Zeros consumes the space in the data file. So, we can optimise the size of a data file,
|
|
|
|
+ if we exclude zero metric values. Let's declare this behavior for the metric.</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import re
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ mpp.api.Child,
|
|
|
|
+ mpp.api.MetricPluginMixin):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ pattern_to_search_java = re.compile(
|
|
|
|
+ r'''((const\s+([_$a-zA-Z][_$a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ pattern_to_search_cpp_cs = re.compile(
|
|
|
|
+ r'''((const\s+([_a-zA-Z][_a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ pattern_to_search = re.compile(
|
|
|
|
+ r'''\b[0-9]+\b''')
|
|
|
|
+ self.declare_metric(self.is_active_numbers,
|
|
|
|
+ self.Field('numbers', int,
|
|
|
|
+ # optimize the size of datafile:
|
|
|
|
+ # store only non-zero results
|
|
|
|
+ non_zero=True),
|
|
|
|
+ {
|
|
|
|
+ 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
|
|
|
|
+ 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ '*': pattern_to_search
|
|
|
|
+ },
|
|
|
|
+ marker_type_mask=mpp.api.Marker.T.CODE,
|
|
|
|
+ region_type_mask=mpp.api.Region.T.ANY)
|
|
|
|
+
|
|
|
|
+ super(Plugin, self).initialize(fields=self.get_fields())
|
|
|
|
+
|
|
|
|
+ if self.is_active() == True:
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode)
|
|
|
|
+
|
|
|
|
+ class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
|
|
|
|
+ def increment(self, match):
|
|
|
|
+ if match.group(0).startswith('const'):
|
|
|
|
+ return 0
|
|
|
|
+ return 1
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to collect and view the results.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" view</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Additional per metric configuration options</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>It is typical that most numbers counted by the metric are equal to 0, -1 or 1.
|
|
|
|
+ They are not necessary magic numbers. 0 or 1 are typical variable initializers.
|
|
|
|
+ -1 is a typical negative return code. So, let's implement simplified version of the metric,
|
|
|
|
+ which does not count 0, -1 and 1, if the specific new option is set.</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import re
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin,
|
|
|
|
+ mpp.api.IConfigurable,
|
|
|
|
+ mpp.api.Child,
|
|
|
|
+ mpp.api.MetricPluginMixin):
|
|
|
|
+
|
|
|
|
+ def declare_configuration(self, parser):
|
|
|
|
+ parser.add_option("--myext.magic.numbers", "--mmn",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Enables collection of magic numbers metric [default: %default]")
|
|
|
|
+ # Add new option
|
|
|
|
+ parser.add_option("--myext.magic.numbers.simplier", "--mmns",
|
|
|
|
+ action="store_true", default=False,
|
|
|
|
+ help="Is set, 0, -1 and 1 numbers are not counted [default: %default]")
|
|
|
|
+
|
|
|
|
+ def configure(self, options):
|
|
|
|
+ self.is_active_numbers = options.__dict__['myext.magic.numbers']
|
|
|
|
+ # remember the option here
|
|
|
|
+ self.is_active_numbers_simplier = options.__dict__['myext.magic.numbers.simplier']
|
|
|
|
+
|
|
|
|
+ def initialize(self):
|
|
|
|
+ pattern_to_search_java = re.compile(
|
|
|
|
+ r'''((const\s+([_$a-zA-Z][_$a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ pattern_to_search_cpp_cs = re.compile(
|
|
|
|
+ r'''((const\s+([_a-zA-Z][_a-zA-Z0-9]*\s+)+[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
|
|
|
|
+ pattern_to_search = re.compile(
|
|
|
|
+ r'''\b[0-9]+\b''')
|
|
|
|
+ self.declare_metric(self.is_active_numbers,
|
|
|
|
+ self.Field('numbers', int,
|
|
|
|
+ non_zero=True),
|
|
|
|
+ {
|
|
|
|
+ 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
|
|
|
|
+ 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
|
|
|
|
+ '*': pattern_to_search
|
|
|
|
+ },
|
|
|
|
+ marker_type_mask=mpp.api.Marker.T.CODE,
|
|
|
|
+ region_type_mask=mpp.api.Region.T.ANY)
|
|
|
|
+
|
|
|
|
+ super(Plugin, self).initialize(fields=self.get_fields(),
|
|
|
|
+ # remember option settings in data file properties
|
|
|
|
+ # in order to detect changes in settings on iterative re-run
|
|
|
|
+ properties=[self.Property('number.simplier', self.is_active_numbers_simplier)])
|
|
|
|
+
|
|
|
|
+ if self.is_active() == True:
|
|
|
|
+ self.subscribe_by_parents_interface(mpp.api.ICode)
|
|
|
|
+
|
|
|
|
+ class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
|
|
|
|
+ def increment(self, match):
|
|
|
|
+ if (match.group(0).startswith('const') or
|
|
|
|
+ (self.plugin.is_active_numbers_simplier == True and
|
|
|
|
+ match.group(0) in ['0', '1', '-1', '+1'])):
|
|
|
|
+ return 0
|
|
|
|
+ return 1
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to collect and view the results.</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" view</pre>
|
|
|
|
+ <pre>
|
|
|
|
+:: info: Overall metrics for 'myext.magic:numbers' metric
|
|
|
|
+ Average : 2.5 (excluding zero metric values)
|
|
|
|
+ Minimum : 2
|
|
|
|
+ Maximum : 3
|
|
|
|
+ Total : 5.0
|
|
|
|
+ Distribution : 2 regions in total (including 0 suppressed)
|
|
|
|
+ Metric value : Ratio : R-sum : Number of regions
|
|
|
|
+ 2 : 0.500 : 0.500 : 1 ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
|
+ 3 : 0.500 : 1.000 : 1 ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
|
+
|
|
|
|
+:: info: Directory content:
|
|
|
|
+ Directory : .
|
|
|
|
+</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Summary</h4>
|
|
|
|
+ <p>We have finished with the tutorial. The tutorial explained how to implement simple and advanced metric plugins.
|
|
|
|
+ We used built-in Metrix++ base classes. If you need to more advanced plugin capabilities,
|
|
|
|
+ override in your plugin class functions inherited from mpp.api base classes. Check code of standard plugins
|
|
|
|
+ to learn more techniques.</p>
|
|
|
|
+ <p></p>
|
|
|
|
+ </section>
|
|
|
|
+ <section id="tool_plugin">
|
|
|
|
+ <h2>Analysis tool plugin</h2>
|
|
|
|
+ <p>This tutorial will explain how to build custom Metrix++ command, which is bound to custom post-analysis tool.
|
|
|
|
+ We will implement the tool, which identifies all new and changed regions and counts number of added lines.
|
|
|
|
+ We skip calculating number of deleted lines, but it is easy to extend from what we get finally in the tutorial.</p>
|
|
|
|
+ <h4>New Metrix++ command / action</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>As in the tutorial for metric plugin, set the environment and
|
|
|
|
+ create new python package 'myext', python lib 'compare.py' and 'compare.ini' file.</li>
|
|
|
|
+ <pre>
|
|
|
|
++ working_directory (set in METRIXPLUSPLUS_PATH variable)
|
|
|
|
+\--+ myext
|
|
|
|
+ \--- __init__.py
|
|
|
|
+ \--- compare.py
|
|
|
|
+ \--- compare.ini
|
|
|
|
+</pre>
|
|
|
|
+ <li>__init__.py is empty file to make myext considered by python as a package.</li>
|
|
|
|
+ <li>Edit compare.py to have the following content:
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin, mpp.api.IRunable):
|
|
|
|
+
|
|
|
|
+ def run(self, args):
|
|
|
|
+ print args
|
|
|
|
+ return 0
|
|
|
|
+</pre>
|
|
|
|
+ Inheritance from mpp.api.IRunable declares that the plugin is runable and requires implementation of 'run' interface.</li>
|
|
|
|
+ <li>Edit compare.ini to have the following content:
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+
|
|
|
|
+[Plugin]
|
|
|
|
+version: 1.0
|
|
|
|
+package: myext
|
|
|
|
+module: compare
|
|
|
|
+class: Plugin
|
|
|
|
+depends: None
|
|
|
|
+actions: compare
|
|
|
|
+enabled: True
|
|
|
|
+</pre>
|
|
|
|
+ This file is a manifest for Metrix++ plugin loader. Actions field has got new value 'compare'.
|
|
|
|
+ Metrix++ engine will automatically pick this action and will add it to the list of available commands.
|
|
|
|
+ This plugin will be loaded on 'compare' action.
|
|
|
|
+ </li>
|
|
|
|
+ <li>Now run Metrix++ to see how this new plugin works:</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" compare -- path1 path2 path3</pre>
|
|
|
|
+ <pre>["path1", "path2", "path3"]</pre>
|
|
|
|
+ </ol>
|
|
|
|
+ <h4>Access data file loader and its' interfaces</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>By default, all post-analysis tools have got --db-file and --db-file-prev options. It is
|
|
|
|
+ because 'mpp.dbf' plugin is loaded for any action, including our new one 'compare'. In order to continue
|
|
|
|
+ the tutorial, we need to have 2 data files with 'std.code.lines:total' metric collected.
|
|
|
|
+ So, write to files by running:</li>
|
|
|
|
+ <pre>cd my_project_version_1
|
|
|
|
+> python "/path/to/metrix++.py" collect --std.code.lines.total</pre>
|
|
|
|
+ <pre>cd my_project_version_2
|
|
|
|
+> python "/path/to/metrix++.py" collect --std.code.lines.total</pre>
|
|
|
|
+ <li>Edit compare.py file to get the loader and iterate collected file paths:</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+# load common utils for post processing tools
|
|
|
|
+import mpp.utils
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin, mpp.api.IRunable):
|
|
|
|
+
|
|
|
|
+ def run(self, args):
|
|
|
|
+ # get data file reader using standard metrix++ plugin
|
|
|
|
+ loader = self.get_plugin('mpp.dbf').get_loader()
|
|
|
|
+
|
|
|
|
+ # iterate and print file length for every path in args
|
|
|
|
+ exit_code = 0
|
|
|
|
+ for path in (args if len(args) > 0 else [""]):
|
|
|
|
+ file_iterator = loader.iterate_file_data(path)
|
|
|
|
+ if file_iterator == None:
|
|
|
|
+ mpp.utils.report_bad_path(path)
|
|
|
|
+ exit_code += 1
|
|
|
|
+ continue
|
|
|
|
+ for file_data in file_iterator:
|
|
|
|
+ print file_data.get_path()
|
|
|
|
+ return exit_code
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to see how it works:</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db</pre>
|
|
|
|
+ </ol>
|
|
|
|
+
|
|
|
|
+ <h4>Identify added, modified files/regions and read metric data</h4>
|
|
|
|
+ <ol>
|
|
|
|
+ <li>Let's extend the logic of the tool to compare files and regions, read 'std.code.lines:total' metric
|
|
|
|
+ and calcuate the summary of number of added lines. mpp.utils.FileRegionsMatcher is helper class
|
|
|
|
+ which does matching and comparison of regions for 2 given mpp.api.FileData objects.</li>
|
|
|
|
+ <pre class="prettyprint linenums">
|
|
|
|
+import mpp.api
|
|
|
|
+import mpp.utils
|
|
|
|
+import mpp.cout
|
|
|
|
+
|
|
|
|
+class Plugin(mpp.api.Plugin, mpp.api.IRunable):
|
|
|
|
+
|
|
|
|
+ def run(self, args):
|
|
|
|
+ loader = self.get_plugin('mpp.dbf').get_loader()
|
|
|
|
+ # get previous db file loader
|
|
|
|
+ loader_prev = self.get_plugin('mpp.dbf').get_loader_prev()
|
|
|
|
+
|
|
|
|
+ exit_code = 0
|
|
|
|
+ for path in (args if len(args) > 0 else [""]):
|
|
|
|
+ added_lines = 0
|
|
|
|
+ file_iterator = loader.iterate_file_data(path)
|
|
|
|
+ if file_iterator == None:
|
|
|
|
+ mpp.utils.report_bad_path(path)
|
|
|
|
+ exit_code += 1
|
|
|
|
+ continue
|
|
|
|
+ for file_data in file_iterator:
|
|
|
|
+ added_lines += self._compare_file(file_data, loader, loader_prev)
|
|
|
|
+ mpp.cout.notify(path, '', mpp.cout.SEVERITY_INFO,
|
|
|
|
+ "Change trend report",
|
|
|
|
+ [('Added lines', added_lines)])
|
|
|
|
+ return exit_code
|
|
|
|
+
|
|
|
|
+ def _compare_file(self, file_data, loader, loader_prev):
|
|
|
|
+ # compare file with previous and return number of new lines
|
|
|
|
+ file_data_prev = loader_prev.load_file_data(file_data.get_path())
|
|
|
|
+ if file_data_prev == None:
|
|
|
|
+ return self._sum_file_regions_lines(file_data)
|
|
|
|
+ elif file_data.get_checksum() != file_data_prev.get_checksum():
|
|
|
|
+ return self._compare_file_regions(file_data, file_data_prev)
|
|
|
|
+
|
|
|
|
+ def _sum_file_regions_lines(self, file_data):
|
|
|
|
+ # just sum up the metric for all regions
|
|
|
|
+ result = 0
|
|
|
|
+ for region in file_data.iterate_regions():
|
|
|
|
+ result += region.get_data('std.code.lines', 'total')
|
|
|
|
+
|
|
|
|
+ def _compare_file_regions(self, file_data, file_data_prev):
|
|
|
|
+ # compare every region with previous and return number of new lines
|
|
|
|
+ matcher = mpp.utils.FileRegionsMatcher(file_data, file_data_prev)
|
|
|
|
+ result = 0
|
|
|
|
+ for region in file_data.iterate_regions():
|
|
|
|
+ if matcher.is_matched(region.get_id()) == False:
|
|
|
|
+ # if added region, just add the lines
|
|
|
|
+ result += region.get_data('std.code.lines', 'total')
|
|
|
|
+ elif matcher.is_modified(region.get_id()):
|
|
|
|
+ # if modified, add the difference in lines
|
|
|
|
+ region_prev = file_data_prev.get_region(
|
|
|
|
+ matcher.get_prev_id(region.get_id()))
|
|
|
|
+ result += (region.get_data('std.code.lines', 'total') -
|
|
|
|
+ region_prev.get_data('std.code.lines', 'total'))
|
|
|
|
+ return result
|
|
|
|
+</pre>
|
|
|
|
+ <li>Now run Metrix++ to see how it works:</li>
|
|
|
|
+ <pre>> python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db</pre>
|
|
|
|
+ <pre>
|
|
|
|
+:: info: Change trend report
|
|
|
|
+ Added lines : 7
|
|
|
|
+</pre>
|
|
|
|
+ </ol>
|
|
|
|
+
|
|
|
|
+ <h4>Summary</h4>
|
|
|
|
+ <p>We have finished with the tutorial. The tutorial explained how to read Metrix++ data files and
|
|
|
|
+ implement custom post-processing tools. Even if some existing Metrix++ code requires clean-up and refactoring,
|
|
|
|
+ check code of standard tool plugins to learn more techniques.</p>
|
|
|
|
+ </section>
|
|
|
|
+ <section id="language_plugin">
|
|
|
|
+ <h2>Language parser plugin</h2>
|
|
|
|
+
|
|
|
|
+ <p>Unfortunately, there is no good documentation at this stage for this part.
|
|
|
|
+ Briefly, if metric plugin counts and stores data into FileData object,
|
|
|
|
+ tool plugin reads this data, language plugin construct the original structure of
|
|
|
|
+ FileData object. The orginal structure includes regions (like functions, classes, etc.)
|
|
|
|
+ and markers (like comments, strings, preprocessor, etc.).
|
|
|
|
+ Check code of existing parsers.</p>
|
|
|
|
+ <ul>
|
|
|
|
+ <li>a language parser plugin is registered in the same way as a metric plugin</li>
|
|
|
|
+ <li>it registers parser's callback in 'std.tools.collect' plugin</li>
|
|
|
|
+ <li>parses a file in a callback, called by 'std.tools.collect'</li>
|
|
|
|
+ <li>parser needs to identify markers and regions
|
|
|
|
+ and tell about this to file data object passed as an
|
|
|
|
+ argument for the callback.</li>
|
|
|
|
+ </ul>
|
|
|
|
+
|
|
|
|
+ <p>There are useful options and tools avaialble for
|
|
|
|
+ trobuleshooting purposes during development:</p>
|
|
|
|
+ <ul>
|
|
|
|
+ <li>metrix++.py debug generates html code showing parsed code structures and their boundaries</li>
|
|
|
|
+ <li>--nest-regions for view tool forces the viewer to indent subregions.</li>
|
|
|
|
+ <li>--general.log-level option is available for any command and is helpful to trace execution.</li>
|
|
|
|
+ </ul>
|
|
|
|
+
|
|
|
|
+ <p>Finally, if there are any questions or enquires, please,
|
|
|
|
+ feel free to <a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">submit new question</a>.</p>
|
|
|
|
+
|
|
|
|
+ </section>
|
|
|
|
+ </div> <!-- end for sections -->
|
|
|
|
+ </div></div> <!-- end for row and container -->
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ <!-- Footer
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <footer class="footer">
|
|
|
|
+ <div class="container">
|
|
|
|
+ <div class="row">
|
|
|
|
+ <div class="span3">
|
|
|
|
+ <p><a href="http://sourceforge.net/projects/metrixplusplus/"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=275605&type=3"
|
|
|
|
+ alt="Get Metrix++ at SourceForge.net. Fast, secure and Free Open Source software downloads" border="0"></a></p>
|
|
|
|
+ <p>·</p>
|
|
|
|
+ <p>· ·<script type="text/javascript" src="http://www.ohloh.net/p/485947/widgets/project_users_logo.js"></script></p>
|
|
|
|
+ <p><a href="http://freecode.com/projects/metrix"><img src="assets/img/fm_logo.png" width="130"></a></p>
|
|
|
|
+ <p>·</p>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="span9">
|
|
|
|
+ <p>Copyright <strong>©</strong> 2009 - 2013, <a href="mailto:avkonst@users.sourceforge.net"><span class="normalImportance">Metrix++</span> Project</a></p>
|
|
|
|
+ <p>Code licensed under <a href="http://www.gnu.org/licenses/gpl.txt" target="_blank">GPL 3.0</a>, documentation under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.</p>
|
|
|
|
+ <ul class="footer-links">
|
|
|
|
+ <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Ask question</a></li>
|
|
|
|
+ <li class="muted">·</li>
|
|
|
|
+ <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Report defect</a></li>
|
|
|
|
+ <li class="muted">·</li>
|
|
|
|
+ <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Feature request</a></li>
|
|
|
|
+ <li class="muted">·</li>
|
|
|
|
+ <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/search/?q=%21status%3Awont-fix+%26%26+%21status%3Aclosed">Open issues</a></li>
|
|
|
|
+ <li class="muted">·</li>
|
|
|
|
+ <li><a href="https://sourceforge.net/p/metrixplusplus/wiki/ChangeLog/">Changelog</a></li>
|
|
|
|
+ </ul>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </footer>
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ <!-- Le javascript
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <!-- Placed at the end of the document so the pages load faster -->
|
|
|
|
+ <script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
|
|
|
|
+ <script src="assets/js/jquery.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-transition.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-alert.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-modal.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-dropdown.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-scrollspy.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-tab.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-tooltip.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-popover.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-button.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-collapse.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-carousel.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-typeahead.js"></script>
|
|
|
|
+ <script src="assets/js/bootstrap-affix.js"></script>
|
|
|
|
+
|
|
|
|
+ <script>
|
|
|
|
+ !function ($) {
|
|
|
|
+ $(function(){
|
|
|
|
+ // carousel demo
|
|
|
|
+ $('#myCarousel').carousel()
|
|
|
|
+ })
|
|
|
|
+ }(window.jQuery)
|
|
|
|
+ </script>
|
|
|
|
+
|
|
|
|
+ <script src="assets/js/holder/holder.js"></script>
|
|
|
|
+ <script src="assets/js/google-code-prettify/prettify.js"></script>
|
|
|
|
+
|
|
|
|
+ <script src="assets/js/application.js"></script>
|
|
|
|
+
|
|
|
|
+ <script>
|
|
|
|
+
|
|
|
|
+ </script>
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ <!-- Analytics
|
|
|
|
+ ================================================== -->
|
|
|
|
+ <!--
|
|
|
|
+ <script>
|
|
|
|
+ var _gauges = _gauges || [];
|
|
|
|
+ (function() {
|
|
|
|
+ var t = document.createElement('script');
|
|
|
|
+ t.type = 'text/javascript';
|
|
|
|
+ t.async = true;
|
|
|
|
+ t.id = 'gauges-tracker';
|
|
|
|
+ t.setAttribute('data-site-id', '4f0dc9fef5a1f55508000013');
|
|
|
|
+ t.src = '//secure.gaug.es/track.js';
|
|
|
|
+ var s = document.getElementsByTagName('script')[0];
|
|
|
|
+ s.parentNode.insertBefore(t, s);
|
|
|
|
+ })();
|
|
|
|
+ </script>
|
|
|
|
+ -->
|
|
|
|
+ </body>
|
|
|
|
+</html>
|