diff options
author | Stas Negara <snegara@google.com> | 2015-04-29 15:07:34 -0700 |
---|---|---|
committer | Stas Negara <snegara@google.com> | 2015-05-05 15:55:22 -0700 |
commit | 81597f2898bb4a179cb43a52352739b6e2669b0a (patch) | |
tree | 8ae3bb34d1090c3f40589ae78340bc3b8e222977 | |
parent | fb062d8a16fc1e8c0b337f71d3e9d9df135ae569 (diff) | |
download | testing-81597f2898bb4a179cb43a52352739b6e2669b0a.tar.gz |
Add debug for a chosen configuration of a cloud matrix run.
Enable a user to click on any test that was run as part of
a matrix execution in the cloud and re-run the corresponding
test configuration on a cloud device matching the matrix
configuration instance of the selected test.
Change-Id: I1a9242b6c9637d8a1b64dc111ad2a318ed83cc38
-rw-r--r-- | src/com/google/gct/testing/CloudConfigurationProviderImpl.java | 60 | ||||
-rw-r--r-- | src/com/google/gct/testing/CloudDebug.png | bin | 0 -> 652 bytes | |||
-rw-r--r-- | src/com/google/gct/testing/CloudResultsLoader.java | 4 | ||||
-rw-r--r-- | src/com/google/gct/testing/DebugConfigurationAction.java | 184 | ||||
-rw-r--r-- | src/com/google/gct/testing/GhostCloudDevice.java (renamed from src/com/google/gct/testing/ui/GhostCloudDevice.java) | 2 | ||||
-rw-r--r-- | src/com/google/gct/testing/results/GoogleCloudTestRunnerToolbarPanel.java | 2 |
6 files changed, 240 insertions, 12 deletions
diff --git a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java index 02dc01d..ceebb4f 100644 --- a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java +++ b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java @@ -20,6 +20,8 @@ import com.android.tools.idea.run.CloudConfiguration; import com.android.tools.idea.run.CloudConfiguration.Kind; import com.android.tools.idea.run.CloudConfigurationProvider; import com.android.tools.idea.sdk.IdeSdks; +//import com.glavsoft.viewer.Viewer; +import com.google.api.client.util.Maps; import com.google.api.client.util.Sets; import com.google.api.services.storage.Storage; import com.google.api.services.storage.model.Buckets; @@ -39,12 +41,10 @@ import com.google.gct.testing.launcher.CloudTestsLauncher; import com.google.gct.testing.results.GoogleCloudTestListener; import com.google.gct.testing.results.GoogleCloudTestResultsConnectionUtil; import com.google.gct.testing.results.GoogleCloudTestingResultParser; -import com.google.gct.testing.ui.GhostCloudDevice; import com.intellij.execution.DefaultExecutionResult; import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; -import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.ui.ConsoleView; import com.intellij.openapi.module.Module; @@ -73,6 +73,8 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { private static final String TEST_RUN_ID_PREFIX = "GoogleCloudTest:"; private static final Map<String, CloudConfigurationImpl> testRunIdToCloudConfiguration = new HashMap<String, CloudConfigurationImpl>(); private static final Map<String, CloudResultsAdapter> testRunIdToCloudResultsAdapter = new HashMap<String, CloudResultsAdapter>(); + // Do not use MultiMap to ensure proper reuse of serial numbers (IP:port). + private static final Map<String, String> serialNumberToConfigurationInstance = Maps.newHashMap(); public static final Icon DEFAULT_ICON = AndroidIcons.AndroidFile; @@ -93,7 +95,11 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { }; - private final Set<GhostCloudDevice> ghostCloudDevices = Sets.newHashSet(); + private static String lastCloudProjectId; + + private static final Set<GhostCloudDevice> ghostCloudDevices = Sets.newHashSet(); + + private static CloudConfigurationProviderImpl instance = null; public static Map<String, List<? extends CloudTestingType>> getAllDimensionTypes() { Map<String, List<? extends CloudTestingType>> dimensionTypes = new HashMap<String, List<? extends CloudTestingType>>(); @@ -104,6 +110,13 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { return dimensionTypes; } + public static CloudConfigurationProviderImpl getInstance() { + if (instance == null) { + instance = new CloudConfigurationProviderImpl(); + } + return instance; + } + @NotNull @Override public List<? extends CloudConfiguration> getCloudConfigurations(@NotNull AndroidFacet facet, @NotNull final Kind configurationKind) { @@ -262,8 +275,13 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { return; } - String[] dimensionValues = - cloudConfiguration.computeConfigurationInstances(ConfigurationInstance.ENCODED_NAME_DELIMITER).get(0).split("-"); + lastCloudProjectId = cloudProjectId; + String configurationInstance = cloudConfiguration.computeConfigurationInstances(ConfigurationInstance.ENCODED_NAME_DELIMITER).get(0); + launchCloudDevice(configurationInstance); + } + + public void launchCloudDevice(String configurationInstance) { + String[] dimensionValues = configurationInstance.split("-"); Device device = new Device().setAndroidDevice( new AndroidDevice() .setAndroidModelId(dimensionValues[0]) @@ -273,12 +291,12 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { Device createdDevice = null; try { - createdDevice = getTest().projects().devices().create(cloudProjectId, device).execute(); + createdDevice = getTest().projects().devices().create(lastCloudProjectId, device).execute(); } catch (IOException e) { CloudTestingUtils.showErrorMessage(null, "Error launching a cloud device", "Failed to launch a cloud device!\n" + - "Exception while launching a cloud device\n\n" + - e.getMessage()); + "Exception while launching a cloud device\n\n" + + e.getMessage()); return; } if (createdDevice == null) { @@ -297,21 +315,35 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { File dir = new File(sdkPath); try { while (System.currentTimeMillis() < stopTime) { - createdDevice = getTest().projects().devices().get(cloudProjectId, createdDevice.getId()).execute(); + createdDevice = getTest().projects().devices().get(lastCloudProjectId, createdDevice.getId()).execute(); System.out.println("Polling for device... (time: " + System.currentTimeMillis() + ", status: " + createdDevice.getState() + ")"); if (createdDevice.getState().equals("READY")) { //if (createdDevice.getDeviceDetails().getConnectionInfo() != null) { String ipAddress = createdDevice.getDeviceDetails().getConnectionInfo().getIpAddress(); Integer adbPort = createdDevice.getDeviceDetails().getConnectionInfo().getAdbPort(); - System.out.println("Device ready with IP address:port " + ipAddress + ":" + adbPort); + Integer vncPort = createdDevice.getDeviceDetails().getConnectionInfo().getVncPort(); + String vncPassword = createdDevice.getDeviceDetails().getConnectionInfo().getVncPassword(); + //TODO: Remove this temporary password. + if ("abc".length() > 1) { + vncPassword = "zeecloud"; + } + String deviceAddress = ipAddress + ":" + adbPort; + System.out.println("Device ready with IP address:port " + deviceAddress); Runtime rt = Runtime.getRuntime(); - Process connect = rt.exec("./adb connect " + ipAddress + ":" + adbPort, null, dir); + Process connect = rt.exec("./adb connect " + deviceAddress, null, dir); connect.waitFor(); + serialNumberToConfigurationInstance.put(deviceAddress, configurationInstance); // Do not wait for "finally" to remove the ghost device // to minimize the time both a ghost device and an actual cloud device are present in the devices table. synchronized (ghostCloudDevices) { ghostCloudDevices.remove(ghostCloudDevice); } + // Make sure the device is unlocked. + Process unlock = rt.exec("./adb -s " + deviceAddress + " wait-for-device shell input keyevent 82" , null, dir); + unlock.waitFor(); + // Open the VNC window for the cloud device. + //String[] viewerArgs = new String[]{"-port=" + vncPort, "-host=" + ipAddress, "-password=" + vncPassword, "-fullScreen=false"}; + //Viewer.main(viewerArgs); return; } Thread.sleep(POLLING_INTERVAL); @@ -330,6 +362,10 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { } } + public static String getConfigurationInstanceForSerialNumber(String serialNumber) { + return serialNumberToConfigurationInstance.get(serialNumber); + } + @NotNull @Override public Collection<IDevice> getLaunchingCloudDevices() { @@ -365,6 +401,8 @@ public class CloudConfigurationProviderImpl extends CloudConfigurationProvider { return null; } + lastCloudProjectId = cloudProjectId; + AndroidTestRunConfiguration testRunConfiguration = (AndroidTestRunConfiguration) runningState.getConfiguration(); AndroidTestConsoleProperties properties = new AndroidTestConsoleProperties(testRunConfiguration, executor); diff --git a/src/com/google/gct/testing/CloudDebug.png b/src/com/google/gct/testing/CloudDebug.png Binary files differnew file mode 100644 index 0000000..bd92d58 --- /dev/null +++ b/src/com/google/gct/testing/CloudDebug.png diff --git a/src/com/google/gct/testing/CloudResultsLoader.java b/src/com/google/gct/testing/CloudResultsLoader.java index c5a8e04..b20b2ec 100644 --- a/src/com/google/gct/testing/CloudResultsLoader.java +++ b/src/com/google/gct/testing/CloudResultsLoader.java @@ -328,6 +328,10 @@ public class CloudResultsLoader { throw new RuntimeException("Failed to retrieve bucket objects: ", e); } + if (storageObjects == null) { + return; + } + Iterable<BucketFileMetadata> files = Iterables.transform(storageObjects, TO_BUCKET_FILE); //ArrayList<ScreenshotDownloadThread> downloadThreads = new ArrayList<ScreenshotDownloadThread>(); for (BucketFileMetadata file : files) { diff --git a/src/com/google/gct/testing/DebugConfigurationAction.java b/src/com/google/gct/testing/DebugConfigurationAction.java new file mode 100644 index 0000000..3b43cde --- /dev/null +++ b/src/com/google/gct/testing/DebugConfigurationAction.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.google.gct.testing; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.google.gct.testing.results.GoogleCloudTestTreeView; +import com.google.gct.testing.results.GoogleCloudTestingResultsForm; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.RunnerRegistry; +import com.intellij.execution.configurations.RunProfile; +import com.intellij.execution.executors.DefaultDebugExecutor; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.ExecutionEnvironmentBuilder; +import com.intellij.execution.runners.ProgramRunner; +import com.intellij.execution.testframework.AbstractTestProxy; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.LangDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.MessageType; +import org.jetbrains.android.run.TargetSelectionMode; +import org.jetbrains.android.run.testing.AndroidTestRunConfiguration; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.io.IOException; + +public class DebugConfigurationAction extends AnAction { + + private final static String TEXT = "Debug Configuration in Cloud"; + private final static String DESCRIPTION = "Debug Configuration on a Cloud Device"; + private static Icon ICON; + + static { + try { + ICON = new ImageIcon(ImageIO.read(DebugConfigurationAction.class.getResourceAsStream("CloudDebug.png"))); + } + catch (Throwable e) { // If something goes wrong, just use the original debug icon. + ICON = AllIcons.General.Debug; + } + } + + + public DebugConfigurationAction() { + super(TEXT, DESCRIPTION, ICON); + } + + @Override + public void actionPerformed(final AnActionEvent actionEvent) { + ExecutionEnvironment environment = actionEvent.getData(LangDataKeys.EXECUTION_ENVIRONMENT); + GoogleCloudTestTreeView sender = actionEvent.getData(GoogleCloudTestTreeView.CLOUD_TEST_RUNNER_VIEW); + + if (environment == null || sender == null) { + return; + } + + Project project = actionEvent.getData(PlatformDataKeys.PROJECT); + + AbstractTestProxy selectedLeaf = + getFirstLeaf(((GoogleCloudTestingResultsForm)sender.getResultsViewer()).getTreeView().getSelectedTest()); + + if (selectedLeaf.getParent() == null || selectedLeaf.getParent().getParent() == null) { + //This leaf is not a test. Most probably it is a pending configuration, so ignore. + CloudTestingUtils.showBalloonMessage(project, "Tests were not yet executed for this configuration", MessageType.WARNING, 3); + return; + } + + AbstractTestProxy selectedConfigurationNode = selectedLeaf.getParent().getParent(); + ConfigurationInstance configurationInstance = + ConfigurationInstance.parseFromResultsViewerDisplayString(selectedConfigurationNode.getName()); + + ApplicationManager.getApplication().executeOnPooledThread(new DebuggingStater(environment, project, configurationInstance)); + } + + private class DebuggingStater extends Thread { + private final ExecutionEnvironment environment; + private final Project project; + private final ConfigurationInstance configurationInstance; + private final RunProfile runProfile; + private final ProgramRunner runner; + + private DebuggingStater(ExecutionEnvironment environment, Project project, ConfigurationInstance configurationInstance) { + this.environment = environment; + this.project = project; + this.configurationInstance = configurationInstance; + runProfile = environment.getRunProfile(); + runner = RunnerRegistry.getInstance().getRunner(DefaultDebugExecutor.getDebugExecutorInstance().getId(), runProfile); + } + + @Override + public void run() { + if (!(runProfile instanceof AndroidTestRunConfiguration)) { + return; + } + IDevice device = getMatchingDevice(); + if (device instanceof GhostCloudDevice) { + // Wait a bit, giving the device sometime to become ready, and then try again. + try { + Thread.sleep(3000); // 3 seconds + } + catch (InterruptedException e) { + //ignore + } + run(); + } else { + if (device == null) { + // Did not find a device, so start a new one. + CloudConfigurationProviderImpl.getInstance().launchCloudDevice(configurationInstance.getEncodedString()); + device = getMatchingDevice(); + // Should not happen unless the user closes the corresponding VNC window thus killing the device before it is booted. + if (device == null) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + CloudTestingUtils.showBalloonMessage(project, "Could not find a launched cloud device!", MessageType.WARNING, 10); + } + }); + return; + } + } + + // Clone the run configuration such that we do not need to reuse and restore the original one. + final AndroidTestRunConfiguration runConfiguration = (AndroidTestRunConfiguration) ((AndroidTestRunConfiguration)runProfile).clone(); + runConfiguration.setTargetSelectionMode(TargetSelectionMode.CLOUD_DEVICE_DEBUGGING); + runConfiguration.CLOUD_DEVICE_SERIAL_NUMBER = device.getSerialNumber(); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + runner.execute(new ExecutionEnvironmentBuilder(environment) + .executor(DefaultDebugExecutor.getDebugExecutorInstance()) + .runProfile(runConfiguration) + .build()); + } catch (ExecutionException e) { + CloudTestingUtils.showBalloonMessage(project, "Failed to start debugging on a cloud device: " + + runConfiguration.CLOUD_DEVICE_SERIAL_NUMBER, MessageType.WARNING, 10); + } + } + }); + } + } + + private IDevice getMatchingDevice() { + for (IDevice device : AndroidDebugBridge.getBridge().getDevices()) { + String deviceConfigurationInstance = + CloudConfigurationProviderImpl.getConfigurationInstanceForSerialNumber(device.getSerialNumber()); + if (configurationInstance.getEncodedString().equals(deviceConfigurationInstance)) { + return device; + } + } + for (IDevice device : CloudConfigurationProviderImpl.getInstance().getLaunchingCloudDevices()) { + if (device.getSerialNumber().equals(configurationInstance.getEncodedString().toLowerCase())) { + return device; + } + } + return null; + } + } + + private AbstractTestProxy getFirstLeaf(AbstractTestProxy testNode) { + while (!testNode.isLeaf()) { + return getFirstLeaf(testNode.getChildren().get(0)); + } + return testNode; + } + +} diff --git a/src/com/google/gct/testing/ui/GhostCloudDevice.java b/src/com/google/gct/testing/GhostCloudDevice.java index 5f2c81d..cdaa3c1 100644 --- a/src/com/google/gct/testing/ui/GhostCloudDevice.java +++ b/src/com/google/gct/testing/GhostCloudDevice.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.gct.testing.ui; +package com.google.gct.testing; import com.android.annotations.NonNull; import com.android.annotations.Nullable; diff --git a/src/com/google/gct/testing/results/GoogleCloudTestRunnerToolbarPanel.java b/src/com/google/gct/testing/results/GoogleCloudTestRunnerToolbarPanel.java index 67cfcfc..0fd0240 100644 --- a/src/com/google/gct/testing/results/GoogleCloudTestRunnerToolbarPanel.java +++ b/src/com/google/gct/testing/results/GoogleCloudTestRunnerToolbarPanel.java @@ -16,6 +16,7 @@ package com.google.gct.testing.results; +import com.google.gct.testing.DebugConfigurationAction; import com.google.gct.testing.ShowScreenshotsAction; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.testframework.TestConsoleProperties; @@ -40,6 +41,7 @@ public class GoogleCloudTestRunnerToolbarPanel extends SMTRunnerToolbarPanel { super.appendAdditionalActions(actionGroup, properties, environment, parent); actionGroup.addAction(new ShowScreenshotsAction()); + actionGroup.addAction(new DebugConfigurationAction()); } } |