| from __future__ import print_function |
| import os |
| import re |
| import glob |
| import subprocess |
| import sys |
| import unittest |
| |
| # |
| # Patch parsing functions |
| # |
| |
| FIND_INFRA_IN_PATCH = re.compile(r"^\+\$\(eval \$\((host-)?([^-]*)-package\)\)$") |
| |
| |
| def analyze_patch(patch): |
| """Parse one patch and return the list of files modified, added or |
| removed by the patch.""" |
| files = set() |
| infras = set() |
| for line in patch: |
| # If the patch is adding a package, find which infra it is |
| m = FIND_INFRA_IN_PATCH.match(line) |
| if m: |
| infras.add(m.group(2)) |
| if not line.startswith("+++ "): |
| continue |
| line.strip() |
| fname = line[line.find("/") + 1:].strip() |
| if fname == "dev/null": |
| continue |
| files.add(fname) |
| return (files, infras) |
| |
| |
| FIND_INFRA_IN_MK = re.compile(r"^\$\(eval \$\((host-)?([^-]*)-package\)\)$") |
| |
| |
| def fname_get_package_infra(fname): |
| """Checks whether the file name passed as argument is a Buildroot .mk |
| file describing a package, and find the infrastructure it's using.""" |
| if not fname.endswith(".mk"): |
| return None |
| |
| if not os.path.exists(fname): |
| return None |
| |
| with open(fname, "r") as f: |
| for line in f: |
| line = line.strip() |
| m = FIND_INFRA_IN_MK.match(line) |
| if m: |
| return m.group(2) |
| return None |
| |
| |
| def get_infras(files): |
| """Search in the list of files for .mk files, and collect the package |
| infrastructures used by those .mk files.""" |
| infras = set() |
| for fname in files: |
| infra = fname_get_package_infra(fname) |
| if infra: |
| infras.add(infra) |
| return infras |
| |
| |
| def analyze_patches(patches): |
| """Parse a list of patches and returns the list of files modified, |
| added or removed by the patches, as well as the list of package |
| infrastructures used by those patches (if any)""" |
| allfiles = set() |
| allinfras = set() |
| for patch in patches: |
| (files, infras) = analyze_patch(patch) |
| allfiles = allfiles | files |
| allinfras = allinfras | infras |
| allinfras = allinfras | get_infras(allfiles) |
| return (allfiles, allinfras) |
| |
| |
| # |
| # Unit-test parsing functions |
| # |
| |
| def get_all_test_cases(suite): |
| """Generate all test-cases from a given test-suite. |
| :return: (test.module, test.name)""" |
| if issubclass(type(suite), unittest.TestSuite): |
| for test in suite: |
| for res in get_all_test_cases(test): |
| yield res |
| else: |
| yield (suite.__module__, suite.__class__.__name__) |
| |
| |
| def list_unittests(path): |
| """Use the unittest module to retreive all test cases from a given |
| directory""" |
| loader = unittest.TestLoader() |
| suite = loader.discover(path) |
| tests = {} |
| for module, test in get_all_test_cases(suite): |
| module_path = os.path.join(path, *module.split('.')) |
| tests.setdefault(module_path, []).append('%s.%s' % (module, test)) |
| return tests |
| |
| |
| unittests = {} |
| |
| |
| # |
| # DEVELOPERS file parsing functions |
| # |
| |
| class Developer: |
| def __init__(self, name, files): |
| self.name = name |
| self.files = files |
| self.packages = parse_developer_packages(files) |
| self.architectures = parse_developer_architectures(files) |
| self.infras = parse_developer_infras(files) |
| self.runtime_tests = parse_developer_runtime_tests(files) |
| self.defconfigs = parse_developer_defconfigs(files) |
| |
| def hasfile(self, f): |
| f = os.path.abspath(f) |
| for fs in self.files: |
| if f.startswith(fs): |
| return True |
| return False |
| |
| def __repr__(self): |
| name = '\'' + self.name.split(' <')[0][:20] + '\'' |
| things = [] |
| if len(self.files): |
| things.append('{} files'.format(len(self.files))) |
| if len(self.packages): |
| things.append('{} pkgs'.format(len(self.packages))) |
| if len(self.architectures): |
| things.append('{} archs'.format(len(self.architectures))) |
| if len(self.infras): |
| things.append('{} infras'.format(len(self.infras))) |
| if len(self.runtime_tests): |
| things.append('{} tests'.format(len(self.runtime_tests))) |
| if len(self.defconfigs): |
| things.append('{} defconfigs'.format(len(self.defconfigs))) |
| if things: |
| return 'Developer <{} ({})>'.format(name, ', '.join(things)) |
| else: |
| return 'Developer <' + name + '>' |
| |
| |
| def parse_developer_packages(fnames): |
| """Given a list of file patterns, travel through the Buildroot source |
| tree to find which packages are implemented by those file |
| patterns, and return a list of those packages.""" |
| packages = set() |
| for fname in fnames: |
| for root, dirs, files in os.walk(fname): |
| for f in files: |
| path = os.path.join(root, f) |
| if fname_get_package_infra(path): |
| pkg = os.path.splitext(f)[0] |
| packages.add(pkg) |
| return packages |
| |
| |
| def parse_arches_from_config_in(fname): |
| """Given a path to an arch/Config.in.* file, parse it to get the list |
| of BR2_ARCH values for this architecture.""" |
| arches = set() |
| with open(fname, "r") as f: |
| parsing_arches = False |
| for line in f: |
| line = line.strip() |
| if line == "config BR2_ARCH": |
| parsing_arches = True |
| continue |
| if parsing_arches: |
| m = re.match(r"^\s*default \"([^\"]*)\".*", line) |
| if m: |
| arches.add(m.group(1)) |
| else: |
| parsing_arches = False |
| return arches |
| |
| |
| def parse_developer_architectures(fnames): |
| """Given a list of file names, find the ones starting by |
| 'arch/Config.in.', and use that to determine the architecture a |
| developer is working on.""" |
| arches = set() |
| for fname in fnames: |
| if not re.match(r"^.*/arch/Config\.in\..*$", fname): |
| continue |
| arches = arches | parse_arches_from_config_in(fname) |
| return arches |
| |
| |
| def parse_developer_infras(fnames): |
| infras = set() |
| for fname in fnames: |
| m = re.match(r"^package/pkg-([^.]*).mk$", fname) |
| if m: |
| infras.add(m.group(1)) |
| return infras |
| |
| |
| def parse_developer_defconfigs(fnames): |
| """Given a list of file names, returns the config names |
| corresponding to defconfigs.""" |
| return {os.path.basename(fname[:-10]) |
| for fname in fnames |
| if fname.endswith('_defconfig')} |
| |
| |
| def parse_developer_runtime_tests(fnames): |
| """Given a list of file names, returns the runtime tests |
| corresponding to the file.""" |
| all_files = [] |
| # List all files recursively |
| for fname in fnames: |
| if os.path.isdir(fname): |
| for root, _dirs, files in os.walk(fname): |
| all_files += [os.path.join(root, f) for f in files] |
| else: |
| all_files.append(fname) |
| |
| # Get all runtime tests |
| runtimes = set() |
| for f in all_files: |
| name = os.path.splitext(f)[0] |
| if name in unittests: |
| runtimes |= set(unittests[name]) |
| return runtimes |
| |
| |
| def parse_developers(basepath=None): |
| """Parse the DEVELOPERS file and return a list of Developer objects.""" |
| developers = [] |
| linen = 0 |
| if basepath is None: |
| basepath = os.getcwd() |
| global unittests |
| unittests = list_unittests(os.path.join(basepath, 'support/testing')) |
| with open(os.path.join(basepath, "DEVELOPERS"), "r") as f: |
| files = [] |
| name = None |
| for line in f: |
| line = line.strip() |
| if line.startswith("#"): |
| continue |
| elif line.startswith("N:"): |
| if name is not None or len(files) != 0: |
| print("Syntax error in DEVELOPERS file, line %d" % linen, |
| file=sys.stderr) |
| name = line[2:].strip() |
| elif line.startswith("F:"): |
| fname = line[2:].strip() |
| dev_files = glob.glob(os.path.join(basepath, fname)) |
| if len(dev_files) == 0: |
| print("WARNING: '%s' doesn't match any file" % fname, |
| file=sys.stderr) |
| files += dev_files |
| elif line == "": |
| if not name: |
| continue |
| developers.append(Developer(name, files)) |
| files = [] |
| name = None |
| else: |
| print("Syntax error in DEVELOPERS file, line %d: '%s'" % (linen, line), |
| file=sys.stderr) |
| return None |
| linen += 1 |
| # handle last developer |
| if name is not None: |
| developers.append(Developer(name, files)) |
| return developers |
| |
| |
| def check_developers(developers, basepath=None): |
| """Look at the list of files versioned in Buildroot, and returns the |
| list of files that are not handled by any developer""" |
| if basepath is None: |
| basepath = os.getcwd() |
| cmd = ["git", "--git-dir", os.path.join(basepath, ".git"), "ls-files"] |
| files = subprocess.check_output(cmd).strip().split("\n") |
| unhandled_files = [] |
| for f in files: |
| handled = False |
| for d in developers: |
| if d.hasfile(os.path.join(basepath, f)): |
| handled = True |
| break |
| if not handled: |
| unhandled_files.append(f) |
| return unhandled_files |