You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ir...@apache.org on 2020/06/16 11:54:37 UTC

[ignite-teamcity-bot] branch master updated: Highlight newly added tests in TC bot visa #166

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5079361  Highlight newly added tests in TC bot visa #166
5079361 is described below

commit 5079361e0c762ef96be43f57be1c32ffd3912ff8
Author: sergeyuttsel <ut...@gmail.com>
AuthorDate: Tue Jun 16 14:54:26 2020 +0300

    Highlight newly added tests in TC bot visa #166
    
    Signed-off-by: Ivan Rakov <iv...@gmail.com>
---
 .../apache/ignite/ci/tcbot/TcBotWebAppModule.java  |  3 +
 .../tcbot/visa/TcBotTriggerAndSignOffService.java  | 84 +++++++++++++++++++++-
 .../src/main/webapp/js/testfails-2.2.js            | 49 ++++++++++++-
 .../ci/tcbot/chain/MockBasedTcBotModule.java       |  1 +
 .../tcbot/engine/chain/TestCompactedMult.java      | 10 ++-
 .../ignite/tcbot/engine/pr/PrChainsProcessor.java  | 84 +++++++++++++++++++++-
 .../apache/ignite/tcbot/engine/ui/DsChainUi.java   | 37 ++++++++++
 .../tcbot/engine/ui/ShortSuiteNewTestsUi.java      | 46 ++++++++++++
 .../apache/ignite/tcbot/engine/ui/ShortTestUi.java | 70 ++++++++++++++++++
 9 files changed, 376 insertions(+), 8 deletions(-)

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/TcBotWebAppModule.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/TcBotWebAppModule.java
index c4affe0..969e6b6 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/TcBotWebAppModule.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/TcBotWebAppModule.java
@@ -44,6 +44,8 @@ import org.apache.ignite.tcbot.notify.TcBotNotificationsModule;
 import org.apache.ignite.tcbot.persistence.TcBotPersistenceModule;
 import org.apache.ignite.tcbot.persistence.scheduler.SchedulerModule;
 import org.apache.ignite.tcignited.TeamcityIgnitedModule;
+import org.apache.ignite.tcservice.ITeamcityConn;
+import org.apache.ignite.tcservice.TeamcityServiceConnection;
 
 /**
  *
@@ -70,6 +72,7 @@ public class TcBotWebAppModule extends AbstractModule {
             }
         });
 
+        bind(ITeamcityConn.class).toInstance(new TeamcityServiceConnection());
         bind(TcUpdatePool.class).in(new SingletonScope());
         bind(IssueDetector.class).in(new SingletonScope());
         bind(ObserverTask.class).in(new SingletonScope());
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
index 8a05a79..4898b19 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
@@ -69,7 +69,9 @@ import org.apache.ignite.tcbot.engine.conf.ITcBotConfig;
 import org.apache.ignite.tcbot.engine.pr.BranchTicketMatcher;
 import org.apache.ignite.tcbot.engine.pr.PrChainsProcessor;
 import org.apache.ignite.tcbot.engine.ui.ShortSuiteUi;
+import org.apache.ignite.tcbot.engine.ui.ShortSuiteNewTestsUi;
 import org.apache.ignite.tcbot.engine.ui.ShortTestFailureUi;
+import org.apache.ignite.tcbot.engine.ui.ShortTestUi;
 import org.apache.ignite.tcbot.persistence.IStringCompactor;
 import org.apache.ignite.tcignited.ITeamcityIgnited;
 import org.apache.ignite.tcignited.ITeamcityIgnitedProvider;
@@ -829,13 +831,17 @@ public class TcBotTriggerAndSignOffService {
                 SyncMode.RELOAD_QUEUED,
                 baseBranch);
 
+            List<ShortSuiteNewTestsUi> newTestsStatuses = prChainsProcessor.getNewTestsSuitesStatuses(buildTypeId, build.branchName, srvCodeOrAlias, prov,
+                SyncMode.RELOAD_QUEUED,
+                baseBranch);
+
             if (suitesStatuses == null)
                 return new Visa("JIRA wasn't commented - no finished builds to analyze." +
                     " Check builds availabiliy for branch: " + build.branchName + "/" + baseBranch);
 
             blockers = suitesStatuses.stream().mapToInt(ShortSuiteUi::totalBlockers).sum();
 
-            String comment = generateJiraComment(suitesStatuses, build.webUrl, buildTypeId, tcIgnited, blockers, build.branchName, baseBranch);
+            String comment = generateJiraComment(suitesStatuses, newTestsStatuses, build.webUrl, buildTypeId, tcIgnited, blockers, build.branchName, baseBranch);
 
 
             res = objMapper.readValue(jira.postJiraComment(ticket, comment), JiraCommentResponse.class);
@@ -862,7 +868,7 @@ public class TcBotTriggerAndSignOffService {
      * @param baseBranch TC Base branch used for comment
      * @return Comment, which should be sent to the JIRA ticket.
      */
-    private String generateJiraComment(List<ShortSuiteUi> suites, String webUrl, String buildTypeId,
+    private String generateJiraComment(List<ShortSuiteUi> suites, List<ShortSuiteNewTestsUi> newTestsStatuses, String webUrl, String buildTypeId,
         ITeamcityIgnited tcIgnited, int blockers, String branchName, String baseBranch) {
         BuildTypeRefCompacted bt = tcIgnited.getBuildTypeRef(buildTypeId);
 
@@ -910,6 +916,61 @@ public class TcBotTriggerAndSignOffService {
             res.append("\\n");
         }
 
+        StringBuilder newTests = new StringBuilder();
+
+        int newTestsCount = 0;
+
+        int failedNewTestsCount = 0;
+
+        for (ShortSuiteNewTestsUi suite : newTestsStatuses) {
+            newTests.append("{color:#00008b}");
+
+            newTests.append(jiraEscText(suite.name)).append("{color}");
+
+            int totalNewTests = suite.tests.size();
+            newTests.append(" [tests ").append(totalNewTests);
+
+            int cnt = 0;
+
+            newTestsCount += suite.tests().size();
+
+            newTests.append("]\\n");
+
+            for (ShortTestUi test : suite.tests()) {
+                String testColor;
+                if (test.status)
+                    testColor = "#013220";
+                else {
+                    testColor = "#8b0000";
+                    failedNewTestsCount++;
+                }
+
+                newTests.append("* ");
+
+                newTests.append(String.format("{color:%s}", testColor));
+
+                if (test.suiteName != null && test.testName != null)
+                    newTests.append(jiraEscText(test.suiteName)).append(": ").append(jiraEscText(test.testName));
+                else
+                    newTests.append(jiraEscText(test.name));
+
+                newTests.append(" - ").append(jiraEscText(test.status ? "PASSED" : "FAILED"));
+
+                newTests.append("{color}");
+
+                newTests.append("\\n");
+
+                cnt++;
+                if (cnt > 10) {
+                    newTests.append("... and ").append(totalNewTests - cnt).append(" tests blockers\\n");
+
+                    break;
+                }
+            }
+
+            newTests.append("\\n");
+        }
+
         String suiteNameForComment = jiraEscText(suiteNameUsedForVisa);
 
         String branchNameForComment = jiraEscText("Branch: [" + branchName + "] ");
@@ -929,7 +990,24 @@ public class TcBotTriggerAndSignOffService {
                 .append("borderStyle=dashed|borderColor=#ccc|titleBGColor=#D6F7C1}{panel}");
         }
 
-        res.append("\\n").append("[TeamCity *").append(suiteNameForComment).append("* Results|").append(webUrl).append(']');
+        if (newTests.length() > 0) {
+            String bgColor;
+            if (failedNewTestsCount > 0)
+                bgColor = "#F7D6C1";
+            else
+                bgColor = "#D6F7C1";
+            String hdrPanel = "{panel:title=" + branchVsBaseComment + ": New Tests (" + newTestsCount + ")|" +
+                "borderStyle=dashed|borderColor=#ccc|titleBGColor=" + bgColor + "}\\n";
+
+            newTests.insert(0, hdrPanel)
+                .append("{panel}");
+        }
+        else {
+            newTests.append("{panel:title=").append(branchVsBaseComment).append(": No new tests found!|")
+                .append("borderStyle=dashed|borderColor=#ccc|titleBGColor=#F7D6C1}{panel}");
+        }
+
+        res.append("\\n").append(newTests).append("\\n").append("[TeamCity *").append(suiteNameForComment).append("* Results|").append(webUrl).append(']');
 
         return xmlEscapeText(res.toString());
     }
diff --git a/ignite-tc-helper-web/src/main/webapp/js/testfails-2.2.js b/ignite-tc-helper-web/src/main/webapp/js/testfails-2.2.js
index 9475f30..e3c5ebc 100644
--- a/ignite-tc-helper-web/src/main/webapp/js/testfails-2.2.js
+++ b/ignite-tc-helper-web/src/main/webapp/js/testfails-2.2.js
@@ -83,11 +83,57 @@ function showChainResultsWithSettings(result, settings) {
         res += showChainCurrentStatusData(server, settings);
     }
 
+    res += "<tr bgcolor='#F5F5FF'><th colspan='4' class='table-title'><b>New Tests</b></th></tr>"
+
+    for (var i = 0; i < result.servers.length; i++) {
+        var newTests = result.servers[i].newTestsUi;
+        res += showNewTestsData(newTests, settings);
+    }
+
+    res += "<tr><td colspan='4'>&nbsp;</td></tr>";
+    res += "</table>";
+
     setTimeout(initMoreInfo, 100);
 
     return res;
 }
 
+/**
+ * @param chain - see org.apache.ignite.ci.web.model.current.ChainAtServerCurrentStatus Java Class.
+ * @param settings - see Settings JavaScript class.
+ */
+function showNewTestsData(chain, settings) {
+    var res = "";
+
+    newTestRows = "";
+
+    res += "<table style='width:100%'>";
+
+    for (var i = 0; i < chain.length; i++) {
+        var newTests = chain[i].tests;
+        for (var j = 0; j < newTests.length; j++) {
+            newTestsFounded = true
+            var newTest = newTests[j];
+            testColor = newTest.status ? "#013220" : "#8b0000";
+            newTestRows += "<tr style='color:" + testColor + "'>";
+            newTestRows += "<td colspan='2' width='10%'></td>";
+            newTestRows += "<td width='5%'>" + (newTest.status ? "PASSED" : "FAILED") + "</td>";
+            if (isDefinedAndFilled(newTest.suiteName) && isDefinedAndFilled(newTest.testName))
+                newTestRows += "<td width='75%'>" + newTest.suiteName + ": " + newTest.testName + "</td>";
+            else
+                newTestRows += "<td width='75%'>" + newTest.name + "</td>";
+            newTestRows += "<td colspan='2' width='10%'></td>";
+            newTestRows += "</tr>";
+        }
+    }
+
+    res += newTestRows !== "" ? newTestRows : "<tr><td colspan='2' width='10%'></td><td width='90%'>No new tests</td></tr>"
+
+    res += "</table>";
+
+    return res;
+
+}
 
 /**
  * @param chain - see org.apache.ignite.ci.web.model.current.ChainAtServerCurrentStatus Java Class.
@@ -325,9 +371,6 @@ function showChainCurrentStatusData(chain, settings) {
         res += showSuiteData(subSuite, settings, chain.prNum);
     }
 
-    res += "<tr><td colspan='4'>&nbsp;</td></tr>";
-    res += "</table>";
-
     return res;
 }
 
diff --git a/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/tcbot/chain/MockBasedTcBotModule.java b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/tcbot/chain/MockBasedTcBotModule.java
index f56522c..daab1cf 100644
--- a/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/tcbot/chain/MockBasedTcBotModule.java
+++ b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/tcbot/chain/MockBasedTcBotModule.java
@@ -52,6 +52,7 @@ import org.apache.ignite.ci.teamcity.ignited.TeamcityIgnitedProviderMock;
 import org.apache.ignite.tcbot.common.conf.IDataSourcesConfigSupplier;
 import org.apache.ignite.tcbot.common.conf.TcBotWorkDir;
 import org.apache.ignite.tcignited.buildlog.IBuildLogProcessor;
+import org.apache.ignite.tcservice.ITeamcityConn;
 import org.mockito.Mockito;
 
 import static org.mockito.ArgumentMatchers.any;
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/TestCompactedMult.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/TestCompactedMult.java
index 9863364..199bdea 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/TestCompactedMult.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/TestCompactedMult.java
@@ -64,7 +64,15 @@ public class TestCompactedMult {
     public String getName() {
         return occurrences.isEmpty() ? "" : occurrences.iterator().next().testName(compactor);
     }
- 
+
+    public Long getId() {
+        return occurrences.isEmpty() ? 0 : occurrences.iterator().next().getTestId();
+    }
+
+    public boolean isPassed() {
+        return occurrences.get(occurrences.size()-1).status() == STATUS_SUCCESS_CID;
+    }
+
     public boolean isInvestigated() {
         return occurrences.stream().anyMatch(ITest::isInvestigated);
     }
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java
index 8cfaffa..0f93b99 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java
@@ -27,6 +27,7 @@ import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
+import javax.inject.Provider;
 import org.apache.ignite.ci.github.PullRequest;
 import org.apache.ignite.githubignited.IGitHubConnIgnited;
 import org.apache.ignite.githubignited.IGitHubConnIgnitedProvider;
@@ -46,7 +47,9 @@ import org.apache.ignite.tcbot.engine.conf.ITrackedChain;
 import org.apache.ignite.tcbot.engine.ui.DsChainUi;
 import org.apache.ignite.tcbot.engine.ui.DsSummaryUi;
 import org.apache.ignite.tcbot.engine.ui.ShortSuiteUi;
+import org.apache.ignite.tcbot.engine.ui.ShortSuiteNewTestsUi;
 import org.apache.ignite.tcbot.engine.ui.ShortTestFailureUi;
+import org.apache.ignite.tcbot.engine.ui.ShortTestUi;
 import org.apache.ignite.tcbot.persistence.IStringCompactor;
 import org.apache.ignite.tcignited.ITeamcityIgnited;
 import org.apache.ignite.tcignited.ITeamcityIgnitedProvider;
@@ -56,6 +59,8 @@ import org.apache.ignite.tcignited.buildref.BranchEquivalence;
 import org.apache.ignite.tcignited.creds.ICredentialsProv;
 import org.apache.ignite.tcignited.history.IRunHistory;
 import org.apache.ignite.tcservice.ITeamcity;
+import org.apache.ignite.tcservice.ITeamcityConn;
+import org.apache.ignite.tcservice.TeamcityServiceConnection;
 
 /**
  * Process pull request/untracked branch chain at particular server.
@@ -87,6 +92,7 @@ public class PrChainsProcessor {
     @Inject private ITcBotConfig cfg;
 
     @Inject private BranchEquivalence branchEquivalence;
+
     @Inject private UpdateCountersStorage countersStorage;
 
     /**
@@ -113,6 +119,7 @@ public class PrChainsProcessor {
         @Nullable Boolean checkAllLogs,
         SyncMode mode) {
         final DsSummaryUi res = new DsSummaryUi();
+
         ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, creds);
 
         IGitHubConnIgnited gitHubConnIgnited = gitHubConnIgnitedProvider.server(srvCodeOrAlias);
@@ -167,7 +174,7 @@ public class PrChainsProcessor {
             //fail rate reference is always default (master)
             chainStatus.initFromContext(tcIgnited, ctx, baseBranchForTc, compactor, false,
                     null, null, -1, null, false, false); // don't need for PR
-
+            chainStatus.findNewTests(ctx, tcIgnited, baseBranchForTc, compactor);
             initJiraAndGitInfo(chainStatus, jiraIntegration, gitHubConnIgnited);
         }
 
@@ -285,6 +292,45 @@ public class PrChainsProcessor {
     }
 
     /**
+     * @param buildTypeId  Build type ID, for which visa was ordered.
+     * @param branchForTc Branch for TeamCity.
+     * @param srvCodeOrAlias Server id.
+     * @param prov Credentials.
+     * @param syncMode
+     * @param baseBranchForTc
+     * @return List of suites with possible blockers.
+     */
+    @Nullable
+    public List<ShortSuiteNewTestsUi> getNewTestsSuitesStatuses(
+        String buildTypeId,
+        String branchForTc,
+        String srvCodeOrAlias,
+        ICredentialsProv prov,
+        SyncMode syncMode,
+        @Nullable String baseBranchForTc) {
+        ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, prov);
+
+        List<Integer> hist = tcIgnited.getLastNBuildsFromHistory(buildTypeId, branchForTc, 1);
+
+        String baseBranch = Strings.isNullOrEmpty(baseBranchForTc) ? dfltBaseTcBranch(srvCodeOrAlias) : baseBranchForTc;
+
+        FullChainRunCtx ctx = buildChainProcessor.loadFullChainContext(
+            tcIgnited,
+            hist,
+            LatestRebuildMode.LATEST,
+            ProcessLogsMode.SUITE_NOT_COMPLETE,
+            false,
+            baseBranch,
+            syncMode,
+            null, null);
+
+        if (ctx.isFakeStub())
+            return null;
+
+        return findNewTests(ctx, tcIgnited, baseBranch);
+    }
+
+    /**
      * @return Blocker failures for given server.
      * @param fullChainRunCtx
      * @param tcIgnited
@@ -329,6 +375,42 @@ public class PrChainsProcessor {
             .collect(Collectors.toList());
     }
 
+    /**
+     * @return New tests for given server.
+     * @param fullChainRunCtx
+     * @param tcIgnited
+     * @param baseBranch
+     */
+    //todo may avoid creation of UI model for simple comment.
+    private List<ShortSuiteNewTestsUi> findNewTests(FullChainRunCtx fullChainRunCtx,
+        ITeamcityIgnited tcIgnited,
+        String baseBranch) {
+        String normalizedBaseBranch = BranchEquivalence.normalizeBranch(baseBranch);
+        Integer baseBranchId = compactor.getStringIdIfPresent(normalizedBaseBranch);
+
+        return fullChainRunCtx
+            .suites()
+            .map((ctx) -> {
+                List<ShortTestUi> missingTests = ctx.getFilteredTests(test -> {
+                    IRunHistory history = test.history(tcIgnited, baseBranchId, null);
+                    return history == null;
+                })
+                    .stream()
+                    .map(occurrence -> new ShortTestUi().initFrom(occurrence, occurrence.isPassed()))
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+
+                if (!missingTests.isEmpty()) {
+                    return new ShortSuiteNewTestsUi()
+                        .tests(missingTests)
+                        .initFrom(ctx);
+                }
+                return null;
+            })
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
     public Map<Integer, Integer> getPrUpdateCounters(String srvCodeOrAlias, String branchForTc, String tcBaseBranchParm,
         ICredentialsProv creds) {
         String baseBranchForTc = Strings.isNullOrEmpty(tcBaseBranchParm) ? dfltBaseTcBranch(srvCodeOrAlias) : tcBaseBranchParm;
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/DsChainUi.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/DsChainUi.java
index bc8f2da..ec47896 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/DsChainUi.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/DsChainUi.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.apache.ignite.internal.util.typedef.T2;
@@ -34,6 +35,8 @@ import org.apache.ignite.tcbot.engine.chain.TestCompactedMult;
 import org.apache.ignite.tcbot.engine.tracked.DisplayMode;
 import org.apache.ignite.tcbot.persistence.IStringCompactor;
 import org.apache.ignite.tcignited.ITeamcityIgnited;
+import org.apache.ignite.tcignited.history.IRunHistory;
+import org.apache.ignite.tcservice.ITeamcityConn;
 import org.apache.ignite.tcservice.model.conf.BuildType;
 
 import static org.apache.ignite.tcbot.engine.ui.DsSuiteUi.createOccurForLogConsumer;
@@ -91,6 +94,9 @@ public class DsChainUi {
     /** Suites involved in chain. */
     public List<DsSuiteUi> suites = new ArrayList<>();
 
+    /** Suites with new tests involved in chain. */
+    public List<ShortSuiteNewTestsUi> newTestsUi = new ArrayList<>();
+
     /** Count of failed tests not muted tests. In case several runs are used, overall by all runs. */
     public Integer failedTests;
 
@@ -285,6 +291,37 @@ public class DsChainUi {
         );
     }
 
+    public void findNewTests(FullChainRunCtx ctx,
+        ITeamcityIgnited tcIgnited,
+        String baseBranchTc,
+        IStringCompactor compactor){
+        String failRateNormalizedBranch = normalizeBranch(baseBranchTc);
+        Integer baseBranchId = compactor.getStringIdIfPresent(failRateNormalizedBranch);
+        newTestsUi = ctx
+            .suites()
+            .map((suite) -> {
+                List<ShortTestUi> missingTests = suite.getFilteredTests(test -> {
+                    IRunHistory history = test.history(tcIgnited, baseBranchId, null);
+                    return history == null;
+                })
+                    .stream()
+                    .map(occurrence -> {
+                        ShortTestUi tst = new ShortTestUi().initFrom(occurrence, occurrence.isPassed());
+                        return tst;
+                    })
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+                if (!missingTests.isEmpty()) {
+                    return new ShortSuiteNewTestsUi()
+                        .tests(missingTests)
+                        .initFrom(suite);
+                }
+                return null;
+            })
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
     private static String buildWebLinkToBuild(ITeamcityIgnited teamcity, FullChainRunCtx chain) {
         return teamcity.host() + "viewLog.html?buildId=" + chain.getSuiteBuildId();
     }
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortSuiteNewTestsUi.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortSuiteNewTestsUi.java
new file mode 100644
index 0000000..8bfb2e5
--- /dev/null
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortSuiteNewTestsUi.java
@@ -0,0 +1,46 @@
+/*
+ * 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.tcbot.engine.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.apache.ignite.tcbot.engine.chain.MultBuildRunCtx;
+
+public class ShortSuiteNewTestsUi extends DsHistoryStatUi {
+    /** Suite Name */
+    public String name;
+
+    public List<ShortTestUi> tests = new ArrayList<>();
+
+    public Collection<? extends ShortTestUi> tests() {
+        return tests;
+    }
+
+    public ShortSuiteNewTestsUi tests(List<ShortTestUi> tests) {
+        this.tests = tests;
+
+        return this;
+    }
+
+    public ShortSuiteNewTestsUi initFrom(@Nonnull MultBuildRunCtx suite) {
+        name = suite.suiteName();
+
+        return this;
+    }
+}
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortTestUi.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortTestUi.java
new file mode 100644
index 0000000..822fd86
--- /dev/null
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/ui/ShortTestUi.java
@@ -0,0 +1,70 @@
+/*
+ * 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.tcbot.engine.ui;
+
+import com.google.common.base.Strings;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.tcbot.engine.chain.TestCompactedMult;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public class ShortTestUi {
+    private static final Pattern TEST_CLASS_AND_METHOD_PATTERN = Pattern.compile("([^.]+[.][^.]+(\\[.+\\])?$)");
+
+    /** Test full Name */
+    public String name;
+
+    /** suite (in code) short name */
+    @Nullable public String suiteName;
+
+    /** test short name with class and method */
+    @Nullable public String testName;
+
+
+    /** Test status */
+    @Nullable public boolean status;
+
+    public ShortTestUi initFrom(@Nonnull TestCompactedMult testCompactedMult, boolean status) {
+        name = testCompactedMult.getName();
+
+        String[] split = Strings.nullToEmpty(name).split("\\:");
+        if (split.length >= 2) {
+            this.suiteName = extractSuite(split[0]);
+            this.testName = extractTest(split[1]);
+        }
+
+        this.status = status;
+
+        return this;
+    }
+
+    public static String extractTest(String s) {
+        String testShort = s.trim();
+        Matcher matcher = TEST_CLASS_AND_METHOD_PATTERN.matcher(testShort);
+        return matcher.find() ? matcher.group(0) : null;
+    }
+
+    public static String extractSuite(String s) {
+        String suiteShort = s.trim();
+        String[] suiteComps = suiteShort.split("\\.");
+        if (suiteComps.length > 1)
+            return suiteComps[suiteComps.length - 1];
+        return null;
+    }
+}