You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ignite.apache.org by GitBox <gi...@apache.org> on 2018/10/24 12:15:54 UTC

[GitHub] asfgit closed pull request #36: IGNITE-9645 [TC Bot] Add comparison of failed tests lists in two date intervals

asfgit closed pull request #36: IGNITE-9645 [TC Bot] Add comparison of failed tests lists in two date intervals
URL: https://github.com/apache/ignite-teamcity-bot/pull/36
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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.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.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 @@ default Build getBuild(int id) {
      * @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 @@ default Build getBuild(int id) {
 
     ChangesList getChangesList(String href);
 
+    CompletableFuture<TestRef> getTestRef(FullQueryParams key);
+
+    Configurations getConfigurations(FullQueryParams key);
+
     /**
      * List of build's related issues.
      *
@@ -251,6 +261,8 @@ default SingleBuildRunCtx loadTestsAndProblems(@Nonnull Build build, @Deprecated
 
     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.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.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 @@
     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 @@
      */
     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 @@
                 buildProblemsCache(),
                 buildStatisticsCache(),
                 buildHistCache(),
-                buildHistIncFailedCache());
+                buildHistIncFailedCache(),
+                testRefsCache());
     }
 
     @Override
@@ -220,6 +230,20 @@ public User getUserByUsername(String username) {
         return getOrCreateCacheV2(ignCacheNme(TEST_FULL));
     }
 
+    /**
+     * @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.
      */
@@ -766,26 +790,20 @@ private Build realLoadBuild(String href1) {
 
     /** {@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 @@ private void registerCriticalBuildProblemInStat(Build build, ProblemOccurrences
             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);
@@ -874,6 +906,16 @@ private void addTestOccurrencesToStat(TestOccurrences val) {
             });
     }
 
+    /** {@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) {
@@ -1119,6 +1161,10 @@ private void migrateTestOneOcurrToAddToLatest(TestOccurrence next) {
         });
     }
 
+    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.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.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 @@
     /** 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 Build getBuild(String href) {
         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} */
@@ -454,6 +459,31 @@ public ProblemOccurrences getProblems(Build build) {
         return supplyAsync(() -> getJaxbUsingHref(href, TestOccurrenceFull.class), executor);
     }
 
+    /** {@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) {
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.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 void dataMigration(
         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 @@
 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 TestOccurrence setStatus(String status) {
 
         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.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 @@ private long getOomeProblemCount(String buildTypeId) {
 
         for (BuildRef buildRef : builds)
             problemOccurrences.addAll(teamcity
-                .getProblems(teamcity.getBuild(buildRef.href))
+                .getProblems(buildRef)
                 .getProblemsNonNull());
 
         return problemOccurrences;
@@ -145,17 +146,12 @@ private long getOomeProblemCount(String buildTypeId) {
      * @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..8b2c46e
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
@@ -0,0 +1,329 @@
+/*
+ * 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 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(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.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.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 TestFailuresSummary getBuildTestFails(
     }
 
     @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 @@
     /** 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 FullQueryParams(String serverId, String suiteId, String branchForTc, Stri
         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 FullQueryParams(String serverId, String suiteId, String branchForTc, Stri
             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 void setCheckAllLogs(@Nullable Boolean checkAllLogs) {
             .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;
+}
+


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services