blob: 04d2ee9ea45da923c9ef08edcce1015e3d73c9b7 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright 2017 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Generates a report on CKI syscall coverage in VTS LTP.
This module generates a report on the syscalls in the Android CKI and
their coverage in VTS LTP.
The coverage report provides, for each syscall in the CKI, the number of
enabled and disabled LTP tests for the syscall in VTS. If VTS test output is
supplied, the report instead provides the number of disabled, skipped, failing,
and passing tests for each syscall.
Assumptions are made about the structure of files in LTP source
and the naming convention.
"""
import argparse
import os.path
import re
import sys
import xml.etree.ElementTree as ET
if "ANDROID_BUILD_TOP" not in os.environ:
print ("Please set up your Android build environment by running "
"\". build/envsetup.sh\" and \"lunch\".")
sys.exit(-1)
sys.path.append(os.path.join(os.environ["ANDROID_BUILD_TOP"],
"bionic/libc/tools"))
import gensyscalls
sys.path.append(os.path.join(os.environ["ANDROID_BUILD_TOP"],
"test/vts-testcase/kernel/ltp/configs"))
import disabled_tests as vts_disabled
import stable_tests as vts_stable
bionic_libc_root = os.path.join(os.environ["ANDROID_BUILD_TOP"], "bionic/libc")
class CKI_Coverage(object):
"""Determines current test coverage of CKI system calls in LTP.
Many of the system calls in the CKI are tested by LTP. For a given
system call an LTP test may or may not exist, that LTP test may or may
not be currently compiling properly for Android, the test may not be
stable, the test may not be running due to environment issues or
passing. This class looks at various sources of information to determine
the current test coverage of system calls in the CKI from LTP.
Note that due to some deviations in LTP of tests from the common naming
convention there there may be tests that are flagged here as not having
coverage when in fact they do.
"""
LTP_SYSCALL_ROOT = os.path.join(os.environ["ANDROID_BUILD_TOP"],
"external/ltp/testcases/kernel/syscalls")
DISABLED_IN_LTP_PATH = os.path.join(os.environ["ANDROID_BUILD_TOP"],
"external/ltp/android/tools/disabled_tests.txt")
ltp_full_set = []
cki_syscalls = []
disabled_in_ltp = []
disabled_in_vts_ltp = vts_disabled.DISABLED_TESTS
stable_in_vts_ltp = vts_stable.STABLE_TESTS
syscall_tests = {}
disabled_tests = {}
failing_tests = {}
skipped_tests = {}
passing_tests = {}
test_results = {}
def __init__(self, arch):
self._arch = arch
def load_ltp_tests(self):
"""Load the list of LTP syscall tests.
Load the list of all syscall tests existing in LTP.
"""
for path, dirs, files in os.walk(self.LTP_SYSCALL_ROOT):
for filename in files:
basename, ext = os.path.splitext(filename)
if ext != ".c": continue
self.ltp_full_set.append(basename)
def load_ltp_disabled_tests(self):
"""Load the list of LTP tests not being compiled.
The LTP repository in Android contains a list of tests which are not
compiled due to incompatibilities with Android.
"""
with open(self.DISABLED_IN_LTP_PATH) as fp:
for line in fp:
line = line.strip()
if not line: continue
test_re = re.compile(r"^(\w+)")
test_match = re.match(test_re, line)
if not test_match: continue
self.disabled_in_ltp.append(test_match.group(1))
def parse_test_results(self, results):
"""Parse xml from VTS output to collect LTP results.
Parse xml to collect pass/fail results for each LTP
test. A failure occurs if a test has a result other than pass or fail.
Args:
results: Path to VTS output XML file.
"""
tree = ET.parse(results)
root = tree.getroot()
found = False
# find LTP module
for module in root.findall("Module"):
if module.attrib["name"] != "VtsKernelLtp": continue
# ARM arch and ABI strings don't match exactly, x86 and mips do.
if self._arch == "arm":
if not module.attrib["abi"].startswith("armeabi-"): continue
elif self._arch == "arm64":
if not module.attrib["abi"].startswith("arm64-"): continue
elif self._arch != module.attrib["abi"]: continue
# find LTP testcase
for testcase in module.findall("TestCase"):
if testcase.attrib["name"] != "KernelLtpTest": continue
found = True
# iterate over each LTP test
for test in testcase.findall("Test"):
test_re = re.compile(r"^syscalls.(\w+)_((32bit)|(64bit))$")
test_match = re.match(test_re, test.attrib["name"])
if not test_match: continue
test_name = test_match.group(1)
if test.attrib["result"] == "pass":
self.test_results[test_name] = "pass"
elif test.attrib["result"] == "fail":
self.test_results[test_name] = "fail"
else:
print ("Unknown VTS LTP test result for %s is %s" %
(test_name, test.attrib["result"]))
sys.exit(-1)
if not found:
print "Error: LTP test results for arch %s not found in supplied test results." % self._arch
sys.exit(-1)
def ltp_test_special_cases(self, syscall, test):
"""Detect special cases in syscall to LTP mapping.
Most syscall tests in LTP follow a predictable naming
convention, but some do not. Detect known special cases.
Args:
syscall: The name of a syscall.
test: The name of a testcase.
Returns:
A boolean indicating whether the given syscall is tested
by the given testcase.
"""
if syscall == "clock_nanosleep" and test == "clock_nanosleep2_01":
return True
if syscall == "fadvise" and test.startswith("posix_fadvise"):
return True
if syscall == "futex" and test.startswith("futex_"):
return True
if syscall == "inotify_add_watch" or syscall == "inotify_rm_watch":
test_re = re.compile(r"^inotify\d+$")
if re.match(test_re, test):
return True
if syscall == "newfstatat":
test_re = re.compile(r"^fstatat\d+$")
if re.match(test_re, test):
return True
return False
def match_syscalls_to_tests(self, syscalls):
"""Match syscalls with tests in LTP.
Create a mapping from CKI syscalls and tests in LTP. This mapping can
largely be determined using a common naming convention in the LTP file
hierarchy but there are special cases that have to be taken care of.
Args:
syscalls: List of syscall structures containing all syscalls
in the CKI.
"""
for syscall in syscalls:
if self._arch not in syscall:
continue
self.cki_syscalls.append(syscall["name"])
self.syscall_tests[syscall["name"]] = []
# LTP does not use the 64 at the end of syscall names for testcases.
ltp_syscall_name = syscall["name"]
if ltp_syscall_name.endswith("64"):
ltp_syscall_name = ltp_syscall_name[0:-2]
# Most LTP syscalls have source files for the tests that follow
# a naming convention in the regexp below. Exceptions exist though.
# For now those are checked for specifically.
test_re = re.compile(r"^%s_?0?\d\d?$" % ltp_syscall_name)
for test in self.ltp_full_set:
if (re.match(test_re, test) or
self.ltp_test_special_cases(ltp_syscall_name, test)):
# The filenames of the ioctl tests in LTP do not match the name
# of the testcase defined in that source, which is what shows
# up in VTS.
if ltp_syscall_name == "ioctl":
test = "ioctl01_02"
self.syscall_tests[syscall["name"]].append(test)
self.cki_syscalls.sort()
def update_test_status(self):
"""Populate test configuration and output for all CKI syscalls.
Go through VTS test configuration and test results (if provided) to
populate data for all CKI syscalls.
"""
for syscall in self.cki_syscalls:
self.disabled_tests[syscall] = []
self.skipped_tests[syscall] = []
self.failing_tests[syscall] = []
self.passing_tests[syscall] = []
if not self.syscall_tests[syscall]:
continue
for test in self.syscall_tests[syscall]:
if (test in self.disabled_in_ltp or
"syscalls.%s" % test in self.disabled_in_vts_ltp or
("syscalls.%s_32bit" % test not in self.stable_in_vts_ltp and
"syscalls.%s_64bit" % test not in self.stable_in_vts_ltp)):
self.disabled_tests[syscall].append(test)
continue
if not self.test_results:
continue
if test not in self.test_results:
self.skipped_tests[syscall].append(test)
elif self.test_results[test] == "fail":
self.failing_tests[syscall].append(test)
elif self.test_results[test] == "pass":
self.passing_tests[syscall].append(test)
else:
print ("Warning - could not resolve test %s status for syscall %s" %
(test, syscall))
def output_results(self):
"""Pretty print the CKI syscall LTP coverage results.
Pretty prints a table of the CKI syscall LTP coverage, pointing out
syscalls which have no passing tests in VTS LTP.
"""
if not self.test_results:
self.output_limited_results()
return
count = 0
uncovered = 0
for syscall in self.cki_syscalls:
if not count % 20:
print ("%25s Disabled Skipped Failing Passing -------------" %
"-------------")
sys.stdout.write("%25s %s %s %s %s" %
(syscall, len(self.disabled_tests[syscall]),
len(self.skipped_tests[syscall]),
len(self.failing_tests[syscall]),
len(self.passing_tests[syscall])))
if not self.passing_tests[syscall]:
print " <-- uncovered"
uncovered += 1
else:
print ""
count += 1
print ""
print ("Total uncovered syscalls: %s out of %s" %
(uncovered, len(self.cki_syscalls)))
def output_limited_results(self):
"""Pretty print the CKI syscall LTP coverage without VTS test results.
When no VTS test results are supplied then only the count of enabled
and disabled LTP tests may be shown.
"""
count = 0
uncovered = 0
for syscall in self.cki_syscalls:
if not count % 20:
print ("%25s Disabled Enabled -------------" %
"-------------")
sys.stdout.write("%25s %s %s" %
(syscall, len(self.disabled_tests[syscall]),
len(self.syscall_tests[syscall]) -
len(self.disabled_tests[syscall])))
if (len(self.syscall_tests[syscall]) -
len(self.disabled_tests[syscall]) <= 0):
print " <-- uncovered"
uncovered += 1
else:
print ""
count += 1
print ""
print ("Total uncovered syscalls: %s out of %s" %
(uncovered, len(self.cki_syscalls)))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Output list of system calls "
"in the Common Kernel Interface and their VTS LTP coverage. If VTS "
"test output is supplied, output includes system calls which have "
"tests in VTS LTP, but the tests are skipped or are failing.")
parser.add_argument("arch", help="architecture of Android platform")
parser.add_argument("-l", action="store_true",
help="list CKI syscalls only, without coverage")
parser.add_argument("-r", "--results", help="path to VTS test_result.xml")
args = parser.parse_args()
if args.arch not in gensyscalls.all_arches:
print "Arch must be one of the following:"
print gensyscalls.all_arches
exit(-1)
cki = gensyscalls.SysCallsTxtParser()
cki.parse_file(os.path.join(bionic_libc_root, "SYSCALLS.TXT"))
cki.parse_file(os.path.join(bionic_libc_root, "SECCOMP_WHITELIST.TXT"))
cki.parse_file(os.path.join(bionic_libc_root, "SECCOMP_WHITELIST_GLOBAL.TXT"))
if args.l:
for syscall in cki.syscalls:
if args.arch in syscall:
print syscall["name"]
exit(0)
cki_cov = CKI_Coverage(args.arch)
cki_cov.load_ltp_tests()
cki_cov.load_ltp_disabled_tests()
if args.results:
cki_cov.parse_test_results(args.results)
cki_cov.match_syscalls_to_tests(cki.syscalls)
cki_cov.update_test_status()
beta_string = ("*** WARNING: This script is still in development and may\n"
"*** report both false positives and negatives.")
print beta_string
cki_cov.output_results()
print beta_string