You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by dp...@apache.org on 2018/10/24 12:15:50 UTC

[ignite-teamcity-bot] 01/02: IGNITE-9645 Add comparison of failed tests lists in two date intervals - Fixes #36.

This is an automated email from the ASF dual-hosted git repository.

dpavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git

commit 3f369509c308163a6495b5e6569773d8d39ad711
Author: ololo3000 <pm...@gmail.com>
AuthorDate: Wed Oct 24 15:15:15 2018 +0300

    IGNITE-9645 Add comparison of failed tests lists in two date intervals - Fixes #36.
    
    Signed-off-by: Dmitriy Pavlov <dp...@apache.org>
---
 .../main/java/org/apache/ignite/ci/ITeamcity.java  |  14 +-
 .../apache/ignite/ci/IgnitePersistentTeamcity.java |  80 +++--
 .../apache/ignite/ci/IgniteTeamcityConnection.java |  48 ++-
 .../java/org/apache/ignite/ci/db/DbMigrations.java |   4 +-
 .../ignite/ci/tcmodel/result/Configurations.java   |  54 ++++
 .../ci/tcmodel/result/tests/TestOccurrence.java    |   8 +
 .../ignite/ci/tcmodel/result/tests/TestRef.java    |   2 +
 .../web/model/current/BuildStatisticsSummary.java  |  16 +-
 .../ignite/ci/web/model/hist/BuildsHistory.java    | 334 +++++++++++++++++++++
 .../ci/web/rest/build/GetBuildTestFailures.java    | 110 +++----
 .../ignite/ci/web/rest/parms/FullQueryParams.java  |  32 +-
 .../src/main/webapp/comparison.html                | 240 ++++++++++++++-
 .../src/main/webapp/css/style-1.5.css              | 128 ++++++++
 13 files changed, 954 insertions(+), 116 deletions(-)

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
index 6aa3b60..628581f 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -36,15 +37,18 @@ import org.apache.ignite.ci.tcmodel.changes.ChangesList;
 import org.apache.ignite.ci.tcmodel.conf.BuildType;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.Configurations;
 import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrence;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.tcmodel.user.User;
 import org.apache.ignite.ci.teamcity.pure.ITeamcityConn;
 import org.apache.ignite.ci.util.Base64Util;
+import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 import org.jetbrains.annotations.NotNull;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
@@ -150,10 +154,12 @@ public interface ITeamcity extends ITeamcityConn {
      * @param build
      * @return
      */
-    ProblemOccurrences getProblems(Build build);
+    ProblemOccurrences getProblems(BuildRef build);
 
     TestOccurrences getTests(String href, String normalizedBranch);
 
+    TestOccurrences getFailedTests(String href, int count, String normalizedBranch);
+
     Statistics getBuildStatistics(String href);
 
     CompletableFuture<TestOccurrenceFull> getTestFull(String href);
@@ -162,6 +168,10 @@ public interface ITeamcity extends ITeamcityConn {
 
     ChangesList getChangesList(String href);
 
+    CompletableFuture<TestRef> getTestRef(FullQueryParams key);
+
+    Configurations getConfigurations(FullQueryParams key);
+
     /**
      * List of build's related issues.
      *
@@ -251,6 +261,8 @@ public interface ITeamcity extends ITeamcityConn {
 
     void setExecutor(ExecutorService pool);
 
+    Executor getExecutor();
+
 
     /**
      * @param tok TeamCity authorization token.
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
index d1e94e4..60f61ba 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
@@ -32,6 +32,7 @@ import java.util.TreeMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -66,16 +67,19 @@ import org.apache.ignite.ci.tcmodel.changes.ChangesList;
 import org.apache.ignite.ci.tcmodel.conf.BuildType;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.Configurations;
 import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrence;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.tcmodel.user.User;
 import org.apache.ignite.ci.util.CacheUpdateUtil;
 import org.apache.ignite.ci.util.CollectionUtil;
 import org.apache.ignite.ci.util.ObjectInterner;
+import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -104,6 +108,8 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
     private static final String BUILD_HIST_FINISHED = "buildHistFinished";
     private static final String BUILD_HIST_FINISHED_OR_FAILED = "buildHistFinishedOrFailed";
     public static final String BOT_DETECTED_ISSUES = "botDetectedIssues";
+    public static final String TEST_REFS = "testRefs";
+    public static final String CONFIGURATIONS = "configurations";
 
     //todo need separate cache or separate key for 'execution time' because it is placed in statistics
     private static final String BUILDS_FAILURE_RUN_STAT = "buildsFailureRunStat";
@@ -125,6 +131,9 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
      */
     private ConcurrentMap<String, CompletableFuture<TestOccurrenceFull>> testOccFullFutures = new ConcurrentHashMap<>();
 
+    /** Cached loads of test refs.*/
+    private ConcurrentMap<String, CompletableFuture<TestRef>> testRefsFutures = new ConcurrentHashMap<>();
+
     /** cached running builds for branch. */
     private ConcurrentMap<String, Expirable<List<BuildRef>>> queuedBuilds = new ConcurrentHashMap<>();
 
@@ -162,7 +171,8 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
                 buildProblemsCache(),
                 buildStatisticsCache(),
                 buildHistCache(),
-                buildHistIncFailedCache());
+                buildHistIncFailedCache(),
+                testRefsCache());
     }
 
     @Override
@@ -221,6 +231,20 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
     }
 
     /**
+     * @return {@link Configurations} instances cache, 32 parts.
+     */
+    private IgniteCache<String, Configurations> configurationsCache() {
+        return getOrCreateCacheV2(ignCacheNme(CONFIGURATIONS));
+    }
+
+    /**
+     * @return {@link TestRef} instances cache, 32 parts.
+     */
+    private IgniteCache<String, TestRef> testRefsCache() {
+        return getOrCreateCacheV2(ignCacheNme(TEST_REFS));
+    }
+
+    /**
      * @return Build {@link ProblemOccurrences} instances cache, 32 parts.
      */
     private IgniteCache<String, ProblemOccurrences> buildProblemsCache() {
@@ -766,26 +790,20 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
 
     /** {@inheritDoc}*/
     @AutoProfiling
-    @Override public ProblemOccurrences getProblems(Build build) {
-        if (build.problemOccurrences != null) {
-            String href = build.problemOccurrences.href;
-
-            return loadIfAbsent(
-                buildProblemsCache(),
-                href,
-                k -> {
-                    ProblemOccurrences problems = teamcity.getProblems(build);
+    @Override public ProblemOccurrences getProblems(BuildRef buildRef) {
+        return loadIfAbsent(
+            buildProblemsCache(),
+            "/app/rest/latest/problemOccurrences?locator=build:(id:" + buildRef.getId() + ")",
+            k -> {
+                ProblemOccurrences problems = teamcity.getProblems(buildRef);
 
-                    registerCriticalBuildProblemInStat(build, problems);
+                registerCriticalBuildProblemInStat(buildRef, problems);
 
-                    return problems;
-                });
-        }
-        else
-            return new ProblemOccurrences();
+                return problems;
+            });
     }
 
-    private void registerCriticalBuildProblemInStat(Build build, ProblemOccurrences problems) {
+    private void registerCriticalBuildProblemInStat(BuildRef build, ProblemOccurrences problems) {
         boolean criticalFail = problems.getProblemsNonNull().stream().anyMatch(occurrence ->
             occurrence.isExecutionTimeout() || occurrence.isJvmCrash());
 
@@ -827,6 +845,20 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
             hrefIgnored -> teamcity.getTests(href, normalizedBranch));
     }
 
+    /** {@inheritDoc} */
+    @AutoProfiling
+    @Override public TestOccurrences getFailedTests(String href, int count, String normalizedBranch) {
+        return getTests(href + ",muted:false,status:FAILURE,count:" + count + "&fields=testOccurrence(id,name)", normalizedBranch);
+    }
+
+    /** {@inheritDoc} */
+    @AutoProfiling
+    @Override public Configurations getConfigurations(FullQueryParams key) {
+        return loadIfAbsent(configurationsCache(),
+            key.toString(),
+            k -> teamcity.getConfigurations(key));
+    }
+
     private void addTestOccurrencesToStat(TestOccurrences val) {
         for (TestOccurrence next : val.getTests())
             addTestOccurrenceToStat(next, ITeamcity.DEFAULT, null);
@@ -876,6 +908,16 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
 
     /** {@inheritDoc} */
     @AutoProfiling
+    @Override public CompletableFuture<TestRef> getTestRef(FullQueryParams key) {
+        return CacheUpdateUtil.loadAsyncIfAbsent(
+            testRefsCache(),
+            key.toString(),
+            testRefsFutures,
+            k -> teamcity.getTestRef(key));
+    }
+
+    /** {@inheritDoc} */
+    @AutoProfiling
     @Override public Change getChange(String href) {
         return loadIfAbsentV2(CHANGE_INFO_FULL, href, href1 -> {
             try {
@@ -1119,6 +1161,10 @@ public class IgnitePersistentTeamcity implements IAnalyticsEnabledTeamcity, ITea
         });
     }
 
+    public Executor getExecutor() {
+        return this.teamcity.getExecutor();
+    }
+
     /** {@inheritDoc} */
     @Override public void setExecutor(ExecutorService executor) {
         this.teamcity.setExecutor(executor);
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityConnection.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityConnection.java
index 0cb684b..892fd54 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityConnection.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityConnection.java
@@ -62,11 +62,13 @@ import org.apache.ignite.ci.tcmodel.conf.bt.BuildTypeFull;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.hist.Builds;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.Configurations;
 import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.tcmodel.user.User;
 import org.apache.ignite.ci.tcmodel.user.Users;
 import org.apache.ignite.ci.teamcity.pure.ITeamcityHttpConnection;
@@ -75,6 +77,7 @@ import org.apache.ignite.ci.util.HttpUtil;
 import org.apache.ignite.ci.util.UrlUtil;
 import org.apache.ignite.ci.util.XmlUtil;
 import org.apache.ignite.ci.util.ZipUtil;
+import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -122,6 +125,12 @@ public class IgniteTeamcityConnection implements ITeamcity {
     /** Build logger processing running. */
     private ConcurrentHashMap<Integer, CompletableFuture<LogCheckTask>> buildLogProcessingRunning = new ConcurrentHashMap<>();
 
+    private static int MAX_CFG_CNT = 500;
+
+    public Executor getExecutor() {
+        return executor;
+    }
+
     /** {@inheritDoc} */
     public void init(@Nullable String tcName) {
         this.tcName = tcName;
@@ -422,18 +431,14 @@ public class IgniteTeamcityConnection implements ITeamcity {
         return getJaxbUsingHref(href, Build.class);
     }
 
-    @Override
     @AutoProfiling
-    public ProblemOccurrences getProblems(Build build) {
-        if (build.problemOccurrences != null) {
-            ProblemOccurrences coll = getJaxbUsingHref(build.problemOccurrences.href, ProblemOccurrences.class);
+    @Override public ProblemOccurrences getProblems(BuildRef buildRef) {
+        ProblemOccurrences coll = getJaxbUsingHref("app/rest/latest/problemOccurrences?" +
+            "locator=build:(id:" + buildRef.getId() + ")", ProblemOccurrences.class);
 
-            coll.getProblemsNonNull().forEach(p -> p.buildRef = build);
+        coll.getProblemsNonNull().forEach(p -> p.buildRef = buildRef);
 
-            return coll;
-        }
-        else
-            return new ProblemOccurrences();
+        return coll;
     }
 
     /** {@inheritDoc} */
@@ -456,6 +461,31 @@ public class IgniteTeamcityConnection implements ITeamcity {
 
     /** {@inheritDoc} */
     @AutoProfiling
+    @Override public TestOccurrences getFailedTests(String href, int count, String normalizedBranch) {
+        return getTests(href + ",muted:false,status:FAILURE,count:" + count + "&fields=testOccurrence(id,name)", normalizedBranch);
+    }
+
+    /** {@inheritDoc} */
+    @AutoProfiling
+    @Override public CompletableFuture<TestRef> getTestRef(FullQueryParams key) {
+        return supplyAsync(() -> {
+            return getJaxbUsingHref("app/rest/latest/tests/name:" + key.getTestName(), TestRef.class);
+        }, executor);
+    }
+
+
+    /** {@inheritDoc} */
+    @AutoProfiling
+    @Override public Configurations getConfigurations(FullQueryParams key) {
+        Configurations configurations = getJaxbUsingHref("app/rest/latest/builds?locator=snapshotDependency:(to:(id:" + key.getBuildId()
+            + "),includeInitial:true),defaultFilter:false,count:" + MAX_CFG_CNT, Configurations.class);
+
+        return configurations.setBuild(key.getBuildId());
+    }
+
+
+    /** {@inheritDoc} */
+    @AutoProfiling
     @Override public Change getChange(String href) {
         return getJaxbUsingHref(href, Change.class);
     }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/db/DbMigrations.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/db/DbMigrations.java
index 1acb643..19f807b 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/db/DbMigrations.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/db/DbMigrations.java
@@ -42,6 +42,7 @@ import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.web.rest.Metrics;
 import org.apache.ignite.ci.web.rest.build.GetBuildTestFailures;
 import org.apache.ignite.ci.web.rest.pr.GetPrTestFailures;
@@ -113,7 +114,8 @@ public class DbMigrations {
         Cache<String, ProblemOccurrences> problemsCache,
         Cache<String, Statistics> buildStatCache,
         Cache<SuiteInBranch, Expirable<List<BuildRef>>> buildHistCache,
-        Cache<SuiteInBranch, Expirable<List<BuildRef>>> buildHistInFailedCache) {
+        Cache<SuiteInBranch, Expirable<List<BuildRef>>> buildHistInFailedCache,
+        Cache<String, TestRef> testRefsCache) {
 
         doneMigrations = doneMigrationsCache();
 
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Configurations.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Configurations.java
new file mode 100644
index 0000000..eb62c46
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Configurations.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.tcmodel.result;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+
+/**
+ */
+@XmlRootElement(name = "builds")
+public class Configurations {
+    /** */
+    @XmlElement(name = "build")
+    private List<BuildRef> builds;
+
+    /** */
+    public List<BuildRef> getBuilds() {
+        return builds == null ? new ArrayList<>() : builds;
+    }
+
+    /** BuildId which that configurations belong to. */
+    private Integer build;
+
+    /** */
+    public Configurations setBuild(Integer build) {
+        this.build = build;
+
+        return this;
+    }
+
+    /** */
+    public Integer getBuild() {
+        return build;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestOccurrence.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestOccurrence.java
index 43c91a7..9a3c807 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestOccurrence.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestOccurrence.java
@@ -20,6 +20,7 @@ package org.apache.ignite.ci.tcmodel.result.tests;
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
+import org.apache.ignite.ci.analysis.RunStat;
 
 /**
  * Test occurrence. Can be provided by build as list of occurrences.
@@ -98,4 +99,11 @@ public class TestOccurrence {
 
         return this;
     }
+
+    /**
+     * @return BuildId which that test occurrence belongs to
+     */
+    public Integer getBuildId() {
+        return RunStat.extractIdPrefixed(id, "build:(id:", ")");
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestRef.java
index 29d369b..76f6a13 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/tests/TestRef.java
@@ -18,11 +18,13 @@
 package org.apache.ignite.ci.tcmodel.result.tests;
 
 import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
 import org.apache.ignite.ci.tcmodel.result.AbstractRef;
 
 /**
  * Reference to particular test
  */
+@XmlRootElement(name = "test")
 public class TestRef extends AbstractRef {
     @XmlAttribute public Long id;
     @XmlAttribute public String name;
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
index d4a7bf6..d3cd968 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
@@ -34,6 +34,7 @@ import org.apache.ignite.ci.tcmodel.result.TestOccurrencesRef;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
 import org.apache.ignite.ci.util.TimeUtil;
 import org.apache.ignite.ci.web.IBackgroundUpdatable;
+import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 
 /**
  * Summary of build statistics.
@@ -132,7 +133,7 @@ public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpd
 
         for (BuildRef buildRef : builds)
             problemOccurrences.addAll(teamcity
-                .getProblems(teamcity.getBuild(buildRef.href))
+                .getProblems(buildRef)
                 .getProblemsNonNull());
 
         return problemOccurrences;
@@ -145,17 +146,12 @@ public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpd
      * @param buildRef Build reference.
      */
     private List<BuildRef> getSnapshotDependencies(@Nonnull final ITeamcity teamcity, BuildRef buildRef){
-        List<BuildRef> snapshotDependencies = new ArrayList<>();
+        FullQueryParams key = new FullQueryParams();
 
-        if (buildRef.isComposite()){
-            Build build = teamcity.getBuild(buildRef.href);
+        key.setServerId(teamcity.serverId());
+        key.setBuildId(buildRef.getId());
 
-            for (BuildRef snDep : build.getSnapshotDependenciesNonNull())
-                snapshotDependencies.addAll(getSnapshotDependencies(teamcity, snDep));
-        } else
-            snapshotDependencies.add(buildRef);
-
-        return snapshotDependencies;
+        return teamcity.getConfigurations(key).getBuilds();
     }
 
     /**
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
new file mode 100644
index 0000000..90b78f0
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model.hist;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.UncheckedIOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import javax.servlet.ServletContext;
+import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
+import org.apache.ignite.ci.ITcHelper;
+import org.apache.ignite.ci.ITeamcity;
+import org.apache.ignite.ci.tcbot.chain.BuildChainProcessor;
+import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrence;
+import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.CtxListener;
+import org.apache.ignite.ci.web.model.current.BuildStatisticsSummary;
+import org.apache.ignite.ci.web.rest.exception.ServiceUnauthorizedException;
+import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Builds History: includes statistic for every build and merged failed unmuted tests in specified time interval.
+ */
+public class BuildsHistory {
+    /** */
+    private String srvId;
+
+    /** */
+    private String projectId;
+
+    /** */
+    private String buildTypeId;
+
+    /** */
+    private String branchName;
+
+    /** */
+    private Date sinceDateFilter;
+
+    /** */
+    private Date untilDateFilter;
+
+    /** */
+    private Map<String, Set<String>> mergedTestsBySuites = new ConcurrentHashMap<>();
+
+    /** */
+    private boolean skipTests;
+
+    /** */
+    public List<BuildStatisticsSummary> buildsStatistics = new ArrayList<>();
+
+    /** */
+    public String mergedTestsJson;
+
+    /** */
+    private static final Logger logger = LoggerFactory.getLogger(BuildsHistory.class);
+
+    /** */
+    public void initialize(ICredentialsProv prov, ServletContext context) {
+        if (!prov.hasAccess(srvId))
+            throw ServiceUnauthorizedException.noCreds(srvId);
+
+        ITcHelper tcHelper = CtxListener.getTcHelper(context);
+
+        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
+
+        int[] finishedBuildsIds = teamcity.getBuildNumbersFromHistory(buildTypeId, branchName,
+            sinceDateFilter, untilDateFilter);
+
+        initStatistics(teamcity, finishedBuildsIds);
+
+        if (!skipTests) {
+            initFailedTests(teamcity, finishedBuildsIds);
+        }
+
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        try {
+            mergedTestsJson = objectMapper.writeValueAsString(mergedTestsBySuites);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** */
+    private void initStatistics(IAnalyticsEnabledTeamcity teamcity, int[] buildIds) {
+        List<Future<BuildStatisticsSummary>> buildStatiscsFutures = new ArrayList<>();
+
+        for (int buildId : buildIds) {
+            Future<BuildStatisticsSummary> buildFuture = CompletableFuture.supplyAsync(() -> {
+                BuildStatisticsSummary buildsStatistic = new BuildStatisticsSummary(buildId);
+
+                buildsStatistic.initialize(teamcity);
+
+                return buildsStatistic;
+
+            }, teamcity.getExecutor());
+
+            buildStatiscsFutures.add(buildFuture);
+        }
+
+        buildStatiscsFutures.forEach(new Consumer<Future<BuildStatisticsSummary>>() {
+            @Override public void accept(Future<BuildStatisticsSummary> v) {
+                try {
+                    BuildStatisticsSummary buildsStatistic = v.get();
+
+                    if (buildsStatistic != null && !buildsStatistic.isFakeStub)
+                        buildsStatistics.add(buildsStatistic);
+                }
+                catch (ExecutionException e) {
+                    if (e.getCause() instanceof UncheckedIOException)
+                        logger.error(e.getStackTrace().toString());
+
+                    else
+                        throw new RuntimeException(e);
+                }
+                catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+    }
+
+    /** */
+    private Map<Integer, String> getConfigurations(ITeamcity teamcity, int buildId) {
+        Map<Integer, String> configurations = new HashMap<>();
+
+        FullQueryParams key = new FullQueryParams();
+
+        key.setServerId(teamcity.serverId());
+        key.setBuildId(buildId);
+
+        teamcity.getConfigurations(key).getBuilds().forEach(buildRef -> {
+            Integer id = buildRef.getId();
+
+            String configurationName = buildRef.buildTypeId;
+
+            if (id != null && configurationName != null)
+                configurations.put(id, configurationName);
+        });
+
+        return configurations;
+    }
+
+    /** */
+    private void initFailedTests(IAnalyticsEnabledTeamcity teamcity, int[] buildIds) {
+        List<Future<Void>> buildProcessorFutures = new ArrayList<>();
+
+        for (int buildId : buildIds) {
+            Future<Void> buildFuture = CompletableFuture.supplyAsync(() -> {
+                Map<Integer, String> configurations = getConfigurations(teamcity, buildId);
+
+                Build build = teamcity.getBuild(teamcity.getBuildHrefById(buildId));
+
+                TestOccurrences testOccurrences = teamcity.getFailedTests(build.testOccurrences.href,
+                    build.testOccurrences.failed, BuildChainProcessor.normalizeBranch(build.branchName));
+
+                for (TestOccurrence testOccurrence : testOccurrences.getTests()) {
+                    String configurationName = configurations.get(testOccurrence.getBuildId());
+
+                    if(configurationName == null)
+                        continue;
+
+                    Set<String> tests = mergedTestsBySuites.computeIfAbsent(configurationName,
+                        k -> new HashSet<>());
+
+                    if (!tests.add(testOccurrence.getName()))
+                        continue;
+
+                    FullQueryParams key = new FullQueryParams();
+
+                    key.setServerId(srvId);
+                    key.setProjectId(projectId);
+                    key.setTestName(testOccurrence.getName());
+                    key.setSuiteId(configurationName);
+
+                    teamcity.getTestRef(key);
+                }
+
+                return null;
+            }, teamcity.getExecutor());
+
+            buildProcessorFutures.add(buildFuture);
+        }
+
+        buildProcessorFutures.forEach(v -> {
+            try {
+                v.get();
+            } catch (ExecutionException e) {
+                if (e.getCause() instanceof  UncheckedIOException)
+                    logger.error(e.getStackTrace().toString());
+
+                else
+                    throw new RuntimeException(e);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    /** */
+    public BuildsHistory(Builder builder) {
+        this.skipTests = builder.skipTests;
+        this.srvId = builder.srvId;
+        this.buildTypeId = builder.buildTypeId;
+        this.branchName = builder.branchName;
+        this.sinceDateFilter = builder.sinceDate;
+        this.untilDateFilter = builder.untilDate;
+        this.projectId = builder.projectId;
+    }
+
+    /** */
+    public static class Builder {
+        /** */
+        private boolean skipTests = false;
+
+        /** */
+        private String projectId = "IgniteTests24Java8";
+
+        /** */
+        private String srvId = "apache";
+
+        /** */
+        private String buildTypeId = "IgniteTests24Java8_RunAll";
+
+        /** */
+        private String branchName = "refs/heads/master";
+
+        /** */
+        private Date sinceDate = null;
+
+        /** */
+        private Date untilDate = null;
+
+        /** */
+        private DateFormat dateFormat = new SimpleDateFormat("ddMMyyyyHHmmss");
+
+        /** */
+        public Builder server(String srvId) {
+            if (!isNullOrEmpty(srvId))
+                this.srvId = srvId;
+
+            return this;
+        }
+
+        /** */
+        public Builder buildType(String buildType) {
+            if (!isNullOrEmpty(buildType))
+                this.buildTypeId = buildType;
+
+            return this;
+        }
+
+        /** */
+        public Builder project(String projectId) {
+            if (!isNullOrEmpty(projectId))
+                this.projectId = projectId;
+
+            return this;
+        }
+
+        /** */
+        public Builder branch(String branchName) {
+            if (!isNullOrEmpty(branchName))
+                this.branchName = branchName;
+
+            return this;
+        }
+
+        /** */
+        public Builder sinceDate(String sinceDate) throws ParseException {
+            if (!isNullOrEmpty(sinceDate))
+                this.sinceDate = dateFormat.parse(sinceDate);
+
+            return this;
+        }
+
+        /** */
+        public Builder untilDate(String untilDate) throws ParseException {
+            if (!isNullOrEmpty(untilDate))
+                this.untilDate = dateFormat.parse(untilDate);
+
+            return this;
+        }
+
+        /** */
+        public Builder skipTests() {
+            this.skipTests = true;
+
+            return this;
+        }
+
+
+        /** */
+        public BuildsHistory build() {
+            return new BuildsHistory(this);
+        }
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
index 15692c8..fdfe218 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
@@ -17,6 +17,9 @@
 
 package org.apache.ignite.ci.web.rest.build;
 
+import java.text.ParseException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import com.google.inject.Injector;
 import org.apache.ignite.ci.tcbot.chain.BuildChainProcessor;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
@@ -26,10 +29,11 @@ import org.apache.ignite.ci.analysis.FullChainRunCtx;
 import org.apache.ignite.ci.analysis.mode.LatestRebuildMode;
 import org.apache.ignite.ci.analysis.mode.ProcessLogsMode;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.hist.BuildsHistory;
 import org.apache.ignite.ci.web.BackgroundUpdater;
 import org.apache.ignite.ci.web.CtxListener;
-import org.apache.ignite.ci.web.model.current.BuildStatisticsSummary;
 import org.apache.ignite.ci.web.model.current.ChainAtServerCurrentStatus;
 import org.apache.ignite.ci.web.model.current.TestFailuresSummary;
 import org.apache.ignite.ci.web.model.current.UpdateInfo;
@@ -46,16 +50,8 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
-import java.util.Date;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-
-import static com.google.common.base.Strings.isNullOrEmpty;
 
 @Path(GetBuildTestFailures.BUILD)
 @Produces(MediaType.APPLICATION_JSON)
@@ -159,75 +155,65 @@ public class GetBuildTestFailures {
     }
 
     @GET
-    @Path("history")
-    public List<BuildStatisticsSummary> getBuildsHistory(
+    @Produces(MediaType.TEXT_PLAIN)
+    @Path("testRef")
+    public String getTestRef(
+        @NotNull @QueryParam("testName") String name,
+        @NotNull @QueryParam("suiteName") String suiteName,
         @Nullable @QueryParam("server") String srv,
-        @Nullable @QueryParam("buildType") String buildType,
-        @Nullable @QueryParam("branch") String branch,
-        @Nullable @QueryParam("sinceDate") String sinceDate,
-        @Nullable @QueryParam("untilDate") String untilDate)
-        throws ServiceUnauthorizedException {
-        String srvId = isNullOrEmpty(srv) ? "apache" : srv;
-        String buildTypeId = isNullOrEmpty(buildType) ? "IgniteTests24Java8_RunAll" : buildType;
-        String branchName = isNullOrEmpty(branch) ? "refs/heads/master" : branch;
-        Date sinceDateFilter = isNullOrEmpty(sinceDate) ? null : dateParse(sinceDate);
-        Date untilDateFilter = isNullOrEmpty(untilDate) ? null : dateParse(untilDate);
-
-        final BackgroundUpdater updater = CtxListener.getBackgroundUpdater(ctx);
-
-        final ITcHelper tcHelper = CtxListener.getTcHelper(ctx);
+        @Nullable @QueryParam("projectId") String projectId)
+        throws InterruptedException, ExecutionException, ServiceUnauthorizedException {
+        final ITcHelper helper = CtxListener.getTcHelper(ctx);
 
         final ICredentialsProv prov = ICredentialsProv.get(req);
 
-        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
-
-        int[] finishedBuilds = teamcity.getBuildNumbersFromHistory(buildTypeId, branchName, sinceDateFilter, untilDateFilter);
-
-        List<BuildStatisticsSummary> buildsStatistics = new ArrayList<>();
+        String project = projectId == null ? "IgniteTests24Java8" : projectId;
 
-        for (int i = 0; i < finishedBuilds.length; i++) {
-            int buildId = finishedBuilds[i];
+        String srvId = srv == null ? "apache" : srv;
 
-            FullQueryParams param = new FullQueryParams();
-            param.setBuildId(buildId);
-            param.setBranch(branchName);
-            param.setServerId(srvId);
+        if (!prov.hasAccess(srvId))
+            throw ServiceUnauthorizedException.noCreds(srvId);
 
-            BuildStatisticsSummary buildsStatistic = updater.get(
-                BUILDS_STATISTICS_SUMMARY_CACHE_NAME, prov, param,
-                (k) -> getBuildStatisticsSummaryNoCache(srvId, buildId), false);
+        IAnalyticsEnabledTeamcity teamcity = helper.server(srvId, prov);
 
-            if (!buildsStatistic.isFakeStub)
-                buildsStatistics.add(buildsStatistic);
-        }
+        FullQueryParams key = new FullQueryParams();
 
-        return buildsStatistics;
-    }
+        key.setTestName(name);
+        key.setProjectId(project);
+        key.setServerId(srvId);
+        key.setSuiteId(suiteName);
 
-    private Date dateParse(String date){
-        DateFormat dateFormat = new SimpleDateFormat("ddMMyyyyHHmmss");
+        CompletableFuture<TestRef> ref = teamcity.getTestRef(key);
 
-        try {
-            return dateFormat.parse(date);
-        }
-        catch (ParseException e) {
-            return null;
-        }
+        return ref.isDone() && !ref.isCompletedExceptionally() ? teamcity.host() + "project.html?"
+            + "projectId=" + project
+            + "&testNameId=" + ref.get().id
+            + "&tab=testDetails" : null;
     }
 
-    private BuildStatisticsSummary getBuildStatisticsSummaryNoCache(String server, int buildId) {
-        String srvId = isNullOrEmpty(server) ? "apache" : server;
-
-        final ITcHelper tcHelper = CtxListener.getTcHelper(ctx);
-
-        final ICredentialsProv creds = ICredentialsProv.get(req);
+    @GET
+    @Path("history")
+    public BuildsHistory getBuildsHistory(
+        @Nullable @QueryParam("server") String srvId,
+        @Nullable @QueryParam("buildType") String buildType,
+        @Nullable @QueryParam("branch") String branch,
+        @Nullable @QueryParam("sinceDate") String sinceDate,
+        @Nullable @QueryParam("untilDate") String untilDate,
+        @Nullable @QueryParam("skipTests") String skipTests)  throws ParseException {
+        BuildsHistory.Builder builder = new BuildsHistory.Builder()
+            .branch(branch)
+            .server(srvId)
+            .buildType(buildType)
+            .sinceDate(sinceDate)
+            .untilDate(untilDate);
 
-        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, creds);
+        if (Boolean.valueOf(skipTests))
+            builder.skipTests();
 
-        BuildStatisticsSummary stat = new BuildStatisticsSummary(buildId);
+        BuildsHistory buildsHistory = builder.build();
 
-        stat.initialize(teamcity);
+        buildsHistory.initialize(ICredentialsProv.get(req), ctx);
 
-        return stat;
+        return buildsHistory;
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/parms/FullQueryParams.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/parms/FullQueryParams.java
index 1026fb4..47fac6f 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/parms/FullQueryParams.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/parms/FullQueryParams.java
@@ -60,6 +60,12 @@ public class FullQueryParams {
     /** TC identified base branch: null means the same as &lt;default>, master. For not tracked branches. */
     @Nullable @QueryParam("baseBranchForTc") private String baseBranchForTc;
 
+    /** TC project identifier */
+    @Nullable @QueryParam("projectId") String projectId;
+
+    /** TC test name  */
+    @Nullable @QueryParam("testName") String testName;
+
     public FullQueryParams() {
     }
 
@@ -97,6 +103,14 @@ public class FullQueryParams {
         return count;
     }
 
+    @Nullable public String getTestName() {
+        return testName;
+    }
+
+    @Nullable public String getProjectId() {
+        return projectId ;
+    }
+
     @Nullable public Boolean getCheckAllLogs() {
         return checkAllLogs;
     }
@@ -118,13 +132,15 @@ public class FullQueryParams {
             Objects.equal(count, param.count) &&
             Objects.equal(checkAllLogs, param.checkAllLogs) &&
             Objects.equal(buildId, param.buildId) &&
+            Objects.equal(projectId, param.projectId) &&
+            Objects.equal(testName, param.testName) &&
             Objects.equal(baseBranchForTc, param.baseBranchForTc);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
         return Objects.hashCode(branch, serverId, suiteId, branchForTc, action, count, checkAllLogs, buildId,
-            baseBranchForTc);
+            baseBranchForTc, testName, projectId);
     }
 
     public void setBranch(@Nullable String branch) {
@@ -146,13 +162,27 @@ public class FullQueryParams {
             .add("checkAllLogs", checkAllLogs)
             .add("buildId", buildId)
             .add("baseBranchForTc", baseBranchForTc)
+            .add("projectId", projectId)
+            .add("testName", testName)
             .toString();
     }
 
+    public void setSuiteId(@Nonnull String suiteId) {
+        this.suiteId = suiteId;
+    }
+
     public void setCount(@Nullable int count) {
         this.count = count;
     }
 
+    public void setProjectId(@Nullable String projectId) {
+        this.projectId = projectId;
+    }
+
+    public void setTestName(@Nullable String testName) {
+        this.testName = testName;
+    }
+
     public void setBuildId(Integer buildId) {
         this.buildId = buildId;
     }
diff --git a/ignite-tc-helper-web/src/main/webapp/comparison.html b/ignite-tc-helper-web/src/main/webapp/comparison.html
index fd62a47..392e71c 100644
--- a/ignite-tc-helper-web/src/main/webapp/comparison.html
+++ b/ignite-tc-helper-web/src/main/webapp/comparison.html
@@ -17,7 +17,7 @@
 <body>
 <br>
 <br>
-<table class="compare"  width="100%">
+<table style="table-layout: fixed" class="compare">
     <tr>
         <th class="section"  width="15%">DATE INTERVAL</th>
         <th width="5%"></th>
@@ -148,15 +148,64 @@
         <td class="t2 data2" id="RunsCount2"></td>
     </tr>
 </table><br>
+<table style="table-layout: fixed" id="testsTable" class="testsTable">
+    <tbody>
+    <tr>
+        <th class="failedTestsHeader" width="15%">FAILED TESTS</th>
+        <th width="5%">
+            <label class="switch">
+                <input type="checkbox" onclick="toggleTests()">
+                <span class="slider"></span>
+            </label>
+        </th>
+        <th width="40%"></th>
+        <th width="40%"></th>
+        </tr>
+    </tbody>
+</table>
+
+<div id="myModal" class="modal">
+
+    <!-- Modal content -->
+    <div class="modal-content">
+        <span class="close">&times;</span>
+        <p id="tooltipText"></p>
+    </div>
+
+</div>
+
 <div id="version"></div>
 <script>
-    let oneWeekAgo = new Date(),
-        twoWeekAgo = new Date(),
-        g_updTimer = null;
+    const TESTS_TABLE = '#testsTable';
+    const SKIP_TESTS = 'skipTests=true';
+    let oneWeekAgo = new Date();
+    let twoWeekAgo = new Date();
+    let g_updTimer = null;
+    let testsTrigger = false;
+
+    /** Structure for storing tests by suites parsed response for every date interval. */
+    let mergedTestsResults = {1 : {}, 2 : {} };
+
 
     oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
     twoWeekAgo.setDate(twoWeekAgo.getDate() - 14);
 
+    let dateIntervals = {1: {start: moment(oneWeekAgo), end: moment()},
+        2: {start: moment(twoWeekAgo), end: moment(oneWeekAgo)}};
+
+    function showTooltip(message) {
+        $("#tooltipText").html(message);
+        modal.show();
+    }
+
+    function toggleTests() {
+        testsTrigger = !testsTrigger;
+
+        loadData(1, dateIntervals[1].start, dateIntervals[1].end, testsTrigger);
+
+        loadData(2, dateIntervals[2].start, dateIntervals[2].end, testsTrigger);
+    }
+
     const parseTime = d3.timeParse("%d-%m-%YT%H:%M:%S"),
         formatTime = d3.timeFormat("%d-%m-%Y %H:%M:%S"),
         formatMillisecond = d3.timeFormat(".%L"),
@@ -224,6 +273,130 @@
             return parseFloat(stringMedian);
     }
 
+    var changeDisplay = function(id) {
+        $('*[id=' + id + ']').each(function() {
+            $(this).toggle();
+        });
+    }
+
+    function mergeSuits(results) {
+        let mergedSuites = new Set();
+
+        for (let key of Object.keys(results)) {
+            for (let suite of Object.keys(results[key]))
+                mergedSuites.add(suite);
+        }
+
+        return Array.from(mergedSuites);
+    }
+
+    function printTests(results) {
+        $(TESTS_TABLE + " tr:not(:first-child)").remove();
+
+        let markedRow = true;
+
+        for (let suite of mergeSuits(results).sort()) {
+            let suiteName = suite.split('_').filter((value, index) => index != 0).join('_');
+            let testsCntCells = '';
+            let testsCells = '';
+
+            for (let key of Object.keys(results)) {
+                let obj = results[key];
+                let testLength = !obj.hasOwnProperty(suite) || obj[suite].length == 0 ?
+                    '' : obj[suite].length;
+
+                testsCntCells = testsCntCells + '<td class="testsCntCell"><p id="' + suite + '">' + testLength + '</p></td>';
+
+                testsCells = testsCells + '<td class="testsCell">' + getSuiteTestsHtml(results, suite, key) + '</td>'
+            }
+
+            $(TESTS_TABLE + " > tbody:last-child").append('<tr class="testsCntRow" onclick="changeDisplay(\'' + suite + '\')">' +
+                '<td class = "suiteCell">' + suiteName + '</td>' +
+                '<td></td>' + testsCntCells + '</tr>');
+
+            $(TESTS_TABLE + " > tbody:last-child").append('<tr class="testsRow" id="' + suite + '">' +
+                '<td class="testsSuiteCell" onclick="changeDisplay(\'' + suite + '\')"></td>' +
+                '<td></td>' + testsCells + '</tr>');
+        }
+    }
+
+    function generateCompareTestsResults(results) {
+        let compareTestsResults = {};
+
+        for (let key of  Object.keys(results)) {
+            let uniqueObj = {};
+
+            for (let suite of  Object.keys(results[key])) {
+                let allTests = [];
+
+                for (let key2 of Object.keys(results)) {
+                    if (key == key2)
+                        continue;
+
+                    allTests = allTests.concat(results[key2][suite]);
+                }
+
+                let uniqTests = results[key][suite].filter(function(test) {
+                    return allTests.indexOf(test) == -1;
+                });
+
+                if (uniqTests.length != 0)
+                    uniqueObj[suite] = uniqTests;
+            }
+
+            compareTestsResults[key] = uniqueObj;
+        }
+
+        return compareTestsResults;
+    }
+
+    function getSuiteTestsHtml(results, suite, key) {
+        if (!results[key].hasOwnProperty(suite) || results[key][suite].length == 0)
+            return '';
+
+        let res = '<body><div  id="' + suite + key + '"style="cursor: default; margin-left: 10px;">';
+
+        for (let test of results[key][suite].sort()) {
+            let list = test.toString().split(".");
+
+            if (list.length < 2)
+                list = test.toString().split(":");
+
+            let testName = list.pop();
+            let testClass = list.pop();
+
+            res += '<p align="left" title="' + test + '">' + testClass + '.' + testName +
+                '<a href="#" onclick="getTestRef(\'' + test + '\'' + ',\'' + suite + '\'); return false;">' +
+                ' &gt&gt</a>' + '</p>'
+        }
+
+        res += '</div></body>';
+
+        return res;
+    }
+
+    function getTestRef(testName, suite) {
+        let res = null;
+
+        $.ajax({
+                async: false,
+                url: 'rest/build/testRef?testName=' + testName + '&suiteName=' + suite,
+                success: function (result) {
+                    res = result;
+                },
+                error: showErrInLoadStatus
+            }
+        );
+
+        if (res == null) {
+            showTooltip('Link to TC test history is not ready yet. Try later.');
+
+            return;
+        }
+
+        window.open(res);
+    }
+
     function printStatistics(num, map, sinceDate, untilDate) {
         clearBackgroundFromAllDataCells();
         clearGraphs(num);
@@ -325,8 +498,8 @@
     }
 
     $(document).ready(function() {
-        loadData(1, moment(oneWeekAgo), moment());
-        loadData(2, moment(twoWeekAgo), moment(oneWeekAgo));
+        loadData(1, moment(oneWeekAgo), moment(), testsTrigger);
+        loadData(2, moment(twoWeekAgo), moment(oneWeekAgo), testsTrigger);
 
         $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
 
@@ -353,6 +526,7 @@
     
     function fillAllDataCells(num, message) {
         $('.data' + num).html(message);
+        $('.testsCntCell').html(message);
     }
 
     function clearGraphs(num) {
@@ -375,16 +549,32 @@
         $('#showInfo').css('display', '');
     }
 
-    function loadData(num, sinceDate, untilDate) {
+    function loadData(num, sinceDate, untilDate, testsTrigger) {
         loadGif(num);
-        $.ajax(
-            {
-                url: 'rest/build/history?sinceDate=' + sinceDate.format("DDMMYYYY") +
-                '000001&untilDate=' + untilDate.format("DDMMYYYY") + '235959',
+
+        mergedTestsResults[num] = {};
+
+        let url = 'rest/build/history?sinceDate=' + sinceDate.format("DDMMYYYY") +
+            '000001&untilDate=' + untilDate.format("DDMMYYYY") + '235959';
+
+        if (!testsTrigger)
+            url = url + '&' + SKIP_TESTS;
+
+        $.ajax({
+                url: url,
                 success: function (result) {
-                    printStatistics(num, result, sinceDate, untilDate);
+                    printStatistics(num, result.buildsStatistics, sinceDate, untilDate);
+
+                    try {
+                        mergedTestsResults[num] = JSON.parse(result.mergedTestsJson);
+                    } catch (e) {
+                        printImportantMessage(num, "#ff0000", "Invalid server response. Unable to parse JSON");
+                    }
+
+                    printTests(generateCompareTestsResults(mergedTestsResults));
                 },
-                error: showErrInLoadStatus
+                error: showErrInLoadStatus,
+                timeout: 1800000
             }
         );
     }
@@ -392,14 +582,22 @@
     $(function() {
         $('input[name="daterange1"]').daterangepicker(
             dateRangePickerParam(oneWeekAgo, new Date()), function (start, end, label) {
-                loadData(1, start, end);
+                dateIntervals[1].start = start;
+
+                dateIntervals[1].end = end;
+
+                loadData(1, start, end, testsTrigger);
             });
     });
 
     $(function() {
         $('input[name="daterange2"]').daterangepicker(
             dateRangePickerParam(twoWeekAgo, oneWeekAgo), function (start, end, label) {
-                loadData(2, start, end);
+                dateIntervals[2].start = start;
+
+                dateIntervals[2].end = end;
+
+                loadData(2, start, end, testsTrigger);
             });
     });
 
@@ -545,6 +743,18 @@
             }
         });
     }
+
+    // Get the modal
+    var modal = $('#myModal');
+
+    // Get the <span> element that closes the modal
+    var span = $(".close");
+
+    // When the user clicks on <span> (x), close the modal
+    span.click(function() {
+        modal.hide();
+    })
+
 </script>
 <div style="visibility:hidden"><div id="modalDialog" title="Information"></div></div>
 </body>
diff --git a/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css b/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
index ca6d537..c5aa999 100644
--- a/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
+++ b/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
@@ -249,6 +249,52 @@ form li:after
 	background-color: #fafaff;
 }
 
+.testsCntCell {
+	cursor: pointer;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.testsCell {
+	cursor: default;
+	word-wrap: break-word;
+	vertical-align: top;
+}
+
+.testsRow {
+	display: none;
+}
+
+.suiteCell {
+	cursor: default;
+	vertical-align: middle;
+	padding: 15px;
+}
+
+.testsSuiteCell {
+	cursor: pointer;
+}
+
+.testsCntRow {
+	padding: 10px
+}
+
+.testsTable {
+	width: 96%;
+	border-collapse: collapse;
+	margin-left: 2%;
+	margin-right: 2%;
+}
+
+.testsTable tr:nth-child(4n + 2) {
+	background-color: #fafaff;
+}
+
+.failedTestsHeader {
+	vertical-align: middle;
+	text-align: left;
+	padding: 5px
+}
 
 td.details-control {
 	//background: url('../resources/details_open.png') no-repeat center center;
@@ -306,3 +352,85 @@ div.tooltip {
 	height: auto;
 }
 
+.switch {
+	position: relative;
+	display: inline-block;
+	width: 60px;
+	height: 34px;
+}
+
+.switch input {display:none;}
+
+.slider {
+	position: absolute;
+	cursor: pointer;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: #ccc;
+	-webkit-transition: .4s;
+	transition: .4s;
+}
+
+.slider:before {
+	position: absolute;
+	content: "";
+	height: 26px;
+	width: 26px;
+	left: 4px;
+	bottom: 4px;
+	background-color: white;
+	-webkit-transition: .4s;
+	transition: .4s;
+}
+
+input:checked + .slider {
+	background-color: #12AD5E;
+}
+
+input:focus + .slider {
+	box-shadow: 0 0 1px #12AD5E;
+}
+
+input:checked + .slider:before {
+	-webkit-transform: translateX(26px);
+	-ms-transform: translateX(26px);
+	transform: translateX(26px);
+}
+
+.modal {
+    display: none;
+    position: fixed;
+    z-index: 1;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+}
+
+.modal-content {
+    margin-top: 100px;
+    margin-left: auto;
+    margin-right: auto;
+    text-align: center;
+    background-color: #fefefe;
+    padding: 10px;
+    border: 5px solid #12AD5E;
+    width: 30%;
+}
+
+.close {
+    color: #12AD5E;
+    float: right;
+    font-size: 28px;
+    font-weight: bold;
+}
+
+.close:hover,
+.close:focus {
+    color: black;
+    text-decoration: none;
+    cursor: pointer;
+}
+