blob: 9042e1e73f42f9a1155c0ae0517c908fde9d6571 [file] [log] [blame]
/*
* 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.board;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.apache.ignite.ci.issue.Issue;
import org.apache.ignite.ci.issue.IssueKey;
import org.apache.ignite.ci.teamcity.ignited.change.ChangeCompacted;
import org.apache.ignite.ci.teamcity.ignited.change.ChangeDao;
import org.apache.ignite.ci.teamcity.ignited.fatbuild.FatBuildCompacted;
import org.apache.ignite.ci.teamcity.ignited.fatbuild.ProblemCompacted;
import org.apache.ignite.ci.user.TcHelperUser;
import org.apache.ignite.tcbot.common.conf.ITcServerConfig;
import org.apache.ignite.tcbot.common.interceptor.MonitoredTask;
import org.apache.ignite.tcbot.common.util.FutureUtil;
import org.apache.ignite.tcbot.engine.chain.BuildChainProcessor;
import org.apache.ignite.tcbot.engine.chain.SingleBuildRunCtx;
import org.apache.ignite.tcbot.engine.conf.ITcBotConfig;
import org.apache.ignite.tcbot.engine.defect.BlameCandidate;
import org.apache.ignite.tcbot.engine.defect.DefectCompacted;
import org.apache.ignite.tcbot.engine.defect.DefectFirstBuild;
import org.apache.ignite.tcbot.engine.defect.DefectIssue;
import org.apache.ignite.tcbot.engine.defect.DefectsStorage;
import org.apache.ignite.tcbot.engine.issue.IIssuesStorage;
import org.apache.ignite.tcbot.engine.issue.IssueType;
import org.apache.ignite.tcbot.engine.ui.BoardDefectIssueUi;
import org.apache.ignite.tcbot.engine.ui.BoardDefectSummaryUi;
import org.apache.ignite.tcbot.engine.ui.BoardSummaryUi;
import org.apache.ignite.tcbot.engine.ui.DsSuiteUi;
import org.apache.ignite.tcbot.engine.ui.DsTestFailureUi;
import org.apache.ignite.tcbot.engine.user.IUserStorage;
import org.apache.ignite.tcbot.persistence.IStringCompactor;
import org.apache.ignite.tcbot.persistence.scheduler.IScheduler;
import org.apache.ignite.tcignited.ITeamcityIgnited;
import org.apache.ignite.tcignited.ITeamcityIgnitedProvider;
import org.apache.ignite.tcignited.build.FatBuildDao;
import org.apache.ignite.tcignited.build.ITest;
import org.apache.ignite.tcignited.creds.ICredentialsProv;
import org.apache.ignite.tcignited.history.IRunHistory;
import static org.apache.ignite.tcignited.history.RunStatus.RES_MISSING;
import static org.apache.ignite.tcignited.history.RunStatus.RES_OK;
public class BoardService {
@Inject IIssuesStorage issuesStorage;
@Inject FatBuildDao fatBuildDao;
@Inject ChangeDao changeDao;
@Inject ITeamcityIgnitedProvider tcProv;
@Inject DefectsStorage defectStorage;
@Inject IScheduler scheduler;
@Inject IStringCompactor compactor;
@Inject BuildChainProcessor buildChainProcessor;
@Inject IUserStorage userStorage;
@Inject ITcBotConfig cfg;
/**
* @param creds Credentials.
*/
public BoardSummaryUi summary(ICredentialsProv creds) {
issuesToDefectsLater();
Map<Integer, Future<FatBuildCompacted>> allBuildsMap = new HashMap<>();
List<DefectCompacted> defects = defectStorage.loadAllDefects();
BoardSummaryUi res = new BoardSummaryUi();
boolean admin = userStorage.getUser(creds.getPrincipalId()).isAdmin();
for (DefectCompacted next : defects) {
BoardDefectSummaryUi defectUi = new BoardDefectSummaryUi(next, compactor);
defectUi.setForceResolveAllowed(admin);
String srvCode = next.tcSrvCode(compactor);
if (!creds.hasAccess(srvCode))
continue;
ITeamcityIgnited tcIgn = tcProv.server(srvCode, creds);
ITcServerConfig cfg = tcIgn.config();
List<BlameCandidate> candidates = next.blameCandidates();
Map<Integer, DefectFirstBuild> build = next.buildsInvolved();
for (DefectFirstBuild cause : build.values()) {
FatBuildCompacted firstBuild = cause.build();
defectUi.addTags(SingleBuildRunCtx.getBuildTagsFromParameters(cfg, compactor, firstBuild));
FatBuildCompacted fatBuild = fatBuildDao.getFatBuild(next.tcSrvId(), firstBuild.id());
List<Future<FatBuildCompacted>> futures = buildChainProcessor.replaceWithRecent(fatBuild, allBuildsMap, tcIgn);
Stream<FatBuildCompacted> results = FutureUtil.getResults(futures);
List<FatBuildCompacted> freshRebuild = results.collect(Collectors.toList());
Optional<FatBuildCompacted> rebuild;
rebuild = !freshRebuild.isEmpty() ? freshRebuild.stream().findFirst() : Optional.empty();
for (DefectIssue issue : cause.issues()) {
BoardDefectIssueUi issueUi = processIssue(tcIgn, rebuild, issue, firstBuild.buildTypeId());
defectUi.addIssue(issueUi);
}
}
defectUi.branch = next.tcBranch(compactor);
res.addDefect(defectUi);
}
return res;
}
public BoardDefectIssueUi processIssue(ITeamcityIgnited tcIgn,
Optional<FatBuildCompacted> rebuild,
DefectIssue issue, int projectId) {
Optional<ITest> testResult;
String issueType = compactor.getStringFromId(issue.issueTypeCode());
boolean suiteProblem = IssueType.newCriticalFailure.code().equals(issueType)
|| IssueType.newTrustedSuiteFailure.code().equals(issueType);
String webUrl = null;
IssueResolveStatus status;
if (suiteProblem) {
if (rebuild.isPresent()) {
FatBuildCompacted fatBuildCompacted = rebuild.get();
List<ProblemCompacted> problems = fatBuildCompacted.problems();
if (IssueType.newCriticalFailure.code().equals(issueType)) {
boolean hasCriticalProblem = problems.stream().anyMatch(occurrence -> occurrence.isCriticalProblem(compactor));
status = hasCriticalProblem ? IssueResolveStatus.FAILING : IssueResolveStatus.FIXED;
} else {
boolean hasBuildProblem = problems.stream().anyMatch(p -> !p.isFailedTests(compactor) && !p.isSnapshotDepProblem(compactor));
status = hasBuildProblem ? IssueResolveStatus.FAILING : IssueResolveStatus.FIXED;
}
webUrl = DsSuiteUi.buildWebLinkToHist(tcIgn,
fatBuildCompacted.buildTypeId(compactor),
fatBuildCompacted.branchName(compactor)
);
} else
status = IssueResolveStatus.UNKNOWN;
} else {
if (rebuild.isPresent()) {
testResult = rebuild.get().getAllTests()
.filter(t -> t.testName() == issue.testNameCid())
.findAny();
} else
testResult = Optional.empty();
if (testResult.isPresent()) {
ITest test = testResult.get();
if (test.isIgnoredTest() || test.isMutedTest())
status = IssueResolveStatus.IGNORED;
else if (IssueType.newTestWithHighFlakyRate.code().equals(issueType)) {
int fullSuiteNameAndFullTestName = issue.testNameCid();
int branchName = rebuild.get().branchName();
IRunHistory runStat = tcIgn.getTestRunHist(fullSuiteNameAndFullTestName, projectId, branchName);
if (runStat == null)
status = IssueResolveStatus.UNKNOWN;
else {
List<Integer> runResults = runStat.getLatestRunResults();
if (runResults == null)
status = IssueResolveStatus.UNKNOWN;
else {
int confidenceOkTestsRow = Math.max(1,
(int) Math.ceil(Math.log(1 - cfg.confidence()) / Math.log(1 - issue.getFlakyRate() / 100.0)));
Collections.reverse(runResults);
int okTestRow = 0;
for (Integer run : runResults) {
if (run == null || run == RES_MISSING.getCode())
continue;
if (run == RES_OK.getCode() && (okTestRow < confidenceOkTestsRow))
okTestRow++;
else
break;
}
status = okTestRow >= confidenceOkTestsRow ? IssueResolveStatus.FIXED : IssueResolveStatus.FAILING;
}
}
}
else
status = test.isFailedTest(compactor) ? IssueResolveStatus.FAILING : IssueResolveStatus.FIXED;
FatBuildCompacted fatBuildCompacted = rebuild.get();
Long testNameId = test.getTestId();
String RebuildProjectId = fatBuildCompacted.projectId(compactor);
String branchName = fatBuildCompacted.branchName(compactor);
webUrl = DsTestFailureUi.buildWebLink(tcIgn, testNameId, RebuildProjectId, branchName);
}
else {
//exception for new test. removal of test means test is fixed
status = IssueType.newContributedTestFailure.code().equals(issueType)
? IssueResolveStatus.FIXED
: IssueResolveStatus.UNKNOWN;
}
}
return new BoardDefectIssueUi(status, compactor, issue, suiteProblem, webUrl);
}
public void issuesToDefectsLater() {
scheduler.sheduleNamed("issuesToDefects", this::issuesToDefects, 15, TimeUnit.MINUTES);
}
@MonitoredTask(name = "Convert issues to defect")
protected String issuesToDefects() {
Stream<Issue> stream = issuesStorage.allIssues();
//todo make property how old issues can be considered as configuration parameter
long minIssueTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(14);
//todo not so good to to call init() twice
fatBuildDao.init();
changeDao.init();
AtomicInteger cntIssues = new AtomicInteger();
HashSet<Integer> processedDefects = new HashSet<>();
stream
.filter(issue -> {
long detected = issue.detectedTs == null ? 0 : issue.detectedTs;
return detected >= minIssueTs;
})
.filter(issue -> {
//String type = issue.type;
//return !IssueType.newContributedTestFailure.code().equals(type);
return true;
})
.forEach(issue -> {
cntIssues.incrementAndGet();
IssueKey key = issue.issueKey;
String srvCode = key.getServer();
//just for init call
int srvId = ITeamcityIgnited.serverIdToInt(srvCode);
FatBuildCompacted fatBuild = fatBuildDao.getFatBuild(srvId, key.buildId);
if (fatBuild == null)
return;
//todo non test failures
String testName = issue.issueKey().getTestOrBuildName();
int issueTypeCid = compactor.getStringId(issue.type);
Integer testNameCid = compactor.getStringIdIfPresent(testName);
int trackedBranchCid = compactor.getStringId(issue.trackedBranchName);
double flakyRate = issue.flakyRate;
int tcSrvCodeCid = compactor.getStringId(srvCode);
defectStorage.merge(tcSrvCodeCid, srvId, fatBuild,
(k, defect) -> {
processedDefects.add(defect.id());
defect.trackedBranchCidSetIfEmpty(trackedBranchCid);
defect.computeIfAbsent(fatBuild).addIssue(issueTypeCid, testNameCid, flakyRate);
defect.removeOldVerBlameCandidates();
if(defect.blameCandidates().isEmpty())
fillBlameCandidates(srvId, fatBuild, defect);
return defect;
});
});
return processedDefects.size() + " defects processed for " + cntIssues.get() + " issues checked";
}
private void fillBlameCandidates(int srvId, FatBuildCompacted fatBuild, DefectCompacted defect) {
//save changes because it can be missed in older DB versions
defect.changeMap(changeDao.getAll(srvId, fatBuild.changes()));
Map<Integer, ChangeCompacted> map = defect.changeMap();
Collection<ChangeCompacted> values = map.values();
for (ChangeCompacted change : values) {
BlameCandidate candidate = new BlameCandidate();
int vcsUsernameCid = change.vcsUsername();
candidate.vcsUsername(vcsUsernameCid);
int tcUserUsername = change.tcUserUsername();
@Nullable TcHelperUser tcHelperUser = null;
if (tcUserUsername != -1)
tcHelperUser = userStorage.getUser(compactor.getStringFromId(tcUserUsername));
else {
String strVcsUsername = compactor.getStringFromId(vcsUsernameCid);
if(!Strings.isNullOrEmpty(strVcsUsername) &&
strVcsUsername.contains("<") && strVcsUsername.contains(">")) {
int emailStartIdx = strVcsUsername.indexOf('<');
int emailEndIdx = strVcsUsername.indexOf('>');
String email = strVcsUsername.substring(emailStartIdx + 1, emailEndIdx);
tcHelperUser = userStorage.findUserByEmail(email);
}
}
if (tcHelperUser != null) {
String username = tcHelperUser.username();
String fullName = tcHelperUser.fullName();
candidate.fullDisplayName(compactor.getStringId(fullName));
candidate.tcHelperUserUsername(compactor.getStringId(username));
}
defect.addBlameCandidate(candidate);
}
}
public void resolveDefect(Integer defectId, ICredentialsProv creds, Boolean forceResolve) {
DefectCompacted defect = defectStorage.load(defectId);
Preconditions.checkState(defect != null, "Can't find defect by ID");
String principalId = creds.getPrincipalId();
if(Boolean.TRUE.equals(forceResolve)) {
boolean admin = userStorage.getUser(principalId).isAdmin();
Preconditions.checkState(admin);
}
//todo if it is not forced resovle need to check blockers count for now
int strId = compactor.getStringId(principalId);
defect.resolvedByUsernameId(strId);
defectStorage.save(defect);
}
}