1#!/usr/bin/env python
2#
3# Copyright (C) 2017 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import gzip
19import logging
20import os
21import re
22import shutil
23import tempfile
24
25from vts.runners.host import asserts
26from vts.runners.host import base_test
27from vts.runners.host import const
28from vts.runners.host import keys
29from vts.runners.host import test_runner
30from vts.utils.python.controllers import android_device
31from vts.utils.python.file import file_utils
32
33
34class VtsKernelConfigTest(base_test.BaseTestClass):
35    """Test case which check config options in /proc/config.gz.
36
37    Attributes:
38        _temp_dir: The temporary directory to which /proc/config.gz is copied.
39    """
40
41    PROC_FILE_PATH = "/proc/config.gz"
42    KERNEL_CONFIG_FILE_PATH = "vts/testcases/kernel/config/data"
43    SUPPORTED_KERNEL_VERSIONS = ["3.18", "4.4", "4.9"]
44
45    def setUpClass(self):
46        required_params = [
47            keys.ConfigKeys.IKEY_DATA_FILE_PATH
48        ]
49        self.getUserParams(required_params)
50        self.dut = self.registerController(android_device)[0]
51        self.dut.shell.InvokeTerminal(
52            "KernelConfigTest")  # creates a remote shell instance.
53        self.shell = self.dut.shell.KernelConfigTest
54        self._temp_dir = tempfile.mkdtemp()
55
56    def checkKernelVersion(self):
57        """Validate the kernel version of DUT is a valid O kernel version.
58
59        Returns:
60            string, kernel version of device
61        """
62        cmd = "uname -a"
63        results = self.shell.Execute(cmd)
64        logging.info("Shell command '%s' results: %s", cmd, results)
65
66        match = re.search(r"\d+\.\d+", results[const.STDOUT][0])
67        if match is None:
68            asserts.fail("Failed to detect kernel version of device.")
69        else:
70            kernel_version = match.group(0)
71        logging.info("Detected kernel version: %s", kernel_version)
72
73        asserts.assertTrue(kernel_version in self.SUPPORTED_KERNEL_VERSIONS,
74                           "Detected kernel version '%s' is not one of %s"
75                           % (kernel_version, self.SUPPORTED_KERNEL_VERSIONS))
76
77        return kernel_version
78
79    def checkKernelArch(self, configs):
80        """Find arch of the device kernel.
81
82        Uses the kernel configuration to determine the architecture
83        it is compiled for.
84
85        Args:
86            configs: dict containing device kernel configuration options
87
88        Returns:
89            A string containing the architecture of the device kernel. If
90            the architecture cannot be determined, an empty string is
91            returned.
92        """
93
94        CONFIG_ARM = "CONFIG_ARM"
95        CONFIG_ARM64 = "CONFIG_ARM64"
96        CONFIG_X86 = "CONFIG_X86"
97
98        if CONFIG_ARM in configs and configs[CONFIG_ARM] == "y":
99            return "arm"
100        elif CONFIG_ARM64 in configs and configs[CONFIG_ARM64] == "y":
101            return "arm64"
102        elif CONFIG_X86 in configs and configs[CONFIG_X86] == "y":
103            return "x86"
104        else:
105            print "Unable to determine kernel architecture."
106            return ""
107
108    def parseConfigFileToDict(self, file, configs):
109        """Parse kernel config file to a dictionary.
110
111        Args:
112            file: file object, android-base.cfg or unzipped /proc/config.gz
113            configs: dict to which config options in file will be added
114
115        Returns:
116            dict: {config_name: config_state}
117        """
118        config_lines = [line.rstrip("\n") for line in file.readlines()]
119
120        for line in config_lines:
121            if line.startswith("#") and line.endswith("is not set"):
122                match = re.search(r"CONFIG_\S+", line)
123                if match is None:
124                    asserts.fail("Failed to parse config file")
125                else:
126                    config_name = match.group(0)
127                config_state = "n"
128            elif line.startswith("CONFIG_"):
129                config_name, config_state = line.split("=", 1)
130                if config_state.startswith(("'", '"')):
131                    config_state = config_state[1:-1]
132            else:
133                continue
134            configs[config_name] = config_state
135
136        return configs
137
138    def testKernelConfigs(self):
139        """Ensures all kernel configs conform to Android requirements.
140
141        Detects kernel version of device and validates against appropriate
142        Common Android Kernel android-base.cfg and Android Treble
143        requirements.
144        """
145        logging.info("Testing existence of %s" % self.PROC_FILE_PATH)
146        file_utils.assertPermissionsAndExistence(
147            self.shell, self.PROC_FILE_PATH, file_utils.IsReadOnly)
148
149        logging.info("Validating kernel version of device.")
150        kernel_version = self.checkKernelVersion()
151
152        # Pull configs from the universal config file.
153        configs = dict()
154        config_file_path = os.path.join(self.data_file_path,
155            self.KERNEL_CONFIG_FILE_PATH, "android-" + kernel_version,
156            "android-base.cfg")
157        with open(config_file_path, 'r') as config_file:
158            configs = self.parseConfigFileToDict(config_file, configs)
159
160        # Pull configs from device.
161        device_configs = dict()
162        self.dut.adb.pull("%s %s" % (self.PROC_FILE_PATH, self._temp_dir))
163        logging.info("Adb pull %s to %s", self.PROC_FILE_PATH, self._temp_dir)
164
165        localpath = os.path.join(self._temp_dir, "config.gz")
166        with gzip.open(localpath, "rb") as device_config_file:
167            device_configs = self.parseConfigFileToDict(device_config_file,
168                    device_configs)
169
170        # Check device architecture and pull arch-specific configs.
171        kernelArch = self.checkKernelArch(device_configs)
172        if kernelArch is not "":
173            config_file_path = os.path.join(self.data_file_path,
174                self.KERNEL_CONFIG_FILE_PATH, "android-" + kernel_version,
175                "android-base-%s.cfg" % kernelArch)
176            if os.path.isfile(config_file_path):
177                with open(config_file_path, 'r') as config_file:
178                    configs = self.parseConfigFileToDict(config_file, configs)
179
180        # Determine any deviations from the required configs.
181        should_be_enabled = []
182        should_not_be_set = []
183        incorrect_config_state = []
184        for config_name, config_state in configs.iteritems():
185            if (config_state == "y" and
186                (config_name not in device_configs or
187                 device_configs[config_name] not in ("y", "m"))):
188                should_be_enabled.append(config_name)
189            elif (config_state == "n" and (config_name in device_configs) and
190                  device_configs[config_name] != "n"):
191                should_not_be_set.append(config_name + "=" +
192                                         device_configs[config_name])
193            elif (config_name in device_configs and
194                  device_configs[config_name] != config_state):
195                incorrect_config_state.append(config_name + "=" +
196                                              device_configs[config_name])
197
198        if ("CONFIG_OF" not in device_configs and
199            "CONFIG_ACPI" not in device_configs):
200               should_be_enabled.append("CONFIG_OF | CONFIG_ACPI")
201
202        asserts.assertTrue(
203            len(should_be_enabled) == 0 and len(should_not_be_set) == 0 and
204            len(incorrect_config_state) == 0,
205            ("The following kernel configs should be enabled: [%s]\n"
206             "The following kernel configs should not be set: [%s]\n"
207             "THe following kernel configs have incorrect state: [%s]") %
208            (", ".join(should_be_enabled), ", ".join(should_not_be_set),
209             ", ".join(incorrect_config_state)))
210
211    def tearDownClass(self):
212        """Deletes the temporary directory."""
213        logging.info("Delete %s", self._temp_dir)
214        shutil.rmtree(self._temp_dir)
215
216
217if __name__ == "__main__":
218    test_runner.main()
219