1*4882a593Smuzhiyunfrom io import open 2*4882a593Smuzhiyunimport os 3*4882a593Smuzhiyunimport re 4*4882a593Smuzhiyunimport glob 5*4882a593Smuzhiyunimport subprocess 6*4882a593Smuzhiyunimport sys 7*4882a593Smuzhiyunimport unittest 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunbrpath = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) 10*4882a593Smuzhiyun 11*4882a593Smuzhiyun# 12*4882a593Smuzhiyun# Patch parsing functions 13*4882a593Smuzhiyun# 14*4882a593Smuzhiyun 15*4882a593SmuzhiyunFIND_INFRA_IN_PATCH = re.compile(r"^\+\$\(eval \$\((host-)?([^-]*)-package\)\)$") 16*4882a593Smuzhiyun 17*4882a593Smuzhiyun 18*4882a593Smuzhiyundef analyze_patch(patch): 19*4882a593Smuzhiyun """Parse one patch and return the list of files modified, added or 20*4882a593Smuzhiyun removed by the patch.""" 21*4882a593Smuzhiyun files = set() 22*4882a593Smuzhiyun infras = set() 23*4882a593Smuzhiyun for line in patch: 24*4882a593Smuzhiyun # If the patch is adding a package, find which infra it is 25*4882a593Smuzhiyun m = FIND_INFRA_IN_PATCH.match(line) 26*4882a593Smuzhiyun if m: 27*4882a593Smuzhiyun infras.add(m.group(2)) 28*4882a593Smuzhiyun if not line.startswith("+++ "): 29*4882a593Smuzhiyun continue 30*4882a593Smuzhiyun line.strip() 31*4882a593Smuzhiyun fname = line[line.find("/") + 1:].strip() 32*4882a593Smuzhiyun if fname == "dev/null": 33*4882a593Smuzhiyun continue 34*4882a593Smuzhiyun files.add(fname) 35*4882a593Smuzhiyun return (files, infras) 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun 38*4882a593SmuzhiyunFIND_INFRA_IN_MK = re.compile(r"^\$\(eval \$\((host-)?([^-]*)-package\)\)$") 39*4882a593Smuzhiyun 40*4882a593Smuzhiyun 41*4882a593Smuzhiyundef fname_get_package_infra(fname): 42*4882a593Smuzhiyun """Checks whether the file name passed as argument is a Buildroot .mk 43*4882a593Smuzhiyun file describing a package, and find the infrastructure it's using.""" 44*4882a593Smuzhiyun if not fname.endswith(".mk"): 45*4882a593Smuzhiyun return None 46*4882a593Smuzhiyun 47*4882a593Smuzhiyun if not os.path.exists(fname): 48*4882a593Smuzhiyun return None 49*4882a593Smuzhiyun 50*4882a593Smuzhiyun with open(fname, "r") as f: 51*4882a593Smuzhiyun for line in f: 52*4882a593Smuzhiyun line = line.strip() 53*4882a593Smuzhiyun m = FIND_INFRA_IN_MK.match(line) 54*4882a593Smuzhiyun if m: 55*4882a593Smuzhiyun return m.group(2) 56*4882a593Smuzhiyun return None 57*4882a593Smuzhiyun 58*4882a593Smuzhiyun 59*4882a593Smuzhiyundef analyze_patches(patches): 60*4882a593Smuzhiyun """Parse a list of patches and returns the list of files modified, 61*4882a593Smuzhiyun added or removed by the patches, as well as the list of package 62*4882a593Smuzhiyun infrastructures used by those patches (if any)""" 63*4882a593Smuzhiyun allfiles = set() 64*4882a593Smuzhiyun allinfras = set() 65*4882a593Smuzhiyun for patch in patches: 66*4882a593Smuzhiyun (files, infras) = analyze_patch(patch) 67*4882a593Smuzhiyun allfiles = allfiles | files 68*4882a593Smuzhiyun allinfras = allinfras | infras 69*4882a593Smuzhiyun return (allfiles, allinfras) 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun# 73*4882a593Smuzhiyun# Unit-test parsing functions 74*4882a593Smuzhiyun# 75*4882a593Smuzhiyun 76*4882a593Smuzhiyundef get_all_test_cases(suite): 77*4882a593Smuzhiyun """Generate all test-cases from a given test-suite. 78*4882a593Smuzhiyun :return: (test.module, test.name)""" 79*4882a593Smuzhiyun if issubclass(type(suite), unittest.TestSuite): 80*4882a593Smuzhiyun for test in suite: 81*4882a593Smuzhiyun for res in get_all_test_cases(test): 82*4882a593Smuzhiyun yield res 83*4882a593Smuzhiyun else: 84*4882a593Smuzhiyun yield (suite.__module__, suite.__class__.__name__) 85*4882a593Smuzhiyun 86*4882a593Smuzhiyun 87*4882a593Smuzhiyundef list_unittests(): 88*4882a593Smuzhiyun """Use the unittest module to retreive all test cases from a given 89*4882a593Smuzhiyun directory""" 90*4882a593Smuzhiyun loader = unittest.TestLoader() 91*4882a593Smuzhiyun suite = loader.discover(os.path.join(brpath, "support", "testing")) 92*4882a593Smuzhiyun tests = {} 93*4882a593Smuzhiyun for module, test in get_all_test_cases(suite): 94*4882a593Smuzhiyun module_path = os.path.join("support", "testing", *module.split('.')) 95*4882a593Smuzhiyun tests.setdefault(module_path, []).append('%s.%s' % (module, test)) 96*4882a593Smuzhiyun return tests 97*4882a593Smuzhiyun 98*4882a593Smuzhiyun 99*4882a593Smuzhiyununittests = {} 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun# 103*4882a593Smuzhiyun# DEVELOPERS file parsing functions 104*4882a593Smuzhiyun# 105*4882a593Smuzhiyun 106*4882a593Smuzhiyunclass Developer: 107*4882a593Smuzhiyun def __init__(self, name, files): 108*4882a593Smuzhiyun self.name = name 109*4882a593Smuzhiyun self.files = files 110*4882a593Smuzhiyun self.packages = parse_developer_packages(files) 111*4882a593Smuzhiyun self.architectures = parse_developer_architectures(files) 112*4882a593Smuzhiyun self.infras = parse_developer_infras(files) 113*4882a593Smuzhiyun self.runtime_tests = parse_developer_runtime_tests(files) 114*4882a593Smuzhiyun self.defconfigs = parse_developer_defconfigs(files) 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun def hasfile(self, f): 117*4882a593Smuzhiyun for fs in self.files: 118*4882a593Smuzhiyun if f.startswith(fs): 119*4882a593Smuzhiyun return True 120*4882a593Smuzhiyun return False 121*4882a593Smuzhiyun 122*4882a593Smuzhiyun def __repr__(self): 123*4882a593Smuzhiyun name = '\'' + self.name.split(' <')[0][:20] + '\'' 124*4882a593Smuzhiyun things = [] 125*4882a593Smuzhiyun if len(self.files): 126*4882a593Smuzhiyun things.append('{} files'.format(len(self.files))) 127*4882a593Smuzhiyun if len(self.packages): 128*4882a593Smuzhiyun things.append('{} pkgs'.format(len(self.packages))) 129*4882a593Smuzhiyun if len(self.architectures): 130*4882a593Smuzhiyun things.append('{} archs'.format(len(self.architectures))) 131*4882a593Smuzhiyun if len(self.infras): 132*4882a593Smuzhiyun things.append('{} infras'.format(len(self.infras))) 133*4882a593Smuzhiyun if len(self.runtime_tests): 134*4882a593Smuzhiyun things.append('{} tests'.format(len(self.runtime_tests))) 135*4882a593Smuzhiyun if len(self.defconfigs): 136*4882a593Smuzhiyun things.append('{} defconfigs'.format(len(self.defconfigs))) 137*4882a593Smuzhiyun if things: 138*4882a593Smuzhiyun return 'Developer <{} ({})>'.format(name, ', '.join(things)) 139*4882a593Smuzhiyun else: 140*4882a593Smuzhiyun return 'Developer <' + name + '>' 141*4882a593Smuzhiyun 142*4882a593Smuzhiyun 143*4882a593Smuzhiyundef parse_developer_packages(fnames): 144*4882a593Smuzhiyun """Given a list of file patterns, travel through the Buildroot source 145*4882a593Smuzhiyun tree to find which packages are implemented by those file 146*4882a593Smuzhiyun patterns, and return a list of those packages.""" 147*4882a593Smuzhiyun packages = set() 148*4882a593Smuzhiyun for fname in fnames: 149*4882a593Smuzhiyun for root, dirs, files in os.walk(os.path.join(brpath, fname)): 150*4882a593Smuzhiyun for f in files: 151*4882a593Smuzhiyun path = os.path.join(root, f) 152*4882a593Smuzhiyun if fname_get_package_infra(path): 153*4882a593Smuzhiyun pkg = os.path.splitext(f)[0] 154*4882a593Smuzhiyun packages.add(pkg) 155*4882a593Smuzhiyun return packages 156*4882a593Smuzhiyun 157*4882a593Smuzhiyun 158*4882a593Smuzhiyundef parse_arches_from_config_in(fname): 159*4882a593Smuzhiyun """Given a path to an arch/Config.in.* file, parse it to get the list 160*4882a593Smuzhiyun of BR2_ARCH values for this architecture.""" 161*4882a593Smuzhiyun arches = set() 162*4882a593Smuzhiyun with open(fname, "r") as f: 163*4882a593Smuzhiyun parsing_arches = False 164*4882a593Smuzhiyun for line in f: 165*4882a593Smuzhiyun line = line.strip() 166*4882a593Smuzhiyun if line == "config BR2_ARCH": 167*4882a593Smuzhiyun parsing_arches = True 168*4882a593Smuzhiyun continue 169*4882a593Smuzhiyun if parsing_arches: 170*4882a593Smuzhiyun m = re.match(r"^\s*default \"([^\"]*)\".*", line) 171*4882a593Smuzhiyun if m: 172*4882a593Smuzhiyun arches.add(m.group(1)) 173*4882a593Smuzhiyun else: 174*4882a593Smuzhiyun parsing_arches = False 175*4882a593Smuzhiyun return arches 176*4882a593Smuzhiyun 177*4882a593Smuzhiyun 178*4882a593Smuzhiyundef parse_developer_architectures(fnames): 179*4882a593Smuzhiyun """Given a list of file names, find the ones starting by 180*4882a593Smuzhiyun 'arch/Config.in.', and use that to determine the architecture a 181*4882a593Smuzhiyun developer is working on.""" 182*4882a593Smuzhiyun arches = set() 183*4882a593Smuzhiyun for fname in fnames: 184*4882a593Smuzhiyun if not re.match(r"^.*/arch/Config\.in\..*$", fname): 185*4882a593Smuzhiyun continue 186*4882a593Smuzhiyun arches = arches | parse_arches_from_config_in(fname) 187*4882a593Smuzhiyun return arches 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun 190*4882a593Smuzhiyundef parse_developer_infras(fnames): 191*4882a593Smuzhiyun infras = set() 192*4882a593Smuzhiyun for fname in fnames: 193*4882a593Smuzhiyun m = re.match(r"^package/pkg-([^.]*).mk$", fname) 194*4882a593Smuzhiyun if m: 195*4882a593Smuzhiyun infras.add(m.group(1)) 196*4882a593Smuzhiyun return infras 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun 199*4882a593Smuzhiyundef parse_developer_defconfigs(fnames): 200*4882a593Smuzhiyun """Given a list of file names, returns the config names 201*4882a593Smuzhiyun corresponding to defconfigs.""" 202*4882a593Smuzhiyun return {os.path.basename(fname[:-10]) 203*4882a593Smuzhiyun for fname in fnames 204*4882a593Smuzhiyun if fname.endswith('_defconfig')} 205*4882a593Smuzhiyun 206*4882a593Smuzhiyun 207*4882a593Smuzhiyundef parse_developer_runtime_tests(fnames): 208*4882a593Smuzhiyun """Given a list of file names, returns the runtime tests 209*4882a593Smuzhiyun corresponding to the file.""" 210*4882a593Smuzhiyun all_files = [] 211*4882a593Smuzhiyun # List all files recursively 212*4882a593Smuzhiyun for fname in fnames: 213*4882a593Smuzhiyun if os.path.isdir(fname): 214*4882a593Smuzhiyun for root, _dirs, files in os.walk(os.path.join(brpath, fname)): 215*4882a593Smuzhiyun all_files += [os.path.join(root, f) for f in files] 216*4882a593Smuzhiyun else: 217*4882a593Smuzhiyun all_files.append(fname) 218*4882a593Smuzhiyun 219*4882a593Smuzhiyun # Get all runtime tests 220*4882a593Smuzhiyun runtimes = set() 221*4882a593Smuzhiyun for f in all_files: 222*4882a593Smuzhiyun name = os.path.splitext(f)[0] 223*4882a593Smuzhiyun if name in unittests: 224*4882a593Smuzhiyun runtimes |= set(unittests[name]) 225*4882a593Smuzhiyun return runtimes 226*4882a593Smuzhiyun 227*4882a593Smuzhiyun 228*4882a593Smuzhiyundef parse_developers(): 229*4882a593Smuzhiyun """Parse the DEVELOPERS file and return a list of Developer objects.""" 230*4882a593Smuzhiyun developers = [] 231*4882a593Smuzhiyun linen = 0 232*4882a593Smuzhiyun global unittests 233*4882a593Smuzhiyun unittests = list_unittests() 234*4882a593Smuzhiyun developers_fname = os.path.join(brpath, 'DEVELOPERS') 235*4882a593Smuzhiyun with open(developers_fname, mode='r', encoding='utf_8') as f: 236*4882a593Smuzhiyun files = [] 237*4882a593Smuzhiyun name = None 238*4882a593Smuzhiyun for line in f: 239*4882a593Smuzhiyun line = line.strip() 240*4882a593Smuzhiyun if line.startswith("#"): 241*4882a593Smuzhiyun continue 242*4882a593Smuzhiyun elif line.startswith("N:"): 243*4882a593Smuzhiyun if name is not None or len(files) != 0: 244*4882a593Smuzhiyun print("Syntax error in DEVELOPERS file, line %d" % linen, 245*4882a593Smuzhiyun file=sys.stderr) 246*4882a593Smuzhiyun name = line[2:].strip() 247*4882a593Smuzhiyun elif line.startswith("F:"): 248*4882a593Smuzhiyun fname = line[2:].strip() 249*4882a593Smuzhiyun dev_files = glob.glob(os.path.join(brpath, fname)) 250*4882a593Smuzhiyun if len(dev_files) == 0: 251*4882a593Smuzhiyun print("WARNING: '%s' doesn't match any file" % fname, 252*4882a593Smuzhiyun file=sys.stderr) 253*4882a593Smuzhiyun for f in dev_files: 254*4882a593Smuzhiyun dev_file = os.path.relpath(f, brpath) 255*4882a593Smuzhiyun dev_file = dev_file.replace(os.sep, '/') # force unix sep 256*4882a593Smuzhiyun files.append(dev_file) 257*4882a593Smuzhiyun elif line == "": 258*4882a593Smuzhiyun if not name: 259*4882a593Smuzhiyun continue 260*4882a593Smuzhiyun developers.append(Developer(name, files)) 261*4882a593Smuzhiyun files = [] 262*4882a593Smuzhiyun name = None 263*4882a593Smuzhiyun else: 264*4882a593Smuzhiyun print("Syntax error in DEVELOPERS file, line %d: '%s'" % (linen, line), 265*4882a593Smuzhiyun file=sys.stderr) 266*4882a593Smuzhiyun return None 267*4882a593Smuzhiyun linen += 1 268*4882a593Smuzhiyun # handle last developer 269*4882a593Smuzhiyun if name is not None: 270*4882a593Smuzhiyun developers.append(Developer(name, files)) 271*4882a593Smuzhiyun return developers 272*4882a593Smuzhiyun 273*4882a593Smuzhiyun 274*4882a593Smuzhiyundef check_developers(developers, basepath=None): 275*4882a593Smuzhiyun """Look at the list of files versioned in Buildroot, and returns the 276*4882a593Smuzhiyun list of files that are not handled by any developer""" 277*4882a593Smuzhiyun if basepath is None: 278*4882a593Smuzhiyun basepath = os.getcwd() 279*4882a593Smuzhiyun cmd = ["git", "--git-dir", os.path.join(basepath, ".git"), "ls-files"] 280*4882a593Smuzhiyun files = subprocess.check_output(cmd).decode(sys.stdout.encoding).strip().split("\n") 281*4882a593Smuzhiyun unhandled_files = [] 282*4882a593Smuzhiyun for f in files: 283*4882a593Smuzhiyun handled = False 284*4882a593Smuzhiyun for d in developers: 285*4882a593Smuzhiyun if d.hasfile(f): 286*4882a593Smuzhiyun handled = True 287*4882a593Smuzhiyun break 288*4882a593Smuzhiyun if not handled: 289*4882a593Smuzhiyun unhandled_files.append(f) 290*4882a593Smuzhiyun return unhandled_files 291