1"""Copyright (C) 2022 The Android Open Source Project
2
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7     http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License.
14"""
15
16load("@bazel_skylib//lib:paths.bzl", "paths")
17load("@bazel_skylib//lib:sets.bzl", "sets")
18load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
19load(":cc_proto.bzl", "PROTO_GEN_NAME_SUFFIX", "cc_proto_library")
20
21PROTO_GEN = "external/protobuf/bin/aprotoc/aprotoc"
22VIRTUAL_IMPORT = "_virtual_imports"
23RUNFILES = "_middlemen/external_Sprotobuf_Sbin_Saprotoc_Saprotoc-runfiles"
24
25GEN_SUFFIX = [
26    ".pb.h",
27    ".pb.cc",
28]
29
30def _get_search_paths(action):
31    cmd = action.argv
32    search_paths = sets.make()
33    cmd_len = len(cmd)
34    for i in range(cmd_len):
35        if cmd[i].startswith("-I"):
36            sets.insert(search_paths, cmd[i].lstrip("- I"))
37
38    return search_paths
39
40def _proto_code_gen_test_impl(ctx):
41    env = analysistest.begin(ctx)
42    target_under_test = analysistest.target_under_test(env)
43    actions = analysistest.target_actions(env)
44    package_root = ctx.label.package
45    local_file_output_path = paths.join(
46        package_root,
47        target_under_test.label.name,
48        package_root,
49    )
50
51    input_files = [
52        ctx.attr.local_file_path,
53        ctx.attr.external_file_path,
54        ctx.attr.deps_file_path,
55    ]
56
57    output_files = [
58        ctx.attr.local_file_path,
59        ctx.attr.external_file_path,
60    ]
61
62    asserts.true(
63        env,
64        len(actions) == 1,
65        "Proto gen action not found: %s" % actions,
66    )
67
68    action = actions[0]
69
70    asserts.set_equals(
71        env,
72        expected = sets.make(
73            [paths.join(package_root, file) for file in input_files] + [
74                PROTO_GEN,
75                RUNFILES,
76            ],
77        ),
78        actual = sets.make([
79            file.short_path
80            for file in action.inputs.to_list()
81        ]),
82    )
83
84    asserts.set_equals(
85        env,
86        expected = sets.make(
87            [
88                paths.join(
89                    local_file_output_path,
90                    paths.replace_extension(file, ext),
91                )
92                for ext in GEN_SUFFIX
93                for file in output_files
94            ],
95        ),
96        actual = sets.make([
97            file.short_path
98            for file in action.outputs.to_list()
99        ]),
100    )
101
102    search_paths = _get_search_paths(action)
103
104    asserts.equals(
105        env,
106        expected = sets.make(
107            ["."] +
108            [paths.join(package_root, f) + "=" + paths.join(package_root, f) for f in input_files],
109        ),
110        actual = search_paths,
111    )
112
113    return analysistest.end(env)
114
115proto_code_gen_test = analysistest.make(
116    _proto_code_gen_test_impl,
117    attrs = {
118        "local_file_path": attr.string(),
119        "deps_file_path": attr.string(),
120        "external_file_path": attr.string(),
121    },
122)
123
124def _test_proto_code_gen():
125    test_name = "proto_code_gen_test"
126    local_file_path = "local/proto_local.proto"
127    external_file_path = "external/proto_external.proto"
128    deps_file_path = "deps/proto_deps.proto"
129    external_proto_name = test_name + "_external_proto"
130    deps_proto_name = test_name + "_deps_proto"
131    local_proto_name = test_name + "_proto"
132    cc_name = test_name + "_cc_proto"
133
134    native.proto_library(
135        name = external_proto_name,
136        srcs = [external_file_path],
137        tags = ["manual"],
138    )
139
140    native.proto_library(
141        name = deps_proto_name,
142        srcs = [deps_file_path],
143        tags = ["manual"],
144    )
145
146    native.proto_library(
147        name = local_proto_name,
148        srcs = [local_file_path],
149        deps = [":" + deps_proto_name],
150        tags = ["manual"],
151    )
152
153    cc_proto_library(
154        name = cc_name,
155        deps = [
156            ":" + local_proto_name,
157            ":" + external_proto_name,
158        ],
159        tags = ["manual"],
160    )
161
162    proto_code_gen_test(
163        name = test_name,
164        target_under_test = cc_name + PROTO_GEN_NAME_SUFFIX,
165        local_file_path = local_file_path,
166        deps_file_path = deps_file_path,
167        external_file_path = external_file_path,
168    )
169
170    return test_name
171
172def _proto_strip_import_prefix_test_impl(ctx):
173    env = analysistest.begin(ctx)
174    actions = analysistest.target_actions(env)
175    package_root = ctx.label.package
176
177    # strip the proto file path, src/stripped/stripped.proto -> stripped/stripped.proto
178    stripped_file_name = paths.relativize(ctx.attr.stripped_file_name, ctx.attr.strip_import_prefix)
179    stripped_file_input_path = paths.join(
180        package_root,
181        VIRTUAL_IMPORT,
182        ctx.attr.stripped_proto_name,
183    )
184    stripped_file_input_full_path = paths.join(
185        stripped_file_input_path,
186        stripped_file_name,
187    )
188
189    asserts.true(
190        env,
191        len(actions) == 1,
192        "Proto gen action not found: %s" % actions,
193    )
194
195    action = actions[0]
196
197    asserts.set_equals(
198        env,
199        expected = sets.make(
200            [
201                paths.join(package_root, ctx.attr.unstripped_file_name),
202                stripped_file_input_full_path,
203                PROTO_GEN,
204                RUNFILES,
205            ],
206        ),
207        actual = sets.make([
208            file.short_path
209            for file in action.inputs.to_list()
210        ]),
211    )
212
213    asserts.set_equals(
214        env,
215        expected = sets.make(ctx.attr.expected_outputs),
216        actual = sets.make([
217            file.short_path
218            for file in action.outputs.to_list()
219        ]),
220    )
221
222    search_paths = _get_search_paths(action)
223
224    asserts.equals(
225        env,
226        expected = sets.make([
227            ".",
228            paths.join(package_root, ctx.attr.unstripped_file_name) + "=" + paths.join(package_root, ctx.attr.unstripped_file_name),
229            stripped_file_input_full_path + "=" +
230            paths.join(
231                ctx.genfiles_dir.path,
232                stripped_file_input_full_path,
233            ),
234            paths.join(
235                ctx.genfiles_dir.path,
236                stripped_file_input_path,
237            ),
238        ]),
239        actual = search_paths,
240    )
241
242    return analysistest.end(env)
243
244proto_strip_import_prefix_test = analysistest.make(
245    _proto_strip_import_prefix_test_impl,
246    attrs = {
247        "stripped_proto_name": attr.string(),
248        "stripped_file_name": attr.string(),
249        "unstripped_file_name": attr.string(),
250        "strip_import_prefix": attr.string(),
251        "expected_outputs": attr.string_list(),
252    },
253)
254
255def _test_proto_strip_import_prefix():
256    test_name = "proto_strip_import_prefix_test"
257    unstripped_proto_name = test_name + "_unstripped_proto"
258    stripped_proto_name = test_name + "_stripped_proto"
259    unstripped_file_name = "unstripped/unstripped.proto"
260    stripped_file_name = "src/stripped/stripped.proto"
261    cc_name = test_name + "_cc_proto"
262    strip_import_prefix = "src"
263
264    native.proto_library(
265        name = unstripped_proto_name,
266        srcs = [unstripped_file_name],
267        tags = ["manual"],
268    )
269
270    native.proto_library(
271        name = stripped_proto_name,
272        srcs = [stripped_file_name],
273        strip_import_prefix = strip_import_prefix,
274        tags = ["manual"],
275    )
276
277    cc_proto_library(
278        name = cc_name,
279        deps = [
280            ":" + stripped_proto_name,
281            ":" + unstripped_proto_name,
282        ],
283        tags = ["manual"],
284    )
285
286    expected_outputs = [
287        # unstripped, the default behavior
288        # bazel package is added to the path
289        "build/bazel/rules/cc/proto_strip_import_prefix_test_cc_proto_proto_gen/build/bazel/rules/cc/unstripped/unstripped.pb.cc",
290        "build/bazel/rules/cc/proto_strip_import_prefix_test_cc_proto_proto_gen/build/bazel/rules/cc/unstripped/unstripped.pb.h",
291        # stripped - src/stripped/stripped.proto --> stripped/stripped.pb.cc
292        # since strip_import_prefix is not nil, the bazel package is not added to the path
293        "build/bazel/rules/cc/proto_strip_import_prefix_test_cc_proto_proto_gen/stripped/stripped.pb.cc",
294        "build/bazel/rules/cc/proto_strip_import_prefix_test_cc_proto_proto_gen/stripped/stripped.pb.h",
295    ]
296
297    proto_strip_import_prefix_test(
298        name = test_name,
299        target_under_test = cc_name + PROTO_GEN_NAME_SUFFIX,
300        stripped_proto_name = stripped_proto_name,
301        stripped_file_name = stripped_file_name,
302        unstripped_file_name = unstripped_file_name,
303        strip_import_prefix = strip_import_prefix,
304        expected_outputs = expected_outputs,
305    )
306
307    return test_name
308
309def _proto_with_external_packages_test_impl(ctx):
310    env = analysistest.begin(ctx)
311    target_under_test = analysistest.target_under_test(env)
312    actions = analysistest.target_actions(env)
313    package_root = ctx.label.package
314    deps_file_path = ctx.attr.deps_file_path
315    external_file_path = ctx.attr.external_file_path
316    local_file_path = ctx.attr.local_file_path
317
318    asserts.true(
319        env,
320        len(actions) == 1,
321        "Proto gen action not found: %s" % actions,
322    )
323
324    action = actions[0]
325
326    asserts.set_equals(
327        env,
328        expected = sets.make(
329            [
330                paths.join(package_root, local_file_path),
331                deps_file_path,
332                external_file_path,
333                PROTO_GEN,
334                RUNFILES,
335            ],
336        ),
337        actual = sets.make([
338            file.short_path
339            for file in action.inputs.to_list()
340        ]),
341    )
342
343    asserts.set_equals(
344        env,
345        expected = sets.make(
346            [
347                paths.join(
348                    package_root,
349                    target_under_test.label.name,
350                    package_root,
351                    paths.replace_extension(local_file_path, ext),
352                )
353                for ext in GEN_SUFFIX
354            ] +
355            [
356                paths.join(
357                    package_root,
358                    target_under_test.label.name,
359                    paths.replace_extension(external_file_path, ext),
360                )
361                for ext in GEN_SUFFIX
362            ],
363        ),
364        actual = sets.make([
365            file.short_path
366            for file in action.outputs.to_list()
367        ]),
368    )
369
370    search_paths = _get_search_paths(action)
371
372    asserts.equals(
373        env,
374        expected = sets.make([
375            ".",
376            paths.join(package_root, local_file_path) + "=" + paths.join(package_root, local_file_path),
377            deps_file_path + "=" + deps_file_path,
378            external_file_path + "=" + external_file_path,
379        ]),
380        actual = search_paths,
381    )
382
383    return analysistest.end(env)
384
385proto_with_external_packages_test = analysistest.make(
386    _proto_with_external_packages_test_impl,
387    attrs = {
388        "local_file_path": attr.string(),
389        "deps_file_path": attr.string(),
390        "external_file_path": attr.string(),
391    },
392)
393
394def _test_proto_with_external_packages():
395    test_name = "proto_with_external_packages_test"
396    proto_name = test_name + "_proto"
397    cc_name = test_name + "_cc_proto"
398    local_file_path = "local/proto_local.proto"
399    deps_file_path = "build/bazel/examples/cc/proto/deps/src/enums/proto_deps.proto"
400    external_file_path = "build/bazel/examples/cc/proto/external/src/enums/proto_external.proto"
401
402    native.proto_library(
403        name = proto_name,
404        srcs = [local_file_path],
405        deps = ["//build/bazel/examples/cc/proto/deps:deps_proto"],
406        tags = ["manual"],
407    )
408
409    cc_proto_library(
410        name = cc_name,
411        deps = [
412            ":" + proto_name,
413            "//build/bazel/examples/cc/proto/external:external_proto",
414        ],
415        tags = ["manual"],
416    )
417
418    proto_with_external_packages_test(
419        name = test_name,
420        target_under_test = cc_name + PROTO_GEN_NAME_SUFFIX,
421        local_file_path = local_file_path,
422        deps_file_path = deps_file_path,
423        external_file_path = external_file_path,
424    )
425
426    return test_name
427
428def cc_proto_test_suite(name):
429    native.test_suite(
430        name = name,
431        tests = [
432            _test_proto_code_gen(),
433            _test_proto_strip_import_prefix(),
434            _test_proto_with_external_packages(),
435        ],
436    )
437