#!/usr/bin/env python
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# -----------------------------FILE OVERVIEW----------------------------- #
# This script generates a list of tests that exist for given parameters.
#
# This script was created so that we could determine which tests are used
# by specific components, so that coverage can be generated for specific
# components.
#
# Use the built in help for instructions.
# ----------------------------------------------------------------------- #
import os
import sys
import argparse
import fnmatch
import re
import time
import json
from threading import Thread, Lock
sys.path.append(os.path.abspath("./third_party/blink/tools"))
sys.path.append(os.path.abspath("./"))
# Chromium check
NOT_CHROME_ERROR = (
'You must be in the chromium src directory to run this command.')
try:
file = open('./DIR_METADATA', 'r')
if not 'project: "chromium"' in file.read():
print(NOT_CHROME_ERROR)
exit()
except IOError:
print(NOT_CHROME_ERROR)
exit()
from third_party.blink.tools.blinkpy.w3c.directory_owners_extractor import DirectoryOwnersExtractor
from third_party.blink.tools.blinkpy.common.host import Host
# CONSTANTS
DIR_METADATA = 'DIR_METADATA'
COMMON_METADATA = 'COMMON_METADATA'
OWNERS = 'OWNERS'
TEST_NAME_REGEX = re.compile(
r"TEST(_[FP])?\(\s*'?([a-zA-Z][a-zA-Z0-9]*)'?,"
"\s*'?([a-zA-Z][a-zA-Z0-9_]*)'?", re.MULTILINE)
TEST_SUITE_REGEX = re.compile(
r'TEST_SUITE(_[FP])?\(\s*([a-zA-Z][a-zA-Z0-9]*),\s*([a-zA-Z][a-zA-Z0-9]*),',
re.MULTILINE)
parser = argparse.ArgumentParser(
"Determines test suites for given directories and components")
parser.add_argument(
'-c',
'--components',
nargs='+',
# TODO: Adjust to buganizer once the
# coverage team updates the tool
help='A monorail component to collect suites for',
default=[])
parser.add_argument(
'-d',
'--dirs',
nargs='*',
help='Directory to search through, at least one must be present',
default=['./'])
parser.add_argument('-i',
'--ignore_dir',
nargs='*',
help='Directories to ignore',
default=[])
parser.add_argument('-o',
'--out_file',
help='The file to write the suites to.',
required=True)
parser.add_argument('-j',
'--jobs',
required=False,
help='The number of threads to allow',
type=int,
default=0)
args = parser.parse_args()
class ThreadManager(int):
def __init__(self, max_threads):
self.num_threads = 0
self.max_threads = max_threads
self.lock = Lock()
def _thread_wrapper(self, func, args):
func(*args)
with self.lock:
self.num_threads -= 1
def run(self, func, args):
if self.num_threads < self.max_threads:
thread = Thread(target=self._thread_wrapper, args=(func, args))
with self.lock:
self.num_threads += 1
thread.start()
return True
else:
return False
def wait_until_threads_done(self):
while self.num_threads > 0:
time.sleep(0.1)
class TestFinder(argparse.Namespace):
def __init__(self, args):
self.host = Host()
self.component_map = dict()
self.dirs_with_tests = set()
self.disabled_tests = dict()
self.maybe_tests = dict()
self.total_disabled = 0
self.total_maybe = 0
self.total_working = 0
self.dirs = args.dirs
self.comps = args.components
self.dir_thread_manager = ThreadManager(args.jobs * .75)
self.read_thread_manager = ThreadManager(args.jobs / 4)
self.out_file = args.out_file
def is_hidden(self, path):
name = os.path.basename(os.path.abspath(path))
return name.startswith('.')
def is_output_dir(self, path):
name = os.path.basename(os.path.abspath(path))
return name.startswith('out')
def is_test_file(self, filepath):
if filepath.endswith('.py'):
return False
if filepath.endswith('.png'):
return False
noext = os.path.splitext(os.path.abspath(filepath))[0]
name = os.path.basename(noext)
return name.endswith('test')
def get_items_in_dir(self, path):
items_list = os.listdir(path)
files = list()
dirs = list()
for item in items_list:
item_path = os.path.abspath(os.path.join(path, item))
if os.path.isfile(item_path):
files.append(item_path)
else:
dirs.append(item_path)
return [files, dirs]
def get_component_for_dir(self, path, type):
# Python does something weird with hidden dirs.
# We don't care about those anyway.
if self.is_hidden(path):
return None
metadata_path = os.path.join(path, type)
extractor = DirectoryOwnersExtractor(Host())
try:
return extractor.extract_component(metadata_path)
except KeyError:
return None
def get_test_suites_for_file(self, filepath, suites):
try:
new_suites = set()
text = open(filepath, 'r').read()
relPath = os.path.relpath(filepath)
# Find regular tests
matches = re.findall(TEST_NAME_REGEX, text)
for m in matches:
new_suites.add(m[1])
# Check for disabled and maybe tests
if m[2] != None and m[2].startswith('DISABLED'):
disabled = self.disabled_tests.get(relPath)
self.total_disabled += 1
if disabled == None:
disabled = []
self.disabled_tests[relPath] = disabled
disabled.append(m[1] + "." + m[2])
elif m[2] != None and m[2].startswith('MAYBE'):
self.total_maybe += 1
maybe = self.maybe_tests.get(relPath)
if maybe == None:
maybe = []
self.maybe_tests[relPath] = maybe
maybe.append(m[1] + "." + m[2])
else:
self.total_working += 1
# Find suites
matches = re.findall(TEST_SUITE_REGEX, text)
for m in matches:
new_suites.add(m[2])
suites.extend(new_suites)
except IOError:
print('Failed to open ' + filepath)
return list()
def get_test_suites_for_dir(self, dir, suites):
files, _ = self.get_items_in_dir(dir)
files = list(filter(lambda file: self.is_test_file(file), files))
if files.__len__() > 0:
self.dirs_with_tests.add(os.path.relpath(dir))
for file in files:
if not self.read_thread_manager.run(self.get_test_suites_for_file,
(file, suites)):
self.get_test_suites_for_file(file, suites)
def start(self):
for dir in self.dirs:
self.build_component_map(dir, None)
self.dir_thread_manager.wait_until_threads_done()
self.save()
def save(self):
with open(self.out_file, 'w') as outfile:
data = {
"components": self.component_map,
"directories": [*self.dirs_with_tests],
"disabled_tests": self.disabled_tests,
"maybe_tests": self.maybe_tests,
"total_disabled": self.total_disabled,
"total_maybe": self.total_maybe,
"total_working": self.total_working,
"total_tests":
self.total_working + self.total_maybe + self.total_disabled
}
json.dump(data, outfile, indent=2)
def build_component_map(self, cur_dir, common_component):
# Normalize cur_dir
cur_dir = os.path.abspath(cur_dir)
# Get the component
component = self.get_component_for_dir(cur_dir, DIR_METADATA)
using_common_metadata = False
# If the component is in common metadata, it should pass through to sub dir.
if component == None and common_component == None:
component = self.get_component_for_dir(cur_dir, COMMON_METADATA)
using_common_metadata = True
if component != None:
allowed = self.comps.__len__() == 0
for comp_glob in self.comps:
result = fnmatch.filter([component], comp_glob)
allowed = result.__len__() > 0
if allowed:
break
if allowed:
tests_for_comp = self.component_map.get(component)
# If the component isn't in the map yet, add it
if tests_for_comp == None:
tests_for_comp = list()
self.component_map[component] = tests_for_comp
self.get_test_suites_for_dir(cur_dir, tests_for_comp)
# Even if the dir didn't have a component,
# we still need to check it's subdirs.
_, sub_dirs = self.get_items_in_dir(cur_dir)
for sub_dir in sub_dirs:
# skip hidden directories
if not self.is_hidden(sub_dir) and not self.is_output_dir(sub_dir):
common_component_to_pass = (component if using_common_metadata else
common_component)
if not self.dir_thread_manager.run(self.build_component_map,
(sub_dir, common_component_to_pass)):
self.build_component_map(sub_dir, common_component_to_pass)
try:
finder = TestFinder(args)
finder.start()
except KeyboardInterrupt:
print('\nUser Interrupted')
exit()