1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import filecmp
18import logging
19import os
20import re
21import shutil
22import tempfile
23
24from acts.libs.proc import job
25from acts.libs.proto import proto_utils
26
27COMMIT_ID_ENV_KEY = 'PREUPLOAD_COMMIT'
28GIT_FILE_NAMES_CMD = 'git diff-tree --no-commit-id --name-status -r %s'
29
30
31def proto_generates_gen_file(proto_file, proto_gen_file):
32    """Check that the proto_gen_file matches the compilation result of the
33    proto_file.
34
35    Params:
36        proto_file: The proto file.
37        proto_gen_file: The generated file.
38
39    Returns: True if the compiled proto matches the given proto_gen_file.
40    """
41    with tempfile.TemporaryDirectory() as tmp_dir:
42        module_name = proto_utils.compile_proto(proto_file, tmp_dir)
43        if not module_name:
44            return False
45        tmp_proto_gen_file = os.path.join(tmp_dir, '%s.py' % module_name)
46        return filecmp.cmp(tmp_proto_gen_file, proto_gen_file)
47
48
49def verify_protos_update_generated_files(proto_files, proto_gen_files):
50    """For each proto file in proto_files, find the corresponding generated
51    file in either proto_gen_files, or in the directory tree of the proto.
52    Verify that the generated file matches the compilation result of the proto.
53
54    Params:
55        proto_files: List of proto files.
56        proto_gen_files: List of generated files.
57    """
58    success = True
59    gen_files = set(proto_gen_files)
60    for proto_file in proto_files:
61        gen_filename = os.path.basename(proto_file).replace('.proto', '_pb2.py')
62        gen_file = ''
63        # search the gen_files set first
64        for path in gen_files:
65            if (os.path.basename(path) == gen_filename
66                    and path.startswith(os.path.dirname(proto_file))):
67                gen_file = path
68                gen_files.remove(path)
69                break
70        else:
71            # search the proto file's directory
72            for root, _, filenames in os.walk(os.path.dirname(proto_file)):
73                for filename in filenames:
74                    if filename == gen_filename:
75                        gen_file = os.path.join(root, filename)
76                        break
77                if gen_file:
78                    break
79
80        # check that the generated file matches the compiled proto
81        if gen_file and not proto_generates_gen_file(proto_file, gen_file):
82            logging.error('Proto file %s does not compile to %s'
83                          % (proto_file, gen_file))
84            protoc = shutil.which('protoc')
85            if protoc:
86                protoc_command = ' '.join([
87                    protoc, '-I=%s' % os.path.dirname(proto_file),
88                    '--python_out=%s' % os.path.dirname(gen_file), proto_file])
89                logging.error('Run this command to re-generate file from proto'
90                              '\n%s' % protoc_command)
91            success = False
92
93    return success
94
95
96def main():
97    if COMMIT_ID_ENV_KEY not in os.environ:
98        logging.error('Missing commit id in environment.')
99        exit(1)
100
101    # get list of *.proto and *_pb2.py files from commit, then compare
102    proto_files = []
103    proto_gen_files = []
104    git_cmd = GIT_FILE_NAMES_CMD % os.environ[COMMIT_ID_ENV_KEY]
105    lines = job.run(git_cmd).stdout.splitlines()
106    for line in lines:
107        match = re.match(r'(\S+)\s+(.*)', line)
108        status, f = match.group(1), match.group(2)
109        if status != 'D':
110            if f.endswith('.proto'):
111                proto_files.append(os.path.abspath(f))
112            if f.endswith('_pb2.py'):
113                proto_gen_files.append(os.path.abspath(f))
114    exit(not verify_protos_update_generated_files(proto_files, proto_gen_files))
115
116
117if __name__ == '__main__':
118    main()
119