summaryrefslogtreecommitdiffstats
path: root/non-puppet
diff options
context:
space:
mode:
authorJuha Sippola <juhasippola@outlook.com>2015-09-21 15:15:18 +0300
committerTony Sarajärvi <tony.sarajarvi@theqtcompany.com>2015-09-23 09:38:09 +0000
commit787465ad37eabef9cb83fdb56954025a301422e6 (patch)
treed947c0e63868d0cffd3fda455134d8767811615e /non-puppet
parentcc1bb3968b1aa9795309bf3514595dc286e5bf27 (diff)
Qt Metrics 2 (v0.21): Testset testfunctions page
New page to list failed and skipped testfunctions for a selected testset in selected configuration. Change-Id: I9612ec4cd8b5ff829f0b8faf7f973345797d7daa Reviewed-by: Tony Sarajärvi <tony.sarajarvi@theqtcompany.com>
Diffstat (limited to 'non-puppet')
-rw-r--r--non-puppet/qtmetrics2/index.php57
-rw-r--r--non-puppet/qtmetrics2/src/Database.php74
-rw-r--r--non-puppet/qtmetrics2/src/Factory.php36
-rw-r--r--non-puppet/qtmetrics2/src/TestfunctionRun.php200
-rw-r--r--non-puppet/qtmetrics2/src/test/DatabaseTest.php45
-rw-r--r--non-puppet/qtmetrics2/src/test/FactoryTest.php37
-rw-r--r--non-puppet/qtmetrics2/templates/about.html4
-rw-r--r--non-puppet/qtmetrics2/templates/testset_testfunctions.html288
8 files changed, 726 insertions, 15 deletions
diff --git a/non-puppet/qtmetrics2/index.php b/non-puppet/qtmetrics2/index.php
index a7d0290..10a4856 100644
--- a/non-puppet/qtmetrics2/index.php
+++ b/non-puppet/qtmetrics2/index.php
@@ -34,7 +34,7 @@
/**
* Qt Metrics API
- * @since 10-08-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -469,6 +469,61 @@ $app->get('/testset/:testset/:project', function($testset, $project) use($app)
})->name('testset');
/**
+ * UI route: /testset/:testset/:project/:conf (GET)
+ */
+
+$app->get('/testset/:testset/:project/:conf', function($testset, $project, $conf) use($app)
+{
+ $testset = strip_tags($testset);
+ $project = strip_tags($project);
+ $conf = strip_tags($conf);
+ if (Factory::checkTestset($testset)) {
+ $testsetRoute = str_replace('/:testset/:project', '', Slim\Slim::getInstance()->urlFor('testset'));
+ $testsetProjectRoute = str_replace('/:project', '', Slim\Slim::getInstance()->urlFor('testsetproject'));
+ $confProjectRoute = str_replace('/:conf/:testsetproject', '', Slim\Slim::getInstance()->urlFor('conf_testsetproject'));
+ $ini = Factory::conf();
+ $breadcrumb = array(
+ array('name' => 'home', 'link' => Slim\Slim::getInstance()->urlFor('root')),
+ array('name' => 'overview', 'link' => Slim\Slim::getInstance()->urlFor('overview')),
+ array('name' => $project, 'link' => $testsetProjectRoute . '/' . $project),
+ array('name' => $conf, 'link' => $confProjectRoute . '/' . urlencode($conf) . '/' . $project),
+ array('name' => $testset, 'link' => $testsetRoute . '/' . $testset. '/' . $project)
+ );
+ $confProjectRoute = str_replace('/:conf/:testsetproject', '', Slim\Slim::getInstance()->urlFor('conf_testsetproject'));
+ $testfunctionRoute = 'testfunction'; // TODO: Replace later with $testfunctionRoute = str_replace('/:testfunction', '', Slim\Slim::getInstance()->urlFor('testfunction'));
+ $app->render('testset_testfunctions.html', array(
+ 'root' => Slim\Slim::getInstance()->urlFor('root'),
+ 'breadcrumb' => $breadcrumb,
+ 'testfunctionRoute' => $testfunctionRoute,
+ 'refreshed' => Factory::db()->getDbRefreshed() . ' (GMT)',
+ 'masterProject' => $ini['master_build_project'],
+ 'masterState' => $ini['master_build_state'],
+ 'conf' => $conf,
+ 'projectRuns' => Factory::createProjectRuns(
+ $ini['master_build_project'],
+ $ini['master_build_state']), // managed as objects
+ 'testset' => Factory::createTestset(
+ $testset,
+ $project,
+ $ini['master_build_project'],
+ $ini['master_build_state']), // managed as object
+ 'testfunctionRuns' => Factory::createTestfunctionRunsInConf(
+ $testset,
+ $project,
+ $conf,
+ $ini['master_build_project'],
+ $ini['master_build_state']) // managed as objects
+ ));
+ } else {
+ $app->render('empty.html', array(
+ 'root' => Slim\Slim::getInstance()->urlFor('root'),
+ 'message' => '404 Not Found'
+ ));
+ $app->response()->status(404);
+ }
+})->name('testset_testfunctions');
+
+/**
* UI route: /sitemap (GET)
*/
diff --git a/non-puppet/qtmetrics2/src/Database.php b/non-puppet/qtmetrics2/src/Database.php
index ce5f5ab..3700b78 100644
--- a/non-puppet/qtmetrics2/src/Database.php
+++ b/non-puppet/qtmetrics2/src/Database.php
@@ -34,8 +34,7 @@
/**
* Database class
- * @version 0.9
- * @since 21-07-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -924,7 +923,7 @@ class Database {
/**
* Get run results for a testset in specified builds by branch and configuration
* @param string $testset
- * @param $testsetProject
+ * @param string $testsetProject
* @param string $runProject
* @param string $runState
* @return array (string branch, string conf, string build_key, string result, string timestamp, string duration, int run)
@@ -977,7 +976,7 @@ class Database {
/**
* Get result counts for a testset project in specified builds by branch and configuration
- * @param $testsetProject
+ * @param string $testsetProject
* @param string $runProject
* @param string $runState
* @return array (string branch, string conf, string build_key, int passed, int ipassed, int failed, int ifailed)
@@ -1030,7 +1029,7 @@ class Database {
/**
* Get results for failed testsets in specified configuration builds by branch
* Only the failures are listed
- * @param $conf
+ * @param string $conf
* @param string $runProject
* @param string $runState
* @return array (string branch, string build_key, string testset, string project, string result, string timestamp, string duration, int run)
@@ -1085,8 +1084,8 @@ class Database {
/**
* Get results for failed testsets in specified configuration builds and project by branch
* Only the failures are listed
- * @param $conf
- * @param $testsetProject
+ * @param string $conf
+ * @param string $testsetProject
* @param string $runProject
* @param string $runState
* @return array (string branch, string build_key, string testset, string project, string result, string timestamp, string duration, int run)
@@ -1117,7 +1116,7 @@ class Database {
conf.name = ? AND
project_run.project_id = (SELECT id FROM project WHERE name = ?) AND
project_run.state_id = (SELECT id FROM state WHERE name = ?)
- ORDER BY branch.name, project.name, testset.name, project_run.build_key DESC;
+ ORDER BY branch.name, testset.name, project_run.build_key DESC;
");
$query->execute(array(
$testsetProject,
@@ -1141,6 +1140,65 @@ class Database {
}
/**
+ * Get results for failed and skipped testfunctions in specified configuration builds and project by branch
+ * Only the fail/skip and xpass/xfail results are listed
+ * @param string $testset
+ * @param string $testsetProject
+ * @param string $conf
+ * @param string $runProject
+ * @param string $runState
+ * @return array (string branch, string build_key, string testfunction, string result, string timestamp, string duration)
+ */
+ public function getTestfunctionConfResultsByBranch($testset, $testsetProject, $conf, $runProject, $runState)
+ {
+ $result = array();
+ $query = $this->db->prepare("
+ SELECT
+ branch.name AS branch,
+ project_run.build_key,
+ testfunction.name AS testfunction,
+ testfunction_run.result,
+ project_run.timestamp,
+ testfunction_run.duration
+ FROM testfunction_run
+ INNER JOIN testfunction ON testfunction_run.testfunction_id = testfunction.id
+ INNER JOIN testset_run ON testfunction_run.testset_run_id = testset_run.id
+ INNER JOIN testset ON testset_run.testset_id = testset.id
+ INNER JOIN project ON testset.project_id = project.id
+ INNER JOIN conf_run ON testset_run.conf_run_id = conf_run.id
+ INNER JOIN conf ON conf_run.conf_id = conf.id
+ INNER JOIN project_run ON conf_run.project_run_id = project_run.id
+ INNER JOIN branch ON project_run.branch_id = branch.id
+ WHERE
+ (testfunction_run.result LIKE '%fail' OR testfunction_run.result LIKE '%skip' OR testfunction_run.result LIKE '%x%') AND
+ testset.name = ? AND
+ project.name = ? AND
+ conf.name = ? AND
+ project_run.project_id = (SELECT id FROM project WHERE name = ?) AND
+ project_run.state_id = (SELECT id FROM state WHERE name = ?)
+ ORDER BY branch.name, testfunction.name, project_run.build_key DESC;
+ ");
+ $query->execute(array(
+ $testset,
+ $testsetProject,
+ $conf,
+ $runProject,
+ $runState
+ ));
+ while($row = $query->fetch(PDO::FETCH_ASSOC)) {
+ $result[] = array(
+ 'branch' => $row['branch'],
+ 'buildKey' => $row['build_key'],
+ 'testfunction' => $row['testfunction'],
+ 'result' => $row['result'],
+ 'timestamp' => $row['timestamp'],
+ 'duration' => $row['duration']
+ );
+ }
+ return $result;
+ }
+
+ /**
* Get the timestamp when database last refreshed
* @return string (timestamp)
*/
diff --git a/non-puppet/qtmetrics2/src/Factory.php b/non-puppet/qtmetrics2/src/Factory.php
index 8429852..fe35142 100644
--- a/non-puppet/qtmetrics2/src/Factory.php
+++ b/non-puppet/qtmetrics2/src/Factory.php
@@ -34,7 +34,7 @@
/**
* Factory class
- * @since 17-08-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -46,6 +46,7 @@ require_once 'Conf.php';
require_once 'ConfRun.php';
require_once 'Testset.php';
require_once 'TestsetRun.php';
+require_once 'TestfunctionRun.php';
class Factory {
@@ -404,6 +405,39 @@ class Factory {
}
/**
+ * Create TestfunctionRun objects in a configuration for those in database
+ * @param string $testset
+ * @param string $testsetProject
+ * @param string $conf
+ * @param string $runProject
+ * @param string $runState
+ * @return array TestfunctionRun objects
+ */
+ public static function createTestfunctionRunsInConf($testset, $testsetProject, $conf, $runProject, $runState)
+ {
+ $objects = array();
+ $dbEntries = self::db()->getTestfunctionConfResultsByBranch($testset, $testsetProject, $conf, $runProject, $runState);
+ foreach($dbEntries as $entry) {
+ $obj = new TestfunctionRun(
+ $entry['testfunction'],
+ $testset,
+ $testsetProject,
+ $runProject,
+ $entry['branch'],
+ $runState,
+ $entry['buildKey'],
+ $conf,
+ TestfunctionRun::stripResult($entry['result']),
+ TestfunctionRun::isBlacklisted($entry['result']),
+ $entry['timestamp'],
+ $entry['duration']
+ );
+ $objects[] = $obj;
+ }
+ return $objects;
+ }
+
+ /**
* Get the date that was n days before the last database refresh date.
* @param int $days
* @return string (date in unix date format)
diff --git a/non-puppet/qtmetrics2/src/TestfunctionRun.php b/non-puppet/qtmetrics2/src/TestfunctionRun.php
new file mode 100644
index 0000000..56f95aa
--- /dev/null
+++ b/non-puppet/qtmetrics2/src/TestfunctionRun.php
@@ -0,0 +1,200 @@
+<?php
+#############################################################################
+##
+## Copyright (C) 2015 The Qt Company Ltd.
+## Contact: http://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:LGPL21$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see http://www.qt.io/terms-conditions. For further
+## information use the contact form at http://www.qt.io/contact-us.
+##
+## GNU Lesser General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU Lesser
+## General Public License version 2.1 or version 3 as published by the Free
+## Software Foundation and appearing in the file LICENSE.LGPLv21 and
+## LICENSE.LGPLv3 included in the packaging of this file. Please review the
+## following information to ensure the GNU Lesser General Public License
+## requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+## http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+##
+## As a special exception, The Qt Company gives you certain additional
+## rights. These rights are described in The Qt Company LGPL Exception
+## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+/**
+ * TestfunctionRun class
+ * @since 08-09-2015
+ * @author Juha Sippola
+ */
+
+class TestfunctionRun extends ProjectRun {
+
+ /**
+ * Testfunction results (these must follow the enumeration in the database; excluding the blacklisted flag)
+ */
+ const RESULT_NOT_SET = NULL;
+ const RESULT_EMPTY = "";
+ const RESULT_NA = "na";
+ const RESULT_SUCCESS = "pass";
+ const RESULT_SUCCESS_UNEXPECTED = "xpass";
+ const RESULT_FAILURE = "fail";
+ const RESULT_FAILURE_EXPECTED = "xfail";
+ const RESULT_SKIP = "skip";
+
+ /**
+ * If the testfunction name long, a shorter version of the name can be requested
+ */
+ const SHORT_NAME_LENGTH = 50;
+
+ /**
+ * Testfunction name.
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Testset name.
+ * @var string
+ */
+ private $testsetName;
+
+ /**
+ * Testset project name.
+ * @var string
+ */
+ private $testsetProjectName;
+
+ /**
+ * Configuration name.
+ * @var string
+ */
+ private $confName;
+
+ /**
+ * Blacklisted flag (true = blacklisted).
+ * @var bool
+ */
+ private $blacklisted;
+
+ /**
+ * TestfunctionRun constructor.
+ * @param string $name
+ * @param string $testsetName
+ * @param string $testsetProjectName
+ * @param string $projectName
+ * @param string $branchName
+ * @param string $stateName
+ * @param int $buildKey
+ * @param string $confName
+ * @param string $result (plain result without any possible flags)
+ * @param bool $blacklisted (true = blacklisted)
+ * @param string $timestamp
+ * @param int $duration (in deciseconds)
+ */
+ public function __construct($name, $testsetName, $testsetProjectName, $projectName, $branchName, $stateName, $buildKey, $confName, $result, $blacklisted, $timestamp, $duration) {
+ parent::__construct($projectName, $branchName, $stateName, $buildKey, $result, $timestamp, $duration);
+ $this->name = $name;
+ $this->testsetName = $testsetName;
+ $this->testsetProjectName = $testsetProjectName;
+ $this->confName = $confName;
+ $this->blacklisted = $blacklisted;
+ }
+
+ /**
+ * Get name of the testfunction.
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get short name of the testfunction.
+ * @return string
+ */
+ public function getShortName()
+ {
+ if (strlen($this->name) > self::SHORT_NAME_LENGTH)
+ return substr($this->name, 0, self::SHORT_NAME_LENGTH - 10) . '...' . substr($this->name, -7);
+ else
+ return $this->name;
+ }
+
+ /**
+ * Get name of the testset project.
+ * @return string
+ */
+ public function getTestsetProjectName()
+ {
+ return $this->testsetProjectName;
+ }
+
+ /**
+ * Get name of the testset.
+ * @return string
+ */
+ public function getTestsetName()
+ {
+ return $this->testsetName;
+ }
+
+ /**
+ * Get configuration name.
+ * @return string
+ */
+ public function getConfName()
+ {
+ return $this->confName;
+ }
+
+ /**
+ * Get blacklisted flag.
+ * @return bool (true = blacklisted)
+ */
+ public function getBlacklisted()
+ {
+ return $this->blacklisted;
+ }
+
+ /**
+ * Strip the result from the combined blacklisted-result string
+ * @param string $resultString
+ * @return string
+ */
+ public static function stripResult($resultString)
+ {
+ $resultString = str_replace('bpass', 'pass', $resultString); // remove the possible blacklisted flag
+ $resultString = str_replace('bfail', 'fail', $resultString); // remove the possible blacklisted flag
+ $resultString = str_replace('bx', 'x', $resultString); // remove the possible blacklisted flag
+ $resultString = str_replace('bskip', 'skip', $resultString); // remove the possible blacklisted flag
+ return $resultString;
+ }
+
+ /**
+ * Check the blacklisted flag from the combined blacklisted-result string
+ * @param string $resultString
+ * @return bool (true = blacklisted)
+ */
+ public static function isBlacklisted($resultString)
+ {
+ $flag = false;
+ if (strpos($resultString, 'b') === 0) // begins with 'b'
+ $flag = true;
+ return $flag;
+ }
+
+}
+
+?>
diff --git a/non-puppet/qtmetrics2/src/test/DatabaseTest.php b/non-puppet/qtmetrics2/src/test/DatabaseTest.php
index 8fc798c..7ce99df 100644
--- a/non-puppet/qtmetrics2/src/test/DatabaseTest.php
+++ b/non-puppet/qtmetrics2/src/test/DatabaseTest.php
@@ -38,8 +38,7 @@ require_once(__DIR__.'/../Factory.php');
* Database unit test class
* Some of the tests require the test data as inserted into database with qtmetrics_insert.sql
* @example To run (in qtmetrics root directory): php <path-to-phpunit>/phpunit.phar ./src/test
- * @version 0.9
- * @since 21-07-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -944,6 +943,48 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
}
/**
+ * Test getTestfunctionConfResultsByBranch
+ * @dataProvider testGetTestfunctionConfResultsByBranchData
+ */
+ public function testGetTestfunctionConfResultsByBranch($testset, $testsetProject, $conf, $runProject, $runState, $exp_branch, $exp_testfunction, $exp_key, $has_data)
+ {
+ $branches = array();
+ $keys = array();
+ $testfunctions = array();
+ $db = Factory::db();
+ $result = $db->getTestfunctionConfResultsByBranch($testset, $testsetProject, $conf, $runProject, $runState);
+ foreach($result as $row) {
+ $this->assertArrayHasKey('branch', $row);
+ $this->assertArrayHasKey('buildKey', $row);
+ $this->assertArrayHasKey('testfunction', $row);
+ $this->assertArrayHasKey('result', $row);
+ $this->assertArrayHasKey('timestamp', $row);
+ $this->assertArrayHasKey('duration', $row);
+ $branches[] = $row['branch'];
+ $keys[] = $row['buildKey'];
+ $testfunctions[] = $row['testfunction'];
+ }
+ if ($has_data) {
+ $this->assertNotEmpty($result);
+ $this->assertContains($exp_branch, $branches);
+ $this->assertContains($exp_key, $keys);
+ $this->assertContains($exp_testfunction, $testfunctions);
+ } else {
+ $this->assertEmpty($result);
+ }
+ }
+ public function testGetTestfunctionConfResultsByBranchData()
+ {
+ return array(
+ array('tst_qfont', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', 'exactMatch', '1348', 1), // fail
+ array('tst_qfont', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', 'lastResortFont', '1348', 1), // skip
+ array('tst_networkselftest', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', 'smbServer', '1348', 1), // skip
+ array('tst_qftp', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', '', '', '', 0), // no fail or skip
+ array('tst_qfont', 'qtbase', 'invalid', 'Qt5', 'state', '', '', '', 0)
+ );
+ }
+
+ /**
* Test getDbRefreshed
*/
public function testGetDbRefreshed()
diff --git a/non-puppet/qtmetrics2/src/test/FactoryTest.php b/non-puppet/qtmetrics2/src/test/FactoryTest.php
index 8fbb17e..5e24833 100644
--- a/non-puppet/qtmetrics2/src/test/FactoryTest.php
+++ b/non-puppet/qtmetrics2/src/test/FactoryTest.php
@@ -37,7 +37,7 @@ require_once(__DIR__.'/../Factory.php');
/**
* Factory unit test class
* @example To run (in qtmetrics root directory): php <path-to-phpunit>/phpunit.phar ./src/test
- * @since 17-08-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -424,6 +424,41 @@ class FactoryTest extends PHPUnit_Framework_TestCase
}
/**
+ * Test createTestfunctionRunsInConf
+ * @dataProvider testCreateTestfunctionRunsInConfData
+ */
+ public function testCreateTestfunctionRunsInConf($testset, $testsetProject, $conf, $runProject, $runState, $exp_branch, $exp_buildKey, $exp_testfunction, $has_data)
+ {
+ $branches = array();
+ $buildKeys = array();
+ $testfunctions = array();
+ $runs = Factory::createTestfunctionRunsInConf($testset, $testsetProject, $conf, $runProject, $runState);
+ foreach($runs as $run) {
+ $this->assertTrue($run instanceof TestfunctionRun);
+ $branches[] = $run->getBranchName();
+ $buildKeys[] = $run->getBuildKey();
+ $testfunctions[] = $run->getName();
+ }
+ if ($has_data) {
+ $this->assertContains($exp_branch, $branches);
+ $this->assertContains($exp_buildKey, $buildKeys);
+ $this->assertContains($exp_testfunction, $testfunctions);
+ } else {
+ $this->assertEmpty($runs);
+ }
+ }
+ public function testCreateTestfunctionRunsInConfData()
+ {
+ return array(
+ array('tst_qfont', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', '1348', 'exactMatch', 1), // fail
+ array('tst_qfont', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', '1348', 'lastResortFont', 1), // skip
+ array('tst_networkselftest', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', 'stable', '1348', 'smbServer', 1), // skip
+ array('tst_qftp', 'qtbase', 'macx-clang_developer-build_OSX_10.8', 'Qt5', 'state', '', '', '', 0), // no fail or skip
+ array('tst_qfont', 'qtbase', 'invalid', 'Qt5', 'state', '', '', '', 0)
+ );
+ }
+
+ /**
* Test getSinceDate
* @dataProvider testGetSinceDateData
*/
diff --git a/non-puppet/qtmetrics2/templates/about.html b/non-puppet/qtmetrics2/templates/about.html
index e8b7dd2..9eb5d0e 100644
--- a/non-puppet/qtmetrics2/templates/about.html
+++ b/non-puppet/qtmetrics2/templates/about.html
@@ -34,7 +34,7 @@
/**
* About window content
- * @since 19-08-2015
+ * @since 08-09-2015
* @author Juha Sippola
*/
@@ -43,4 +43,4 @@
<p>This is Qt Metrics revision 2 with redesigned UI and database.</p>
<p>These pages are still <strong>under construction</strong> and therefore the views and functionality is limited.</p>
<p>See the <a href="https://wiki.qt.io/Qt_Metrics_2_Backlog" target="_blank">backlog</a> for development items currently identified or in progress.</p>
-<p><small>Version 0.20 (19-Aug-2015)</small></p>
+<p><small>Version 0.21 (8-Sep-2015)</small></p>
diff --git a/non-puppet/qtmetrics2/templates/testset_testfunctions.html b/non-puppet/qtmetrics2/templates/testset_testfunctions.html
new file mode 100644
index 0000000..6f6317c
--- /dev/null
+++ b/non-puppet/qtmetrics2/templates/testset_testfunctions.html
@@ -0,0 +1,288 @@
+{#
+#############################################################################
+##
+## Copyright (C) 2015 The Qt Company Ltd.
+## Contact: http://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:LGPL21$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see http://www.qt.io/terms-conditions. For further
+## information use the contact form at http://www.qt.io/contact-us.
+##
+## GNU Lesser General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU Lesser
+## General Public License version 2.1 or version 3 as published by the Free
+## Software Foundation and appearing in the file LICENSE.LGPLv21 and
+## LICENSE.LGPLv3 included in the packaging of this file. Please review the
+## following information to ensure the GNU Lesser General Public License
+## requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+## http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+##
+## As a special exception, The Qt Company gives you certain additional
+## rights. These rights are described in The Qt Company LGPL Exception
+## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+/**
+ * Testfunctions page
+ * @since 08-09-2015
+ * @author Juha Sippola
+ */
+
+#}
+
+{% include "header.html" %}
+
+{# testset as Testset object
+/**
+ * @var Testset[] testset
+ */
+#}
+
+{# projectRuns as ProjectRun objects
+/**
+ * @var ProjectRun[] projectRuns
+ */
+#}
+
+{# testfunctionRuns as TestfunctionRun objects
+/**
+ * @var TestfunctionRun[] testfunctionRuns
+ */
+#}
+
+<ol class="breadcrumb">
+{% for link in breadcrumb %}
+<li><a href="{{ link.link }}">{{ link.name }}</a></li>
+{% endfor %}
+<li class="active">testfunctions</li>
+</ol>
+
+<div class="container-fluid">
+<div class="row">
+
+<div class="col-sm-12 col-md-12 main">
+
+{# Check if any runs available #}
+{% set runsAvailable = 0 %}
+{% for run in testfunctionRuns %}
+{% set runsAvailable = 1 %}
+{% endfor %}
+
+{##### Title #####}
+
+<h1 class="page-header">
+{{ testset.getName }}
+<button type="button" class="btn btn-xs btn-info" data-toggle="collapse" data-target="#info" aria-expanded="false" aria-controls="info">
+<span class="glyphicon glyphicon-info-sign"></span>
+</button>
+<small>{{ refreshed }}</small>
+</h1>
+
+{##### Info well #####}
+
+<div class="collapse" id="info">
+<div class="well infoWell">
+<span class="glyphicon glyphicon-info-sign"></span> <strong>Testset</strong><br>
+<ul>
+<li><strong>Testfunction Results in Branches</strong> shows the {{ testset.getName }} <strong>failed and skipped</strong> results in configuration
+{{ conf }} by branch on <strong>{{ masterProject }} {{ masterState }}</strong> builds
+<ul>
+<li>flags: <span class="label label-default">b</span> = blacklisted flag set for the testfunction on the latest build shown</li>
+<li>results: <span class="glyphicon glyphicon-remove red"></span> = {{ constant('TestfunctionRun::RESULT_FAILURE') }},
+<span class="glyphicon glyphicon-ok-sign red"></span> = {{ constant('TestfunctionRun::RESULT_SUCCESS_UNEXPECTED') }},
+<span class="glyphicon glyphicon-remove-sign green"></span> = {{ constant('TestfunctionRun::RESULT_FAILURE_EXPECTED') }},
+<span class="glyphicon glyphicon-ban-circle gray"></span> = {{ constant('TestfunctionRun::RESULT_SKIP') }}</li>
+</ul>
+</li>
+<li>Details on the runs are available as tooltip on result icon</li>
+</ul>
+</div>
+</div>
+
+{% if runsAvailable %}
+
+{##### Results in Branches #####}
+
+<div class="panel panel-primary">
+<div class="panel-heading">
+<h4 class="panel-title bold">Testfunction Results in Branches <small>(failures and skipped only)</small></h4>
+</div>
+</div>
+
+{# Get branches #}
+{% set branches = [] %}
+{% for run in projectRuns %}
+{% if run.getBranchName not in branches %}
+{% set branches = branches|merge([run.getBranchName]) %}
+{% endif %}
+{% endfor %}
+
+{# Loop all the branches #}
+{% for branch in branches %}
+
+{# Get all build keys, dates and log links #}
+{% set buildKey = '' %}
+{% set buildKeys = [] %}
+{% set dates = [] %}
+{% for run in projectRuns %}
+{% if run.getBranchName == branch %}
+{% if buildKey != run.getBuildKey %}
+{% set buildKey = run.getBuildKey %}
+{% set buildKeys = buildKeys|merge([run.getBuildKey]) %}
+{% set dates = dates|merge([run.getTimestamp]) %}
+{% endif %}
+{% endif %}
+{% endfor %}
+
+{# Check if testfunction run for this branch #}
+{% set testfunctionBranch = 0 %}
+{% for run in testfunctionRuns if run.getBranchName == branch %}
+{% set testfunctionBranch = 1 %}
+{% endfor %}
+
+{# Show branch if testfunction run for it #}
+{% if testfunctionBranch %}
+<div class="panel panel-info">
+<div class="panel-heading">
+<h4 class="panel-title bold">{{ branch }}</h4>
+</div>
+<div class="panel-body">
+<div class="table-responsive">
+<table class="table table-striped">
+<thead>
+<tr>
+<th class="bold">{{ testset.getName }}</th>
+<th class="bold rightBorder">flags</th>
+{% for key, buildKey in buildKeys %}
+<th class="center">
+{% if buildKey|length > 6 %}
+<span class="clickOnTouch" data-toggle="tooltip" data-placement="top" title="{{ buildKey }}">{{ buildKey|slice(0, 4) }}...</span><br>
+{% else %}
+{{ buildKey }}<br>
+{% endif %}
+<span class="gray"><small>{{ dates[key]|date("m-d") }}</small></span>
+</th>
+{% endfor %}
+</tr>
+</thead>
+<tbody>
+{% set testfunctionPrev = '' %}
+{% set buildKeyIndexPrinted = -1 %}
+{% set buildKeyFound = 0 %}
+{% for run in testfunctionRuns if run.getBranchName == branch %}
+
+{# New row for each testfunction #}
+{% if testfunctionPrev != run.getName %}
+{# Close previous row #}
+{% if testfunctionPrev != '' %}
+{# Fill empty cells at the end of the row #}
+{% for key, buildKey in buildKeys %}
+{% if key > buildKeyIndexPrinted %}
+<td></td>
+{% endif %}
+{% endfor %}
+</tr>
+{% endif %}
+<tr>
+{% set link = testfunctionRoute ~ '/' ~ run.getName|url_encode ~ '/' ~ testset.getName|url_encode ~ '/' ~ testset.getProjectName|url_encode ~ '/' ~ run.getConfName|url_encode %}
+<td><a href="{{ link }}"><small>
+{% if run.getName|length > constant('TestfunctionRun::SHORT_NAME_LENGTH') %}
+<span class="clickOnTouch" data-toggle="tooltip" data-placement="top" title="{{ run.getName }}">{{ run.getShortName }}</span>
+{% else %}
+{{ run.getName }}
+{% endif %}
+</small></a></td>
+
+{# Flags for the latest build #}
+<td class="center rightBorder">
+{% if run.getBlacklisted %}
+<span class="label label-default">b</span>
+{% endif %}
+</td>
+{% set buildKeyIndexPrinted = -1 %}
+{% endif %}
+
+{# Result per build key #}
+{% set buildKeyFound = 0 %}
+{% for key, buildKey in buildKeys %}
+{# Print each column only once (checked based on column index key and buildKeyFound flag) #}
+{% if key > buildKeyIndexPrinted and not buildKeyFound %}
+{% if buildKey == run.getBuildKey %}
+{# Print result #}
+{% if run.getResult == constant('TestfunctionRun::RESULT_FAILURE') %}
+{% set resultIcon = 'glyphicon glyphicon-remove red' %}
+{% elseif run.getResult == constant('TestfunctionRun::RESULT_FAILURE_EXPECTED') %}
+{% set resultIcon = 'glyphicon glyphicon-remove-sign green' %}
+{% elseif run.getResult == constant('TestfunctionRun::RESULT_SUCCESS_UNEXPECTED') %}
+{% set resultIcon = 'glyphicon glyphicon-ok-sign red' %}
+{% elseif run.getResult == constant('TestfunctionRun::RESULT_SKIP') %}
+{% set resultIcon = 'glyphicon glyphicon-ban-circle gray' %}
+{% else %}
+{% set resultIcon = '' %}
+{% endif %}
+{% if (run.getDuration / 10) > 60 %}
+{% set durationFormatted = ' (00:' ~ ((run.getDuration/10)|round)|date("i:s") ~ ')' %}
+{% else %}
+{% set durationFormatted = '' %}
+{% endif %}
+<td class="center">
+<span class="spaceHorizontal {{ resultIcon }} clickOnTouch" data-toggle="tooltip" data-placement="top" data-html="true"
+title="<table>
+<tr><th>Build key: </th><td>{{ buildKey }}</td></tr>
+<tr><th>Configuration: </th><td>{{ run.getConfName }}</td></tr>
+<tr><th>Timestamp: </th><td>{{ run.getTimestamp }}</td></tr>
+<tr><th>Result: </th><td>{{ run.getResult }}</td></tr>
+<tr><th>Duration: </th><td>{{ run.getDuration / 10 }} s {{ durationFormatted }}</td></tr>
+<tr><th>Blacklisted: </th><td>{% if run.getBlacklisted %}yes{% else %}no{% endif %}</td></tr></table>">
+</span></td>
+{% set buildKeyFound = 1 %}
+{% else %}
+{# Print empty cell #}
+<td></td>
+{% endif %}
+{% set buildKeyIndexPrinted = key %}
+{% endif %}{# key #}
+{% endfor %}{# key #}
+{% set testfunctionPrev = run.getName %}
+{% endfor %}{# run #}
+
+{# Close last row (also fill empty cells at the end of the row) #}
+{% for key, buildKey in buildKeys %}
+{% if key > buildKeyIndexPrinted %}
+<td></td>
+{% endif %}
+{% endfor %}{# key #}
+</tr>
+</tbody>
+</table>
+</div> {# .table-responsive #}
+</div> {# .panel-body #}
+</div> {# .panel... #}
+{% endif %}{# testfunctionBranch #}
+{% endfor %}{# branch #}
+
+{% else %}{# runsAvailable #}
+<div class="alert alert-success" role="alert">
+No failed or skipped testfunctions in testset {{ testset.getName }} in project {{ testset.getProjectName }} and configuration {{ conf }}!
+</div>
+{% endif %}{# runsAvailable #}
+</div> {# .col... #}
+</div> {# .row #}
+</div> {# /container-fluid #}
+
+{% include "footer.html" %}
+
+{# Local scripts for this page #}
+<script src="scripts/tooltip.js"></script>
+
+{% include "close.html" %}