1 /*
2  * Copyright (C) 2023 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.testtype.binary;
17 
18 import com.android.tradefed.config.Option;
19 import com.android.tradefed.config.OptionClass;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.device.DeviceRuntimeException;
22 import com.android.tradefed.device.ITestDevice;
23 import com.android.tradefed.invoker.TestInformation;
24 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
25 import com.android.tradefed.log.LogUtil.CLog;
26 import com.android.tradefed.result.FailureDescription;
27 import com.android.tradefed.result.ITestInvocationListener;
28 import com.android.tradefed.result.TestDescription;
29 import com.android.tradefed.result.error.DeviceErrorIdentifier;
30 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
31 import com.android.tradefed.util.CommandResult;
32 import com.android.tradefed.util.CommandStatus;
33 
34 import java.io.IOException;
35 import java.util.HashMap;
36 import java.util.LinkedHashMap;
37 import java.util.Map;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 
41 /** Test runner for executable running on the target and parsing tesult of kernel test. */
42 @OptionClass(alias = "kernel-target-test")
43 public class KernelTargetTest extends ExecutableTargetTest {
44     private Integer mCurrKver = null;
45     private Pattern mKverPattern = Pattern.compile("(\\d+)\\.(\\d+)(?:\\.(\\d+))?");
46 
47     /**
48      * @deprecated use skip-binary-check instead. Left for backwards compatibility.
49      */
50     @Deprecated
51     @Option(
52             name = "ignore-binary-check",
53             description = "Deprecated - use skip-binary-check instead.")
54     private boolean mIgnoreBinaryCheck = false;
55 
56     @Option(name = "exit-code-skip", description = "Exit code for skipped tests.")
57     private Integer mExitCodeSkip = null;
58 
59     @Option(
60             name = "min-kernel-version",
61             description =
62                     "The minimum kernel version needed to run a test.  The test name should be the"
63                         + " key and the minimum kernel version should be the value.  Should contain"
64                         + " at least the kernel version and major revision, and optionally the"
65                         + " minor revision, separated by periods, e.g. 5.4 or 4.19.1")
66     private Map<String, String> mTestMinKernelVersion = new LinkedHashMap<>();
67 
68     @Option(name = "parse-ktap", description = "Parse test outputs in KTAP format")
69     private boolean mParseKTAP = false;
70 
71     @Override
doesRunBinaryGenerateTestResults()72     protected boolean doesRunBinaryGenerateTestResults() {
73         return mParseKTAP;
74     }
75 
76     /**
77      * Skips the binary check in findBinary. Redundant with mSkipBinaryCheck but needed for
78      * backwards compatibility.
79      */
80     @Override
findBinary(String binary)81     public String findBinary(String binary) throws DeviceNotAvailableException {
82         if (mIgnoreBinaryCheck) return binary;
83         return super.findBinary(binary);
84     }
85 
86     /**
87      * Parse the kernel version, major revision, and, optionally, the minimum revision from a
88      * version string into a single integer that can used for numerical comparison.
89      *
90      * @param version linux version string.
91      */
parseKernelVersion(String version)92     public Integer parseKernelVersion(String version) {
93         Matcher m = mKverPattern.matcher(version);
94         if (m.find()) {
95             Integer v1 = Integer.valueOf(m.group(1));
96             Integer v2 = Integer.valueOf(m.group(2));
97             Integer v3 = m.group(3) == null ? 0 : Integer.valueOf(m.group(3));
98             return (v1 << 20) + (v2 << 10) + v3;
99         }
100         throw new IllegalArgumentException("Invalid kernel version string: " + version);
101     }
102 
103     /**
104      * Check if the kernel version meets or exceeds the minimum kernel version for this test.
105      *
106      * @param minKernelVersion the min version string from the config.
107      */
compareKernelVersion(String minKernelVersion)108     public boolean compareKernelVersion(String minKernelVersion) {
109         Integer minKver = parseKernelVersion(minKernelVersion);
110         return mCurrKver >= minKver;
111     }
112 
113     /** Get the device kernel version with uname -r. */
getDeviceKernelVersion()114     public Integer getDeviceKernelVersion() throws DeviceNotAvailableException {
115         ITestDevice device = getDevice();
116         if (device == null) {
117             throw new IllegalArgumentException("Device has not been set");
118         }
119         CommandResult result = device.executeShellV2Command("uname -r");
120         if (result.getStatus() != CommandStatus.SUCCESS) {
121             throw new DeviceRuntimeException(
122                     "Failed to get the kernel version with uname",
123                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
124         }
125         return parseKernelVersion(result.getStdout());
126     }
127 
128     @Override
runBinary( String binaryPath, ITestInvocationListener listener, TestDescription description)129     public void runBinary(
130             String binaryPath, ITestInvocationListener listener, TestDescription description)
131             throws DeviceNotAvailableException, IOException {
132         String testName = description.getTestName();
133         if (mTestMinKernelVersion.containsKey(testName)
134                 && !compareKernelVersion(mTestMinKernelVersion.get(testName))) {
135             listener.testIgnored(description);
136         } else {
137             super.runBinary(binaryPath, listener, description);
138         }
139     }
140 
141     @Override
run(TestInformation testInfo, ITestInvocationListener listener)142     public void run(TestInformation testInfo, ITestInvocationListener listener)
143             throws DeviceNotAvailableException {
144         if (!mTestMinKernelVersion.isEmpty()) {
145             mCurrKver = getDeviceKernelVersion();
146         }
147         super.run(testInfo, listener);
148     }
149 
150     /**
151      * Check the result of the test command.
152      *
153      * @param result test result of the command {@link CommandResult}
154      * @param listener the {@link ITestInvocationListener}
155      * @param description The test in progress.
156      */
157     @Override
checkCommandResult( CommandResult result, ITestInvocationListener listener, TestDescription description)158     protected void checkCommandResult(
159             CommandResult result, ITestInvocationListener listener, TestDescription description) {
160         if (mParseKTAP) {
161             if (result.getExitCode().equals(mExitCodeSkip)) {
162                 listener.testStarted(description);
163                 listener.testIgnored(description);
164                 listener.testEnded(description, new HashMap<String, Metric>());
165                 return;
166             }
167             try {
168                 KTapResultParser.applyKTapResultToListener(
169                         listener,
170                         description.getTestName(),
171                         result.getStdout(),
172                         KTapResultParser.ParseResolution.AGGREGATED_TOP_LEVEL);
173             } catch (RuntimeException exception) {
174                 CLog.e("KTAP parse error: %s", exception.toString());
175                 listener.testStarted(description);
176                 listener.testFailed(
177                         description,
178                         FailureDescription.create(exception.toString())
179                                 .setFailureStatus(FailureStatus.TEST_FAILURE));
180                 listener.testEnded(description, new HashMap<String, Metric>());
181             }
182             return;
183         }
184         if (result.getExitCode().equals(mExitCodeSkip)) {
185             listener.testIgnored(description);
186             return;
187         }
188         super.checkCommandResult(result, listener, description);
189     }
190 }
191