1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.cuttlefish.test; 17 18 import static com.google.common.collect.ImmutableList.toImmutableList; 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertTrue; 21 import static org.junit.Assume.assumeNoException; 22 import static org.junit.Assume.assumeNotNull; 23 import static org.junit.Assume.assumeTrue; 24 25 import com.android.cuttlefish.test.DataType; 26 import com.android.cuttlefish.test.Exit; 27 import com.android.cuttlefish.test.TestMessage; 28 import com.android.ddmlib.Log.LogLevel; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.log.LogUtil.CLog; 31 import com.android.tradefed.util.FileUtil; 32 import com.google.auto.value.AutoValue; 33 import com.google.common.collect.ImmutableList; 34 import com.google.inject.Inject; 35 import com.google.protobuf.ByteString; 36 import java.io.BufferedReader; 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.InputStreamReader; 44 import java.io.OutputStream; 45 import java.util.UUID; 46 import javax.annotation.Nullable; 47 import org.junit.rules.TestRule; 48 import org.junit.runner.Description; 49 import org.junit.runners.model.Statement; 50 51 /** 52 * Manager for a dedicated GCE instance for every @Test function. 53 * 54 * Must be constructed through Guice injection. Calls out to the cvd_test_gce_driver binary to 55 * create the GCE instances. 56 */ 57 public final class GceInstanceRule implements TestRule { 58 @Inject(optional = true) 59 @SetOption("gce-driver-service-account-json-key-path") 60 @Nullable 61 private String gceJsonKeyPath = null; 62 63 @Inject(optional = true) @SetOption("cloud-project") private String cloudProject; 64 65 @Inject(optional = true) @SetOption("zone") private String zone = "us-west1-a"; 66 67 @Inject(optional = true) 68 @SetOption("internal-addresses") 69 private boolean internal_addresses = false; 70 71 @Inject private TestInformation testInfo; 72 @Inject private BuildChooser buildChooser; 73 74 private Process driverProcess; 75 private String managedInstance; 76 launchDriver(File gceDriver)77 private Process launchDriver(File gceDriver) throws IOException { 78 ImmutableList.Builder<String> cmdline = new ImmutableList.Builder(); 79 cmdline.add(gceDriver.toString()); 80 assumeNotNull(gceJsonKeyPath); 81 cmdline.add("--internal-addresses=" + internal_addresses); 82 cmdline.add("--cloud-project=" + cloudProject); 83 cmdline.add("--service-account-json-private-key-path=" + gceJsonKeyPath); 84 ProcessBuilder processBuilder = new ProcessBuilder(cmdline.build()); 85 processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE); 86 processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE); 87 processBuilder.redirectError(ProcessBuilder.Redirect.PIPE); 88 return processBuilder.start(); 89 } 90 launchLogger(InputStream input)91 private static Thread launchLogger(InputStream input) { 92 BufferedReader reader = new BufferedReader(new InputStreamReader(input)); 93 Thread logThread = new Thread(() -> { 94 try { 95 String line; 96 while ((line = reader.readLine()) != null) { 97 CLog.logAndDisplay(LogLevel.DEBUG, "cvd_test_gce_driver output: %s", line); 98 } 99 } catch (Exception e) { 100 CLog.logAndDisplay(LogLevel.DEBUG, "cvd_test_gce_driver exception: %s", e); 101 } 102 }); 103 logThread.start(); 104 return logThread; 105 } 106 createInstance()107 private String createInstance() throws IOException { 108 String desiredName = "cuttlefish-integration-" + UUID.randomUUID(); 109 TestMessage.Builder request = TestMessage.newBuilder(); 110 request.getCreateInstanceBuilder().getIdBuilder().setName(desiredName); 111 request.getCreateInstanceBuilder().getIdBuilder().setZone(zone); 112 sendMessage(request.build()); 113 ImmutableList<TestMessage> errors = 114 collectResponses().stream().filter(TestMessage::hasError).collect(toImmutableList()); 115 if (errors.size() > 0) { 116 throw new IOException("Failed to create instance: " + errors); 117 } 118 return desiredName; 119 } 120 121 @AutoValue 122 public static abstract class SshResult { create(File stdout, File stderr, int ret)123 public static SshResult create(File stdout, File stderr, int ret) { 124 return new AutoValue_GceInstanceRule_SshResult(stdout, stderr, ret); 125 } 126 stdout()127 public abstract File stdout(); stderr()128 public abstract File stderr(); returnCode()129 public abstract int returnCode(); 130 } 131 ssh(String... command)132 public SshResult ssh(String... command) throws IOException { 133 return ssh(ImmutableList.copyOf(command)); 134 } 135 ssh(ImmutableList<String> command)136 public SshResult ssh(ImmutableList<String> command) throws IOException { 137 TestMessage.Builder request = TestMessage.newBuilder(); 138 request.getSshCommandBuilder().getInstanceBuilder().setName(managedInstance); 139 request.getSshCommandBuilder().addAllArguments(command); 140 sendMessage(request.build()); 141 File stdout = FileUtil.createTempFile("ssh_", "_stdout.txt"); 142 OutputStream stdoutStream = new FileOutputStream(stdout); 143 File stderr = FileUtil.createTempFile("ssh_", "_stderr.txt"); 144 OutputStream stderrStream = new FileOutputStream(stderr); 145 int returnCode = -1; 146 IOException storedException = null; 147 while (true) { 148 TestMessage response = receiveMessage(); 149 switch (response.getContentsCase()) { 150 case DATA: 151 if (response.getData().getType().equals(DataType.DATA_TYPE_STDOUT)) { 152 stdoutStream.write(response.getData().getContents().toByteArray()); 153 } else if (response.getData().getType().equals(DataType.DATA_TYPE_STDERR)) { 154 stderrStream.write(response.getData().getContents().toByteArray()); 155 } else if (response.getData().getType().equals(DataType.DATA_TYPE_RETURN_CODE)) { 156 returnCode = Integer.valueOf(response.getData().getContents().toStringUtf8()); 157 } else { 158 throw new RuntimeException("Unexpected type: " + response.getData().getType()); 159 } 160 break; 161 case ERROR: 162 if (storedException == null) { 163 storedException = new IOException(response.getError().getText()); 164 } else { 165 storedException.addSuppressed(new IOException(response.getError().getText())); 166 } 167 case STREAM_END: 168 if (storedException == null) { 169 return SshResult.create(stdout, stderr, returnCode); 170 } else { 171 throw storedException; 172 } 173 default: { 174 IOException exception = new IOException("Unexpected message: " + response); 175 if (storedException == null) { 176 exception.addSuppressed(storedException); 177 } 178 throw exception; 179 } 180 } 181 } 182 } 183 uploadFile(File sourceFile, String destFile)184 public void uploadFile(File sourceFile, String destFile) throws IOException { 185 TestMessage.Builder request = TestMessage.newBuilder(); 186 request.getUploadFileBuilder().getInstanceBuilder().setName(managedInstance); 187 request.getUploadFileBuilder().setRemotePath(destFile); 188 189 // Allow this to error out before initiating the transfer 190 FileInputStream stream = new FileInputStream(sourceFile); 191 sendMessage(request.build()); 192 byte[] buffer = new byte[1 << 14 /* 16 KiB */]; 193 int read = 0; 194 while ((read = stream.read(buffer)) != -1) { 195 TestMessage.Builder dataMessage = TestMessage.newBuilder(); 196 dataMessage.getDataBuilder().setType(DataType.DATA_TYPE_FILE_CONTENTS); 197 dataMessage.getDataBuilder().setContents(ByteString.copyFrom(buffer, 0, read)); 198 sendMessage(dataMessage.build()); 199 } 200 TestMessage.Builder endRequest = TestMessage.newBuilder(); 201 endRequest.setStreamEnd(StreamEnd.getDefaultInstance()); 202 sendMessage(endRequest.build()); 203 ImmutableList<TestMessage> errors = 204 collectResponses().stream().filter(TestMessage::hasError).collect(toImmutableList()); 205 if (errors.size() > 0) { 206 throw new IOException("Failed to upload file: " + errors); 207 } 208 } 209 uploadBuildArtifact(String artifact, String destFile)210 public void uploadBuildArtifact(String artifact, String destFile) throws IOException { 211 TestMessage.Builder request = TestMessage.newBuilder(); 212 request.getUploadBuildArtifactBuilder().getInstanceBuilder().setName(managedInstance); 213 request.getUploadBuildArtifactBuilder().setBuild(buildChooser.buildProto()); 214 request.getUploadBuildArtifactBuilder().setArtifactName(artifact); 215 request.getUploadBuildArtifactBuilder().setRemotePath(destFile); 216 sendMessage(request.build()); 217 ImmutableList<TestMessage> errors = 218 collectResponses().stream().filter(TestMessage::hasError).collect(toImmutableList()); 219 if (errors.size() > 0) { 220 throw new IOException("Failed to upload build artifact: " + errors); 221 } 222 } 223 receiveMessage()224 private TestMessage receiveMessage() throws IOException { 225 TestMessage message = TestMessage.parser().parseDelimitedFrom(driverProcess.getInputStream()); 226 CLog.logAndDisplay(LogLevel.DEBUG, "Received message \"" + message + "\""); 227 return message; 228 } 229 collectResponses()230 private ImmutableList<TestMessage> collectResponses() throws IOException { 231 ImmutableList.Builder<TestMessage> messages = ImmutableList.builder(); 232 while (true) { 233 TestMessage received = receiveMessage(); 234 messages.add(received); 235 if (received.hasStreamEnd()) { 236 return messages.build(); 237 } 238 } 239 } 240 sendMessage(TestMessage message)241 private void sendMessage(TestMessage message) throws IOException { 242 CLog.logAndDisplay(LogLevel.DEBUG, "Sending message \"" + message + "\""); 243 message.writeDelimitedTo(driverProcess.getOutputStream()); 244 driverProcess.getOutputStream().flush(); 245 } 246 247 @Override apply(Statement base, Description description)248 public Statement apply(Statement base, Description description) { 249 final File gceDriver; 250 try { 251 gceDriver = testInfo.getDependencyFile("cvd_test_gce_driver", false); 252 } catch (FileNotFoundException e) { 253 assumeNoException("Could not find cvd_test_gce_driver", e); 254 return null; 255 } 256 assumeTrue("cvd_test_gce_driver file did not exist", gceDriver.exists()); 257 return new Statement() { 258 @Override 259 public void evaluate() throws Throwable { 260 // TODO(schuffelen): Reuse instances with GCE resets. 261 // The trick will be figuring out when the instances can actually be destroyed. 262 driverProcess = launchDriver(gceDriver); 263 Thread logStderr = launchLogger(driverProcess.getErrorStream()); 264 managedInstance = createInstance(); 265 try { 266 base.evaluate(); 267 } finally { 268 boolean cleanExit = false; 269 for (int i = 0; i < 10; i++) { 270 sendMessage(TestMessage.newBuilder().setExit(Exit.getDefaultInstance()).build()); 271 TestMessage response = receiveMessage(); 272 if (response.hasExit()) { 273 cleanExit = true; 274 break; 275 } else if (!response.hasError() 276 && !response.hasStreamEnd()) { // Swallow some errors to to get out if necessary 277 throw new AssertionError("Unexpected message " + response); 278 } 279 } 280 assertTrue("Failed to get an exit response", cleanExit); 281 assertEquals(0, driverProcess.waitFor()); 282 logStderr.join(); 283 driverProcess = null; 284 } 285 } 286 }; 287 } 288 } 289