/*
* Copyright 2015-present Facebook, Inc.
*
* Licensed 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 com.facebook.buck.intellij.ideabuck.ui;
import com.facebook.buck.intellij.ideabuck.actions.BuckInstallDebugAction;
import com.facebook.buck.intellij.ideabuck.debugger.AndroidDebugger;
import com.facebook.buck.intellij.ideabuck.ui.tree.BuckTreeNodeBuild;
import com.facebook.buck.intellij.ideabuck.ui.tree.BuckTreeNodeDetail;
import com.facebook.buck.intellij.ideabuck.ui.tree.BuckTreeNodeFileError;
import com.facebook.buck.intellij.ideabuck.ui.tree.BuckTreeNodeTarget;
import com.facebook.buck.intellij.ideabuck.ui.utils.CompilerErrorItem;
import com.facebook.buck.intellij.ideabuck.ui.utils.ErrorExtractor;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckBuildEndConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckBuildProgressUpdateConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckBuildStartConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckConsoleEventConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckInstallFinishedConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckProjectGenerationFinishedConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckProjectGenerationProgressConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.BuckProjectGenerationStartedConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.CompilerErrorConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.RulesParsingEndConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.RulesParsingProgressUpdateConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.RulesParsingStartConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.TestResultsAvailableConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.TestRunCompleteConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.consumers.TestRunStartedConsumer;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.parts.TestCaseSummary;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.parts.TestResults;
import com.facebook.buck.intellij.ideabuck.ws.buckevents.parts.TestResultsSummary;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.util.messages.MessageBusConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.tree.DefaultTreeModel;
public class BuckEventsConsumer
implements BuckBuildStartConsumer,
BuckBuildEndConsumer,
BuckBuildProgressUpdateConsumer,
BuckProjectGenerationFinishedConsumer,
BuckProjectGenerationProgressConsumer,
BuckProjectGenerationStartedConsumer,
RulesParsingStartConsumer,
RulesParsingEndConsumer,
RulesParsingProgressUpdateConsumer,
TestRunStartedConsumer,
TestRunCompleteConsumer,
TestResultsAvailableConsumer,
BuckConsoleEventConsumer,
CompilerErrorConsumer,
BuckInstallFinishedConsumer {
private Project mProject;
public BuckEventsConsumer(Project project) {
mProject = project;
}
private String mTarget = null;
private MessageBusConnection mConnection;
private DefaultTreeModel mTreeModel;
private Map<String, List<String>> mErrors =
Collections.synchronizedMap(new HashMap<String, List<String>>());
private double mBuildProgressValue = 0;
private double mParseProgressValue = 0;
private double mProjectGenerationProgressValue = 0;
private long mMainBuildStartTimestamp = 0;
private long mMainBuildEndTimestamp = 0;
private long mParseFilesStartTimestamp = 0;
private long mParseFilesEndTimestamp = 0;
private long mTestingEndTimestamp = 0;
private long mTestingStartTimestamp = 0;
private List<TestResults> mTestResultsList =
Collections.synchronizedList(new LinkedList<TestResults>());
private boolean attached = false;
private long mProjectGenerationStartTimestamp = 0;
private long mProjectGenerationFinishedTimestamp = 0;
BuckTreeNodeBuild mCurrentBuildRootElement;
BuckTreeNodeDetail mBuildProgress;
BuckTreeNodeDetail mParseProgress;
BuckTreeNodeDetail mTestResults;
BuckTreeNodeDetail mProjectGenerationProgress;
public void detach() {
if (mConnection != null) {
mConnection.disconnect();
}
mBuildProgress = null;
mParseProgress = null;
mTestResults = null;
mProjectGenerationProgress = null;
attached = false;
mBuildProgressValue = 0;
mParseProgressValue = 0;
mProjectGenerationProgressValue = 0;
mTestResultsList = Collections.synchronizedList(new LinkedList<TestResults>());
mErrors = Collections.synchronizedMap(new HashMap<String, List<String>>());
}
public boolean isAttached() {
return attached;
}
public void attach(String target, DefaultTreeModel treeModel) {
mTreeModel = treeModel;
mTarget = target == null ? "NONE" : target;
mCurrentBuildRootElement = new BuckTreeNodeBuild(mTarget);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
mTreeModel.setRoot(mCurrentBuildRootElement);
}
});
mMainBuildStartTimestamp = 0;
mConnection = mProject.getMessageBus().connect();
mConnection.subscribe(
BuckProjectGenerationFinishedConsumer.PROJECT_GENERATION_FINISHED_CONSUMER, this);
mConnection.subscribe(
BuckProjectGenerationStartedConsumer.PROJECT_GENERATION_STARTED_CONSUMER, this);
mConnection.subscribe(
BuckProjectGenerationProgressConsumer.PROJECT_GENERATION_PROGRESS_CONSUMER, this);
mConnection.subscribe(BuckBuildStartConsumer.BUCK_BUILD_START, this);
mConnection.subscribe(BuckBuildEndConsumer.BUCK_BUILD_END, this);
mConnection.subscribe(BuckBuildProgressUpdateConsumer.BUCK_BUILD_PROGRESS_UPDATE, this);
mConnection.subscribe(RulesParsingStartConsumer.BUCK_PARSE_RULE_START, this);
mConnection.subscribe(RulesParsingEndConsumer.BUCK_PARSE_RULE_END, this);
mConnection.subscribe(RulesParsingProgressUpdateConsumer.BUCK_PARSE_PROGRESS_UPDATE, this);
mConnection.subscribe(CompilerErrorConsumer.COMPILER_ERROR_CONSUMER, this);
mConnection.subscribe(BuckConsoleEventConsumer.BUCK_CONSOLE_EVENT, this);
mConnection.subscribe(TestRunStartedConsumer.BUCK_TEST_RUN_STARTED, this);
mConnection.subscribe(TestRunCompleteConsumer.BUCK_TEST_RUN_COMPLETE, this);
mConnection.subscribe(TestResultsAvailableConsumer.BUCK_TEST_RESULTS_AVAILABLE, this);
mConnection.subscribe(BuckInstallFinishedConsumer.INSTALL_FINISHED_CONSUMER, this);
attached = true;
}
@Override
public void consumeBuckBuildProgressUpdate(long timestamp, double progressValue) {
if (mBuildProgress == null) {
consumeBuildStart(timestamp);
}
mBuildProgressValue = progressValue;
final String message = "Current build progress: " + Math.round(mBuildProgressValue * 100) + "%";
BuckEventsConsumer.this.mBuildProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
@Override
public void consumeParseRuleStart(long timestamp) {
mParseFilesStartTimestamp = timestamp;
// start may be called before of after progress update
if (BuckEventsConsumer.this.mParseProgress == null) {
BuckEventsConsumer.this.mParseProgress =
new BuckTreeNodeDetail(
mCurrentBuildRootElement,
BuckTreeNodeDetail.DetailType.INFO,
"Current file parsing progress: " + Math.round(mParseProgressValue * 100) + "%");
BuckEventsConsumer.this.mCurrentBuildRootElement.addChild(mParseProgress);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
@Override
public void consumeParseRuleEnd(long timestamp) {
consumeParseRuleProgressUpdate(timestamp, 1.0f);
mParseFilesEndTimestamp = timestamp;
float duration = (mParseFilesEndTimestamp - mParseFilesStartTimestamp) / 1000;
final String message = "File parsing ended, took " + duration + " seconds!";
BuckEventsConsumer.this.mParseProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
@Override
public void consumeParseRuleProgressUpdate(long timestamp, double progressValue) {
if (mParseProgress == null) {
consumeParseRuleStart(timestamp);
}
if (mParseProgressValue != 1.0f) {
mParseProgressValue = progressValue;
final String message =
"Current file parsing progress: " + Math.round(mParseProgressValue * 100) + "%";
BuckEventsConsumer.this.mParseProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
@Override
public void consumeCompilerError(
String target, long timestamp, String error, ImmutableSet<String> suggestions) {
List<String> existingErrors;
if (mErrors.containsKey(target)) {
existingErrors = mErrors.get(target);
} else {
existingErrors = new ArrayList<String>();
}
existingErrors.add(error);
mErrors.put(target, existingErrors);
}
private BuckTreeNodeTarget buildTargetErrorNode(String target, List<String> errorMessages) {
BuckTreeNodeTarget targetNode = new BuckTreeNodeTarget(mCurrentBuildRootElement, target);
for (String currentError : errorMessages) {
ErrorExtractor errorExtractor = new ErrorExtractor(currentError);
ImmutableList<CompilerErrorItem> errorItems = errorExtractor.getErrors();
BuckTreeNodeFileError currentFileErrorNode = null;
String currentFilePath = "";
for (CompilerErrorItem currentErrorItem : errorItems) {
if (!currentErrorItem.getFilePath().equals(currentFilePath)) {
if (currentFileErrorNode != null) {
// Add to parent
targetNode.addFileError(currentFileErrorNode);
}
// Create the new node, for the new file
currentFileErrorNode =
new BuckTreeNodeFileError(
targetNode, currentErrorItem.getFilePath(), currentErrorItem);
currentFilePath = currentErrorItem.getFilePath();
} else {
currentFileErrorNode.addError(currentErrorItem);
}
}
if (currentFileErrorNode != null) {
// Add the leftovers
targetNode.addFileError(currentFileErrorNode);
}
}
return targetNode;
}
private void displayErrors() {
if (mErrors.size() > 0) {
if (!BuckToolWindowFactory.isToolWindowVisible(mProject)) {
BuckToolWindowFactory.showToolWindow(mProject);
}
Set<String> targetsWithErrors = mErrors.keySet();
for (String target : targetsWithErrors) {
List<String> errorMessages = mErrors.get(target);
if (errorMessages.size() > 0) {
BuckTreeNodeTarget targetNode = buildTargetErrorNode(target, errorMessages);
mCurrentBuildRootElement.addChild(targetNode);
}
}
}
}
public void clearDisplay() {
if (mTestResults != null) {
mCurrentBuildRootElement.removeChild(mTestResults);
}
if (mParseProgress != null) {
mCurrentBuildRootElement.removeChild(mParseProgress);
}
if (mBuildProgress != null) {
mCurrentBuildRootElement.removeChild(mBuildProgress);
}
if (mProjectGenerationProgress != null) {
mCurrentBuildRootElement.removeChild(mProjectGenerationProgress);
}
}
@Override
public void consumeBuildEnd(final long timestamp) {
mMainBuildEndTimestamp = timestamp;
float duration = (mMainBuildEndTimestamp - mMainBuildStartTimestamp) / 1000;
final String message = "Build ended, took " + duration + " seconds!";
int errors = 0;
int warnings = 0;
for (String cTarget : mErrors.keySet()) {
List<String> currentErrorMessages = mErrors.get(cTarget);
for (String currentErrorMessage : currentErrorMessages) {
ErrorExtractor errorExtractor = new ErrorExtractor(currentErrorMessage);
for (CompilerErrorItem currentErrorItem : errorExtractor.getErrors()) {
if (currentErrorItem.getType() == CompilerErrorItem.Type.ERROR) {
errors++;
} else {
warnings++;
}
}
}
}
String errorsMessage = "";
if (errors != 0) {
errorsMessage = "Found " + errors + " errors";
}
if (warnings != 0) {
if (errors != 0) {
errorsMessage += " and " + warnings + " warnings";
} else {
errorsMessage = "Found " + warnings + " warnings";
}
}
if (errorsMessage.length() > 0) {
errorsMessage += "!";
}
final String errorsMessageToUse = errorsMessage;
//set progress to 100%
consumeBuckBuildProgressUpdate(timestamp, 1f);
if (errorsMessageToUse.length() > 0) {
clearDisplay();
BuckTreeNodeDetail errorsMessageNode =
new BuckTreeNodeDetail(
BuckEventsConsumer.this.mCurrentBuildRootElement,
BuckTreeNodeDetail.DetailType.ERROR,
errorsMessageToUse);
BuckEventsConsumer.this.mCurrentBuildRootElement.addChild(errorsMessageNode);
// Display errors
BuckEventsConsumer.this.displayErrors();
}
BuckEventsConsumer.this.mBuildProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
@Override
public void consumeBuildStart(long timestamp) {
mMainBuildStartTimestamp = timestamp;
if (BuckEventsConsumer.this.mBuildProgress == null) {
BuckEventsConsumer.this.mBuildProgress =
new BuckTreeNodeDetail(
BuckEventsConsumer.this.mCurrentBuildRootElement,
BuckTreeNodeDetail.DetailType.INFO,
"Current build progress: " + Math.round(mBuildProgressValue * 100) + "%");
// start may be called before of after progress update
BuckEventsConsumer.this.mCurrentBuildRootElement.addChild(mBuildProgress);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
@Override
public void consumeBuckProjectGenerationFinished(long timestamp) {
consumeBuckProjectGenerationProgress(timestamp, 1.0f);
mProjectGenerationFinishedTimestamp = timestamp;
float duration =
(mProjectGenerationFinishedTimestamp - mProjectGenerationStartTimestamp) / 1000;
final String message = "Project generation ended, took " + duration + " seconds!";
BuckEventsConsumer.this.mProjectGenerationProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
// IntelliJ's synchronize action
FileDocumentManager.getInstance().saveAllDocuments();
VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
}
});
}
@Override
public void consumeBuckProjectGenerationProgress(long timestamp, double progressValue) {
if (mProjectGenerationProgress == null) {
consumeBuckProjectGenerationStarted(timestamp);
}
if (mProjectGenerationProgressValue != 1.0f) {
mProjectGenerationProgressValue = progressValue;
final String message =
"Current file parsing progress: "
+ Math.round(mProjectGenerationProgressValue * 100)
+ "%";
BuckEventsConsumer.this.mProjectGenerationProgress.setDetail(message);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
@Override
public void consumeBuckProjectGenerationStarted(long timestamp) {
mProjectGenerationStartTimestamp = timestamp;
// start may be called before of after progress update
if (BuckEventsConsumer.this.mProjectGenerationProgress == null) {
BuckEventsConsumer.this.mProjectGenerationProgress =
new BuckTreeNodeDetail(
BuckEventsConsumer.this.mCurrentBuildRootElement,
BuckTreeNodeDetail.DetailType.INFO,
"Current project generation progress: "
+ Math.round(BuckEventsConsumer.this.mProjectGenerationProgressValue * 100)
+ "%");
BuckEventsConsumer.this.mCurrentBuildRootElement.addChild(mProjectGenerationProgress);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
public void showTestResults() {
if (!mTestResultsList.isEmpty() && !BuckToolWindowFactory.isToolWindowVisible(mProject)) {
BuckToolWindowFactory.showToolWindow(mProject);
}
float duration = (mTestingEndTimestamp - mTestingStartTimestamp) / 1000;
final StringBuilder message =
new StringBuilder("Testing ended, took " + duration + " seconds!\n");
for (TestResults testResults : mTestResultsList) {
for (TestCaseSummary testCaseSummary : testResults.getTestCases()) {
long time = testCaseSummary.getTotalTime();
String formattedTime;
if (time >= 1000) {
formattedTime = (time / 1000 + time % 1000) + "s";
} else {
formattedTime = time + "ms";
}
if (testCaseSummary.isSuccess() == true) {
message.append(
"PASS "
+ formattedTime
+ " "
+ testCaseSummary.getTestResults().size()
+ " passed "
+ testCaseSummary.getTestCaseName()
+ "\n");
} else {
int failureCount = 0;
StringBuilder failureMessage = new StringBuilder();
for (TestResultsSummary testResultSummary : testCaseSummary.getTestResults()) {
if (!testResultSummary.isSuccess()) {
failureMessage.append(testResultSummary.getStacktrace() + "\n");
failureCount++;
}
}
message.append(
"FAILED "
+ formattedTime
+ " "
+ (testCaseSummary.getTestResults().size() - failureCount)
+ " passed "
+ failureCount
+ " failed "
+ testCaseSummary.getTestCaseName()
+ "\n");
message.append(failureMessage);
}
}
}
BuckEventsConsumer.this.mTestResults.setDetail(message.toString());
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
mTestResultsList.clear();
}
@Override
public void consumeTestResultsAvailable(long timestamp, final TestResults testResults) {
if (mTestResults == null) {
consumeTestRunStarted(timestamp);
}
mTestResultsList.add(testResults);
}
@Override
public void consumeTestRunComplete(long timestamp) {
if (mTestResults == null) {
consumeTestRunStarted(timestamp);
}
mTestingEndTimestamp = timestamp;
showTestResults();
}
@Override
public void consumeTestRunStarted(long timestamp) {
mTestingStartTimestamp = timestamp;
// start may be called before of after progress update
if (BuckEventsConsumer.this.mTestResults == null) {
BuckEventsConsumer.this.mTestResults =
new BuckTreeNodeDetail(
mCurrentBuildRootElement, BuckTreeNodeDetail.DetailType.INFO, "Running tests");
BuckEventsConsumer.this.mCurrentBuildRootElement.addChild(mTestResults);
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
}
@Override
public void consumeConsoleEvent(final String message) {
if (!BuckToolWindowFactory.isToolWindowInstantiated(mProject)) {
return;
}
if (!BuckToolWindowFactory.isToolWindowVisible(mProject)) {
BuckToolWindowFactory.showToolWindow(mProject);
}
if (mCurrentBuildRootElement == null) {
return;
}
mCurrentBuildRootElement.addChild(
new BuckTreeNodeDetail(
mCurrentBuildRootElement, BuckTreeNodeDetail.DetailType.ERROR, message));
ApplicationManager.getApplication()
.invokeLater(
new Runnable() {
@Override
public void run() {
BuckEventsConsumer.this.mTreeModel.reload();
}
});
}
@Override
public void consumeInstallFinished(long timestamp, final String packageName) {
if (BuckInstallDebugAction.shouldDebug()) {
ApplicationManager.getApplication()
.executeOnPooledThread(
new Runnable() {
@Override
public void run() {
try {
AndroidDebugger.init();
AndroidDebugger.attachDebugger(packageName, mProject);
BuckInstallDebugAction.setDebug(false);
} catch (InterruptedException | RuntimeException e) {
// Display the error on our console
consumeConsoleEvent(e.toString());
}
}
});
}
}
}