Skip to content

Commit 0f6c6a3

Browse files
allevatoswiple-rules-gardener
authored andcommitted
Record warnings from the Xcode rules as structured information in dummy actions so that we can test them.
PiperOrigin-RevId: 631862483
1 parent f1bbadc commit 0f6c6a3

File tree

3 files changed

+159
-49
lines changed

3 files changed

+159
-49
lines changed

test/test_helpers.bzl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,47 @@ FIXTURE_TAGS = [
2626
"manual",
2727
]
2828

29+
def assert_warning(env, msg_id, data = None):
30+
"""Asserts that a warning was printed.
31+
32+
The logic in this helper is specifically meant to handle warnings printed by
33+
`xcode_config.bzl`, because we want to preserve the behavior of the original
34+
rules and their tests.
35+
36+
Args:
37+
env: The analysis test environment.
38+
msg_id: The fixed identifier of the warning message.
39+
data: The semicolon-delimited, sorted by key, `key=value` string
40+
representation of the format arguments for the message, if any.
41+
"""
42+
expected = "Warning:{}".format(msg_id)
43+
if data:
44+
expected += ":{}".format(data)
45+
46+
found_warnings = []
47+
48+
actions = analysistest.target_actions(env)
49+
for action in actions:
50+
mnemonic = action.mnemonic
51+
if mnemonic == expected:
52+
return
53+
if mnemonic.startswith("Warning:"):
54+
found_warnings.append(mnemonic)
55+
56+
found_warnings_msg = ""
57+
if found_warnings:
58+
found_warnings_msg = "; the following warnings were emitted:\n{}".format(
59+
"\n".join(found_warnings),
60+
)
61+
62+
analysistest.fail(
63+
env,
64+
"Expected warning '{}' was not emitted{}".format(
65+
expected,
66+
found_warnings_msg,
67+
),
68+
)
69+
2970
def find_action(env, mnemonic):
3071
"""Finds the first action with the given mnemonic in the target under test.
3172

test/xcode_config_test.bzl

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ load(
2121
)
2222
load(
2323
"@build_bazel_apple_support//xcode:xcode_config.bzl",
24+
"UNAVAILABLE_XCODE_MESSAGE",
2425
"xcode_config",
2526
)
2627
load(
@@ -35,7 +36,13 @@ load(
3536
"@build_bazel_apple_support//xcode/private:providers.bzl",
3637
"XcodeVersionPropertiesInfo",
3738
) # buildifier: disable=bzl-visibility
38-
load(":test_helpers.bzl", "FIXTURE_TAGS", "find_action", "make_all_tests")
39+
load(
40+
":test_helpers.bzl",
41+
"FIXTURE_TAGS",
42+
"assert_warning",
43+
"find_action",
44+
"make_all_tests",
45+
)
3946

4047
visibility("private")
4148

@@ -333,6 +340,12 @@ def _prefers_flag_over_mutually_available_test_impl(ctx):
333340
asserts.true(env, "no-local" in xcode_version_info.execution_info())
334341
asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info())
335342

343+
assert_warning(
344+
env,
345+
"version_not_available_locally",
346+
"command={};local_versions=8.4;version=5.1.2".format(UNAVAILABLE_XCODE_MESSAGE),
347+
)
348+
336349
return analysistest.end(env)
337350

338351
_prefers_flag_over_mutually_available_test = analysistest.make(
@@ -369,16 +382,18 @@ def _warn_with_explicit_local_only_version_test_impl(ctx):
369382
target_under_test = analysistest.target_under_test(env)
370383
xcode_version_info = target_under_test[apple_common.XcodeVersionConfig]
371384

372-
# TODO: b/311385128 - Once we move the rules to apple_support, hack up
373-
# something that would let us actually test the warning messages. We can't
374-
# test `print`.
375-
376385
asserts.equals(env, "8.4", str(xcode_version_info.xcode_version()))
377386
asserts.equals(env, "local", xcode_version_info.availability())
378387
asserts.true(env, "requires-darwin" in xcode_version_info.execution_info())
379388
asserts.true(env, "no-remote" in xcode_version_info.execution_info())
380389
asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info())
381390

391+
assert_warning(
392+
env,
393+
"explicit_version_not_available_remotely",
394+
"version=8.4",
395+
)
396+
382397
return analysistest.end(env)
383398

384399
_warn_with_explicit_local_only_version_test = analysistest.make(
@@ -415,16 +430,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_main_version_test_impl(
415430
target_under_test = analysistest.target_under_test(env)
416431
xcode_version_info = target_under_test[apple_common.XcodeVersionConfig]
417432

418-
# TODO: b/311385128 - Once we move the rules to apple_support, hack up
419-
# something that would let us actually test the warning messages. We can't
420-
# test `print`.
421-
422433
asserts.equals(env, "8.4", str(xcode_version_info.xcode_version()))
423434
asserts.equals(env, "local", xcode_version_info.availability())
424435
asserts.true(env, "requires-darwin" in xcode_version_info.execution_info())
425436
asserts.true(env, "no-remote" in xcode_version_info.execution_info())
426437
asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info())
427438

439+
assert_warning(
440+
env,
441+
"local_default_not_available_remotely",
442+
"local_version=8.4;remote_versions=5.1.2",
443+
)
444+
428445
return analysistest.end(env)
429446

430447
_prefer_local_default_if_no_mutual_no_flag_different_main_version_test = analysistest.make(
@@ -460,16 +477,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_build_alias_test_impl(c
460477
target_under_test = analysistest.target_under_test(env)
461478
xcode_version_info = target_under_test[apple_common.XcodeVersionConfig]
462479

463-
# TODO: b/311385128 - Once we move the rules to apple_support, hack up
464-
# something that would let us actually test the warning messages. We can't
465-
# test `print`.
466-
467480
asserts.equals(env, "10.0.0.10C504", str(xcode_version_info.xcode_version()))
468481
asserts.equals(env, "local", xcode_version_info.availability())
469482
asserts.true(env, "requires-darwin" in xcode_version_info.execution_info())
470483
asserts.true(env, "no-remote" in xcode_version_info.execution_info())
471484
asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info())
472485

486+
assert_warning(
487+
env,
488+
"local_default_not_available_remotely",
489+
"local_version=10.0.0.10C504;remote_versions=10.0",
490+
)
491+
473492
return analysistest.end(env)
474493

475494
_prefer_local_default_if_no_mutual_no_flag_different_build_alias_test = analysistest.make(
@@ -505,16 +524,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_full_version_test_impl(
505524
target_under_test = analysistest.target_under_test(env)
506525
xcode_version_info = target_under_test[apple_common.XcodeVersionConfig]
507526

508-
# TODO: b/311385128 - Once we move the rules to apple_support, hack up
509-
# something that would let us actually test the warning messages. We can't
510-
# test `print`.
511-
512527
asserts.equals(env, "10.0.0.10C504", str(xcode_version_info.xcode_version()))
513528
asserts.equals(env, "local", xcode_version_info.availability())
514529
asserts.true(env, "requires-darwin" in xcode_version_info.execution_info())
515530
asserts.true(env, "no-remote" in xcode_version_info.execution_info())
516531
asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info())
517532

533+
assert_warning(
534+
env,
535+
"local_default_not_available_remotely",
536+
"local_version=10.0.0.10C504;remote_versions=10.0.0.101ff",
537+
)
538+
518539
return analysistest.end(env)
519540

520541
_prefer_local_default_if_no_mutual_no_flag_different_full_version_test = analysistest.make(
@@ -554,10 +575,6 @@ def _choose_newest_mutual_xcode_test_impl(ctx):
554575
target_under_test = analysistest.target_under_test(env)
555576
xcode_version_info = target_under_test[apple_common.XcodeVersionConfig]
556577

557-
# TODO: b/311385128 - Once we move the rules to apple_support, hack up
558-
# something that would let us actually test the warning messages. We can't
559-
# test `print`.
560-
561578
asserts.equals(env, "10", str(xcode_version_info.xcode_version()))
562579
asserts.equals(env, "both", xcode_version_info.availability())
563580
asserts.true(env, "requires-darwin" in xcode_version_info.execution_info())

xcode/xcode_config.bzl

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ load(
2323

2424
visibility("public")
2525

26-
unavailable_xcode_message = "'bazel fetch --configure' (Bzlmod) or 'bazel sync --configure' (WORKSPACE)"
26+
UNAVAILABLE_XCODE_MESSAGE = "'bazel fetch --configure' (Bzlmod) or 'bazel sync --configure' (WORKSPACE)"
2727

2828
def _xcode_config_impl(ctx):
2929
apple_fragment = ctx.fragments.apple
@@ -54,6 +54,7 @@ def _xcode_config_impl(ctx):
5454
remote_versions,
5555
):
5656
xcode_version_properties, availability = _resolve_xcode_from_local_and_remote(
57+
ctx.actions,
5758
local_versions,
5859
remote_versions,
5960
apple_fragment.xcode_version_flag,
@@ -208,6 +209,7 @@ def _aliases_to_xcode_version(versions):
208209
return version_map
209210

210211
def _resolve_xcode_from_local_and_remote(
212+
actions,
211213
local_versions,
212214
remote_versions,
213215
xcode_version_flag,
@@ -256,32 +258,29 @@ def _resolve_xcode_from_local_and_remote(
256258
)
257259

258260
elif local_version_from_flag:
259-
error = (
260-
" --xcode_version={} specified, but it is not available remotely. Actions " +
261-
"requiring Xcode will be run locally, which could make your build slower."
262-
).format(
263-
xcode_version_flag,
264-
)
265-
if (mutually_available_versions):
266-
error += " Consider using one of [{}].".format(
267-
", ".join([version for version in mutually_available_versions]),
261+
if mutually_available_versions:
262+
_warn(
263+
actions,
264+
"explicit_version_not_available_remotely_consider_mutual",
265+
version = xcode_version_flag,
266+
mutual_versions = [version for version in mutually_available_versions],
267+
)
268+
else:
269+
_warn(
270+
actions,
271+
"explicit_version_not_available_remotely",
272+
version = xcode_version_flag,
268273
)
269-
270-
# buildifier: disable=print
271-
print(error)
272274
return local_version_from_flag.xcode_version_properties, "LOCAL"
273275

274276
elif remote_version_from_flag:
275-
# buildifier: disable=print
276-
print(("--xcode_version={version} specified, but it is not available locally. " +
277-
"Your build will fail if any actions require a local Xcode. " +
278-
"If you believe you have '{version}' installed, try running {command}," +
279-
"and then re-run your command. Locally available versions: {local_versions}. ")
280-
.format(
277+
_warn(
278+
actions,
279+
"version_not_available_locally",
281280
version = xcode_version_flag,
282-
command = unavailable_xcode_message,
281+
command = UNAVAILABLE_XCODE_MESSAGE,
283282
local_versions = ", ".join([version for version in local_alias_to_version_map.keys()]),
284-
))
283+
)
285284
availability = "REMOTE"
286285

287286
return remote_version_from_flag.xcode_version_properties, availability
@@ -293,7 +292,7 @@ def _resolve_xcode_from_local_and_remote(
293292
" you believe you have '{0}' installed, try running {1}, and then" +
294293
" re-run your command.").format(
295294
xcode_version_flag,
296-
unavailable_xcode_message,
295+
UNAVAILABLE_XCODE_MESSAGE,
297296
", ".join([version.xcode_version_properties.xcode_version for version in local_versions]),
298297
", ".join([version.xcode_version_properties.xcode_version for version in remote_versions]),
299298
),
@@ -305,12 +304,11 @@ def _resolve_xcode_from_local_and_remote(
305304

306305
# If there aren't any mutually available versions, select the local default.
307306
if not mutually_available_versions:
308-
# buildifier: disable=print
309-
print(
310-
("Using a local Xcode version, '{}', since there are no" +
311-
" remotely available Xcodes on this machine. Consider downloading one of the" +
312-
" remotely available Xcode versions ({}) in order to get the best build" +
313-
" performance.").format(local_default_version.xcode_version_properties.xcode_version, ", ".join([version.xcode_version_properties.xcode_version for version in remote_versions])),
307+
_warn(
308+
actions,
309+
"local_default_not_available_remotely",
310+
local_version = local_default_version.xcode_version_properties.xcode_version,
311+
remote_versions = ", ".join([version.xcode_version_properties.xcode_version for version in remote_versions]),
314312
)
315313
local_version = local_default_version
316314
availability = "LOCAL"
@@ -385,3 +383,57 @@ def _resolve_explicitly_defined_version(
385383

386384
def _dotted_version_or_default(field, default):
387385
return apple_common.dotted_version(field) or default
386+
387+
_WARNINGS = {
388+
"version_not_available_locally": """\
389+
--xcode_version={version} specified, but it is not available locally. \
390+
Your build will fail if any actions require a local Xcode. \
391+
If you believe you have '{version}' installed, try running {command}, \
392+
and then re-run your command. Locally available versions: {local_versions}.
393+
""",
394+
"local_default_not_available_remotely": """\
395+
Using a local Xcode version, '{local_version}', since there are no \
396+
remotely available Xcodes on this machine. Consider downloading one of the \
397+
remotely available Xcode versions ({remote_versions}) in order to get the best \
398+
build performance.
399+
""",
400+
"explicit_version_not_available_remotely": """\
401+
--xcode_version={version} specified, but it is not available remotely. Actions \
402+
requiring Xcode will be run locally, which could make your build slower.
403+
""",
404+
"explicit_version_not_available_remotely_consider_mutual": """\
405+
--xcode_version={version} specified, but it is not available remotely. Actions \
406+
requiring Xcode will be run locally, which could make your build slower. \
407+
Consider using one of [{mutual_versions}].
408+
""",
409+
}
410+
411+
def _warn(actions, msg_id, **kwargs):
412+
"""Print a warning and also record it as a testable dummy action.
413+
414+
Starlark doesn't support testing the output of the `print()` function, so
415+
this function also registers a dummy action with a specifically formatted
416+
mnemonic that can be read in the test using the `assert_warning` helper.
417+
418+
Args:
419+
actions: The object used to register actions.
420+
msg_id: A string identifying the warning as a key in the `_WARNINGS`
421+
dictionary.
422+
**kwargs: Formatting arguments for the message string.
423+
"""
424+
425+
# buildifier: disable=print
426+
print(_WARNINGS[msg_id].format(**kwargs))
427+
428+
mnemonic = "Warning:{}".format(msg_id)
429+
if kwargs:
430+
# Sort the format arguments by key so that they're deterministically
431+
# ordered for tests.
432+
sorted_values = []
433+
for key in sorted(kwargs.keys()):
434+
sorted_values.append("{}={}".format(key, str(kwargs[key])))
435+
mnemonic += ":{}".format(";".join(sorted_values))
436+
437+
actions.do_nothing(
438+
mnemonic = mnemonic,
439+
)

0 commit comments

Comments
 (0)