Skip to content

Commit c04de30

Browse files
committed
Add tests for py_binary issues
1 parent f3f9831 commit c04de30

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

test/unit/codechecker_script/BUILD

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2026 Ericsson AB
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load(
16+
"@rules_cc//cc:defs.bzl",
17+
"cc_library",
18+
)
19+
load(
20+
"//:defs.bzl",
21+
"codechecker_test",
22+
)
23+
load(
24+
"//test/unit/codechecker_script:action_test.bzl",
25+
"ANALYZE_OPTIONS",
26+
"action_test_suite",
27+
)
28+
29+
# Defect free library: the tests check how the analysis is invoked
30+
cc_library(
31+
name = "clean_lib",
32+
srcs = ["clean.cc"],
33+
tags = ["manual"],
34+
)
35+
36+
# Real analyses, they also serve as subjects of the analysis tests below
37+
codechecker_test(
38+
name = "monolithic_test",
39+
size = "small",
40+
analyze = ANALYZE_OPTIONS,
41+
targets = [":clean_lib"],
42+
)
43+
44+
codechecker_test(
45+
name = "per_file_test",
46+
size = "small",
47+
analyze = ANALYZE_OPTIONS,
48+
per_file = True,
49+
targets = [":clean_lib"],
50+
)
51+
52+
action_test_suite(
53+
name = "action",
54+
monolithic = ":monolithic_test",
55+
per_file = ":per_file_test",
56+
)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright 2026 Ericsson AB
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Analysis tests for how the CodeChecker action invokes its script py_binary.
17+
18+
NOTE: Both invariants below fail only under remote execution,
19+
therefore they are asserted at analysis time:
20+
21+
- The action must execute py_binary itself, not a per-target symlink to it.
22+
Symlink points into the local output base and breaks runfiles lookup.
23+
The stub looks for runfiles next to argv[0], and follows argv[0] when it is
24+
a symlink, into the local output base, where a remote worker has nothing.
25+
26+
- Arguments must not carry literal shell quoting.
27+
ctx.actions.args() builds argv directly, so quotes are ordinary characters.
28+
They become quoting when the script interpolates them into its shell command,
29+
collapsing the analyze options into one argument.
30+
"""
31+
32+
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
33+
34+
# The following is used for quoting test.
35+
# NOTE: two or more options are needed, a single option survives the quoting
36+
ANALYZE_OPTIONS = [
37+
"--ctu",
38+
"--disable=cplusplus",
39+
]
40+
41+
def _codechecker_actions(env):
42+
"""Return the actions with the CodeChecker mnemonic"""
43+
return [
44+
action
45+
for action in analysistest.target_actions(env)
46+
if action.mnemonic == "CodeChecker"
47+
]
48+
49+
def _assert_executes_script_directly(env, action, script_name):
50+
"""Assert argv[0] is the shared py_binary, not a per-target symlink.
51+
52+
Args:
53+
env: Analysis test environment.
54+
action: Action to inspect.
55+
script_name: Basename of the expected script executable.
56+
"""
57+
label = analysistest.target_under_test(env).label
58+
argv0 = action.argv[0]
59+
60+
asserts.true(
61+
env,
62+
argv0.endswith(script_name),
63+
"Expected argv[0] to be the {} executable, got: {}".format(
64+
script_name,
65+
argv0,
66+
),
67+
)
68+
69+
# A per-target symlink is declared in the target's own output directory,
70+
# so its path contains the target name. The py_binary lives under src/.
71+
asserts.false(
72+
env,
73+
"/{}/{}".format(label.name, script_name) in argv0,
74+
"Per-target symlink instead of the {} py_binary: {}".format(
75+
script_name,
76+
argv0,
77+
),
78+
)
79+
80+
def _assert_no_shell_quoting(env, action):
81+
"""Assert that no argument carries literal quote characters.
82+
83+
Args:
84+
env: Analysis test environment.
85+
action: Action to inspect.
86+
"""
87+
for arg in action.argv:
88+
asserts.false(
89+
env,
90+
"'" in arg,
91+
"Argument carries a literal quote: {}".format(arg),
92+
)
93+
94+
def _monolithic_action_test_impl(ctx):
95+
env = analysistest.begin(ctx)
96+
97+
actions = _codechecker_actions(env)
98+
asserts.equals(
99+
env,
100+
1,
101+
len(actions),
102+
"Expected exactly one CodeChecker action, got {}".format(len(actions)),
103+
)
104+
105+
if len(actions) == 1:
106+
action = actions[0]
107+
_assert_executes_script_directly(env, action, "codechecker_script")
108+
_assert_no_shell_quoting(env, action)
109+
110+
# The options should reach script as a single --analyze=<opts> token:
111+
# argparse rejects a separate value that starts with a dash.
112+
expected = "--analyze=" + " ".join(ANALYZE_OPTIONS)
113+
asserts.true(
114+
env,
115+
expected in action.argv,
116+
"Expected {} in argv, got: {}".format(expected, action.argv),
117+
)
118+
119+
return analysistest.end(env)
120+
121+
monolithic_action_test = analysistest.make(_monolithic_action_test_impl)
122+
123+
def _per_file_action_test_impl(ctx):
124+
env = analysistest.begin(ctx)
125+
126+
actions = _codechecker_actions(env)
127+
asserts.true(
128+
env,
129+
len(actions) > 0,
130+
"Expected at least one CodeChecker action",
131+
)
132+
133+
for action in actions:
134+
_assert_executes_script_directly(env, action, "per_file_script")
135+
_assert_no_shell_quoting(env, action)
136+
137+
return analysistest.end(env)
138+
139+
per_file_action_test = analysistest.make(_per_file_action_test_impl)
140+
141+
def action_test_suite(name, monolithic, per_file):
142+
"""Instantiate the analysis tests for both rule flavours.
143+
144+
Args:
145+
name: Name prefix of the generated targets.
146+
monolithic: Target using the monolithic rule.
147+
per_file: Target using per_file = True.
148+
"""
149+
monolithic_action_test(
150+
name = name + "_monolithic_test",
151+
size = "small",
152+
target_under_test = monolithic,
153+
)
154+
155+
per_file_action_test(
156+
name = name + "_per_file_test",
157+
size = "small",
158+
target_under_test = per_file,
159+
)
160+
161+
native.test_suite(
162+
name = name,
163+
tests = [
164+
name + "_monolithic_test",
165+
name + "_per_file_test",
166+
],
167+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Defect free source, the tests check how the analysis is invoked
2+
3+
int add(int first, int second)
4+
{
5+
return first + second;
6+
}

0 commit comments

Comments
 (0)