1 /* 2 * Copyright (C) 2024 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.tradefed.result.skipped; 17 import com.android.tradefed.config.IConfiguration; 18 import com.android.tradefed.config.IConfigurationReceiver; 19 import com.android.tradefed.invoker.InvocationContext; 20 import com.android.tradefed.invoker.TestInformation; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.service.IRemoteFeature; 23 import com.android.tradefed.service.TradefedFeatureClient; 24 import com.android.tradefed.testtype.ITestInformationReceiver; 25 26 import build.bazel.remote.execution.v2.Digest; 27 28 import com.google.common.base.Joiner; 29 import com.google.common.base.Strings; 30 import com.google.protobuf.InvalidProtocolBufferException; 31 import com.proto.tradefed.feature.ErrorInfo; 32 import com.proto.tradefed.feature.FeatureRequest; 33 import com.proto.tradefed.feature.FeatureResponse; 34 import com.proto.tradefed.feature.MultiPartResponse; 35 import com.proto.tradefed.feature.PartResponse; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.LinkedHashMap; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Map.Entry; 45 import java.util.Set; 46 47 /** A feature allowing to access some of the {@link SkipManager} information. */ 48 public class SkipFeature 49 implements IRemoteFeature, IConfigurationReceiver, ITestInformationReceiver { 50 public static final String SKIP_FEATURE = "skipFeature"; 51 public static final String SKIPPED_MODULES = "skipModules"; 52 public static final String IMAGE_DIGESTS = "imageDigests"; 53 public static final String PRESUBMIT = "presubmit"; 54 public static final String DELIMITER_NAME = "delimiter"; 55 private static final String DELIMITER = "+,"; 56 private static final String ESCAPED_DELIMITER = "\\+,"; 57 private IConfiguration mConfig; 58 private TestInformation mInfo; 59 60 @Override setConfiguration(IConfiguration configuration)61 public void setConfiguration(IConfiguration configuration) { 62 mConfig = configuration; 63 } 64 65 @Override setTestInformation(TestInformation testInformation)66 public void setTestInformation(TestInformation testInformation) { 67 mInfo = testInformation; 68 } 69 70 @Override getTestInformation()71 public TestInformation getTestInformation() { 72 return mInfo; 73 } 74 75 @Override getName()76 public String getName() { 77 return SKIP_FEATURE; 78 } 79 80 @Override execute(FeatureRequest request)81 public FeatureResponse execute(FeatureRequest request) { 82 FeatureResponse.Builder responseBuilder = FeatureResponse.newBuilder(); 83 if (mConfig != null) { 84 boolean presubmit = InvocationContext.isPresubmit(mInfo.getContext()); 85 MultiPartResponse.Builder multiPartBuilder = MultiPartResponse.newBuilder(); 86 multiPartBuilder.addResponsePart( 87 PartResponse.newBuilder().setKey(DELIMITER_NAME).setValue(ESCAPED_DELIMITER)); 88 multiPartBuilder.addResponsePart( 89 PartResponse.newBuilder() 90 .setKey(PRESUBMIT) 91 .setValue(Boolean.toString(presubmit))); 92 multiPartBuilder.addResponsePart( 93 PartResponse.newBuilder() 94 .setKey(SKIPPED_MODULES) 95 .setValue( 96 Joiner.on(DELIMITER) 97 .join(mConfig.getSkipManager().getUnchangedModules()))); 98 multiPartBuilder.addResponsePart( 99 PartResponse.newBuilder() 100 .setKey(IMAGE_DIGESTS) 101 .setValue( 102 Joiner.on(DELIMITER) 103 .join( 104 serializeDigest( 105 mConfig.getSkipManager() 106 .getImageToDigest())))); 107 responseBuilder.setMultiPartResponse(multiPartBuilder); 108 } else { 109 responseBuilder.setErrorInfo( 110 ErrorInfo.newBuilder().setErrorTrace("Configuration not set.")); 111 } 112 return responseBuilder.build(); 113 } 114 115 /** Fetch and populate unchanged modules if needed. */ getSkipContext()116 public static SkipContext getSkipContext() { 117 boolean isPresubmit = false; 118 Set<String> unchangedModulesSet = new HashSet<>(); 119 Map<String, Digest> imageToDigestMap = new LinkedHashMap<String, Digest>(); 120 try (TradefedFeatureClient client = new TradefedFeatureClient()) { 121 FeatureResponse unchangedModules = 122 client.triggerFeature(SkipFeature.SKIP_FEATURE, new HashMap<String, String>()); 123 if (unchangedModules.hasMultiPartResponse()) { 124 String delimiter = DELIMITER; 125 for (PartResponse rep : 126 unchangedModules.getMultiPartResponse().getResponsePartList()) { 127 if (rep.getKey().equals(DELIMITER_NAME)) { 128 delimiter = rep.getValue().trim(); 129 } 130 } 131 for (PartResponse rep : 132 unchangedModules.getMultiPartResponse().getResponsePartList()) { 133 if (rep.getKey().equals(SKIPPED_MODULES)) { 134 unchangedModulesSet.addAll(splitStringFilters(delimiter, rep.getValue())); 135 } else if (rep.getKey().equals(PRESUBMIT)) { 136 isPresubmit = Boolean.parseBoolean(rep.getValue()); 137 } else if (rep.getKey().equals(IMAGE_DIGESTS)) { 138 imageToDigestMap = parseDigests(delimiter, rep.getValue()); 139 } else if (rep.getKey().equals(DELIMITER_NAME)) { 140 // Ignore 141 } else { 142 CLog.w("Unexpected response key '%s' for unchanged modules", rep.getKey()); 143 } 144 } 145 } else { 146 CLog.w("Unexpected response for unchanged modules: %s", unchangedModules); 147 } 148 } catch (Exception e) { 149 CLog.e(e); 150 } 151 return new SkipContext(isPresubmit, unchangedModulesSet, imageToDigestMap); 152 } 153 splitStringFilters(String delimiter, String value)154 private static List<String> splitStringFilters(String delimiter, String value) { 155 if (Strings.isNullOrEmpty(value)) { 156 return new ArrayList<String>(); 157 } 158 return Arrays.asList(value.split(delimiter)); 159 } 160 serializeDigest(Map<String, Digest> imageToDigest)161 private static List<String> serializeDigest(Map<String, Digest> imageToDigest) { 162 List<String> serializedItems = new ArrayList<>(); 163 for (Entry<String, Digest> entry : imageToDigest.entrySet()) { 164 if (entry.getValue() == null) { 165 serializedItems.add(String.format("%s=null=null", entry.getKey())); 166 } else { 167 serializedItems.add( 168 String.format( 169 "%s=%s=%s", 170 entry.getKey(), 171 entry.getValue().getHash(), 172 entry.getValue().getSizeBytes())); 173 } 174 } 175 return serializedItems; 176 } 177 parseDigests(String delimiter, String serializedString)178 public static Map<String, Digest> parseDigests(String delimiter, String serializedString) 179 throws InvalidProtocolBufferException { 180 Map<String, Digest> imageToDigest = new LinkedHashMap<>(); 181 if (Strings.isNullOrEmpty(serializedString)) { 182 return imageToDigest; 183 } 184 for (String sub : serializedString.split(delimiter)) { 185 String[] keyValue = sub.split("="); 186 if ("null".equals(keyValue[1])) { 187 imageToDigest.put(keyValue[0], null); 188 } else { 189 imageToDigest.put( 190 keyValue[0], 191 Digest.newBuilder() 192 .setHash(keyValue[1]) 193 .setSizeBytes(Long.parseLong(keyValue[2])) 194 .build()); 195 } 196 } 197 return imageToDigest; 198 } 199 } 200