• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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