#!/usr/bin/env python3
# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Parses OWNERS recursively and generates a machine readable component mapping.
OWNERS files are expected to contain a well-formatted pair of tags as shown
below. A presubmit check exists that validates this.
This script finds lines in the OWNERS files such as:
`# TEAM: [email protected]` and
`# COMPONENT: Tools>Test>Findit`
and dumps this information into a json file.
Refer to crbug.com/667952
"""
from __future__ import print_function
import json
import argparse
import os
import sys
from owners_file_tags import aggregate_components_from_owners, scrape_owners
_DEFAULT_SRC_LOCATION = os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir)
_README = """
This file is generated by src/tools/checkteamtags/extract_components.py
by parsing the contents of OWNERS files throughout the chromium source code and
extracting `# TEAM:` and `# COMPONENT:` tags.
Manual edits of this file will be overwritten by an automated process.
""".splitlines()
def write_results(filename, data):
"""Write data to the named file, or the default location."""
if not filename:
filename = 'component_map.json'
with open(filename, 'w') as f:
f.write(data)
def display_stat(stats, root, args):
""""Display coverage statistic.
The following three values are always displayed:
- The total number of OWNERS files under directory root and its sub-
directories.
- The number of OWNERS files (and its percentage of the total) that have
component information but no team information.
- The number of OWNERS files (and its percentage of the total) that have
both component and team information.
Optionally, if args.stat_coverage or args.complete_coverage are given,
the same information will be shown for each depth level.
(up to the level given by args.stat_coverage, if any).
Args:
stats (dict): Tha statistics in dictionary form as produced by the
owners_file_tags module.
root (str): The root directory from which the depth level is calculated.
args (argparse.Values): The command line args as returned by
argparse.
"""
file_total = stats['OWNERS-count']
print("%d OWNERS files in total." % file_total)
file_with_component = stats['OWNERS-with-component-only-count']
file_pct_with_component = "N/A"
if file_total > 0:
file_pct_with_component = "{0:.2f}".format(
100.0 * file_with_component / file_total)
print('%(file_with_component)d (%(file_pct_with_component)s%%) OWNERS '\
'files have COMPONENT' % {
'file_with_component': file_with_component,
'file_pct_with_component': file_pct_with_component})
file_with_team_component = stats['OWNERS-with-team-and-component-count']
file_pct_with_team_component = "N/A"
if file_total > 0:
file_pct_with_team_component = "{0:.2f}".format(
100.0 * file_with_team_component / file_total)
print('%(file_with_team_component)d (%(file_pct_with_team_component)s%%) '\
'OWNERS files have TEAM and COMPONENT' % {
'file_with_team_component': file_with_team_component,
'file_pct_with_team_component': file_pct_with_team_component})
print("\nUnder directory %s " % root)
# number of depth to display, default is max depth under root
num_output_depth = len(stats['OWNERS-count-by-depth'])
if (args.stat_coverage and args.stat_coverage > 0
and args.stat_coverage < num_output_depth):
num_output_depth = args.stat_coverage
for depth in range(num_output_depth):
file_total_by_depth = stats['OWNERS-count-by-depth'][depth]
file_with_component_by_depth =\
stats['OWNERS-with-component-only-count-by-depth'][depth]
file_pct_with_component_by_depth = "N/A"
if file_total_by_depth > 0:
file_pct_with_component_by_depth = "{0:.2f}".format(
100.0 * file_with_component_by_depth / file_total_by_depth)
file_with_team_component_by_depth =\
stats['OWNERS-with-team-and-component-count-by-depth'][depth]
file_pct_with_team_component_by_depth = "N/A"
if file_total_by_depth > 0:
file_pct_with_team_component_by_depth = "{0:.2f}".format(
100.0 * file_with_team_component_by_depth / file_total_by_depth)
print('%(file_total_by_depth)d OWNERS files at depth %(depth)d' % {
'file_total_by_depth': file_total_by_depth,
'depth': depth
})
print('have COMPONENT: %(file_with_component_by_depth)d, '\
'percentage: %(file_pct_with_component_by_depth)s%%' % {
'file_with_component_by_depth':
file_with_component_by_depth,
'file_pct_with_component_by_depth':
file_pct_with_component_by_depth})
print('have COMPONENT and TEAM: %(file_with_team_component_by_depth)d,'\
'percentage: %(file_pct_with_team_component_by_depth)s%%' % {
'file_with_team_component_by_depth':
file_with_team_component_by_depth,
'file_pct_with_team_component_by_depth':
file_pct_with_team_component_by_depth})
def display_missing_info_OWNERS_files(stats, num_output_depth):
"""Display OWNERS files that have missing team and component by depth.
OWNERS files that have no team and no component information will be shown
for each depth level (up to the level given by num_output_depth).
Args:
stats (dict): The statistics in dictionary form as produced by the
owners_file_tags module.
num_output_depth (int): number of levels to be displayed.
"""
print("OWNERS files that have missing team and component by depth:")
max_output_depth = len(stats['OWNERS-count-by-depth'])
if (num_output_depth < 0
or num_output_depth > max_output_depth):
num_output_depth = max_output_depth
for depth in range(num_output_depth):
print('at depth %(depth)d' % {'depth': depth})
print(stats['OWNERS-missing-info-by-depth'][depth])
def main():
usage = """Usage: python %prog [options] [<root_dir>]
root_dir specifies the topmost directory to traverse looking for OWNERS
files, defaults to two levels up from this file's directory.
i.e. where src/ is expected to be.
Examples:
python %prog
python %prog /b/build/src
python %prog -v /b/build/src
python %prog -w /b/build/src
python %prog -o ~/components.json /b/build/src
python %prog -c /b/build/src
python %prog -s 3 /b/build/src
python %prog -m 2 /b/build/src
"""
parser = argparse.ArgumentParser(usage=usage)
parser.add_argument('-w',
'--write',
action='store_true',
help='If no errors occur, write the mappings to disk.')
parser.add_argument('-v',
'--verbose',
action='store_true',
help='Print warnings.')
parser.add_argument('-o',
'--output_file',
help='Specify file to write the '
'mappings to instead of the default: <CWD>/'
'component_map.json (implies -w)')
parser.add_argument('-c',
'--complete_coverage',
action='store_true',
help='Print complete coverage statistic')
parser.add_argument('-s',
'--stat_coverage',
type=int,
help='Specify directory depth to display coverage stats')
parser.add_argument(
'--include-subdirs',
action='store_true',
default=False,
help='List subdirectories without OWNERS file or component '
'tag as having same component as parent')
parser.add_argument('-m',
'--list_missing_info_by_depth',
type=int,
help='List OWNERS files that have missing team and '
'component information by depth')
args, root_dir = parser.parse_known_args()
if root_dir:
root = root_dir[0]
else:
root = _DEFAULT_SRC_LOCATION
scrape_result = scrape_owners(root, include_subdirs=args.include_subdirs)
mappings, warnings, stats = aggregate_components_from_owners(scrape_result,
root)
if args.verbose:
for w in warnings:
print(w)
if args.stat_coverage or args.complete_coverage:
display_stat(stats, root, args)
if args.list_missing_info_by_depth:
display_missing_info_OWNERS_files(stats, args.list_missing_info_by_depth)
mappings['AAA-README']= _README
mapping_file_contents = json.dumps(mappings, sort_keys=True, indent=2)
if args.write or args.output_file:
write_results(args.output_file, mapping_file_contents)
else:
print(mapping_file_contents)
return 0
if __name__ == '__main__':
sys.exit(main())