diff --git a/bazel/wrapper_hook/plus_interface.py b/bazel/wrapper_hook/plus_interface.py index 2254a4323a0..93222bf6bd9 100644 --- a/bazel/wrapper_hook/plus_interface.py +++ b/bazel/wrapper_hook/plus_interface.py @@ -1,3 +1,4 @@ +import difflib import os import pathlib import platform @@ -183,7 +184,20 @@ def test_runner_interface( ): bin_targets.append(bin_target) real_target = bin_target - replacements[arg] = [real_target] + if not real_target: + # Target not found - suggest similar ones and pass through + suggestions = difflib.get_close_matches(test_name, sources_to_bin.keys(), n=5) + error_msg = f"WARNING: Target '{arg}' not found" + if suggestions: + error_msg += "\n\nDid you mean one of these?\n" + for suggestion in suggestions: + error_msg += f" +{suggestion}\n" + else: + error_msg += " and no similar targets." + print(error_msg, file=sys.stderr) + # Pass through the argument unchanged: it might be an important argument + else: + replacements[arg] = [real_target] else: # defer source targets to see if we can skip redundant tests source_targets[test_name] = [arg, real_target] diff --git a/buildscripts/tests/test_bazel_plus_test_interface.py b/buildscripts/tests/test_bazel_plus_test_interface.py index 2ca60acb13e..1edc1944440 100644 --- a/buildscripts/tests/test_bazel_plus_test_interface.py +++ b/buildscripts/tests/test_bazel_plus_test_interface.py @@ -1,5 +1,7 @@ import sys import unittest +from contextlib import redirect_stderr +from io import StringIO sys.path.append(".") @@ -10,6 +12,23 @@ from bazel.wrapper_hook.plus_interface import ( ) +def validate_first_suggestion(stderr_output: str, expected_suggestion: str): + assert ( + "Did you mean one of these?" in stderr_output + ), f"Expected 'Did you mean one of these?' in stderr, got: {stderr_output}" + + suggestion_section = stderr_output.split("Did you mean one of these?")[1] + first_suggestion_line = [ + line.strip() + for line in suggestion_section.split("\n") + if line.strip() and line.strip().startswith("+") + ][0] + + assert ( + first_suggestion_line == expected_suggestion + ), f"Expected first suggestion to be '{expected_suggestion}', got '{first_suggestion_line}'" + + class Tests(unittest.TestCase): def test_single_source_file(self): def buildozer_output(autocomplete_query): @@ -80,7 +99,9 @@ class Tests(unittest.TestCase): "+some_feature", ] - result = test_runner_interface(args, False, buildozer_output) + stderr_capture = StringIO() + with redirect_stderr(stderr_capture): + result = test_runner_interface(args, False, buildozer_output) assert result == [ "test", @@ -92,6 +113,10 @@ class Tests(unittest.TestCase): "--test_arg=source1|source2", ] + # Verify that the warning for +some_feature was captured + stderr_output = stderr_capture.getvalue() + assert "WARNING: Target '+some_feature' not found" in stderr_output + def test_single_bin_file(self): def buildozer_output(autocomplete_query): return "//some:test [source1.cpp source2.cpp]" @@ -185,7 +210,9 @@ class Tests(unittest.TestCase): args = ["wrapper_hook", "test", "+source1", "+source2", "+source3", "+source4"] - result = test_runner_interface(args, False, buildozer_output) + stderr_capture = StringIO() + with redirect_stderr(stderr_capture): + result = test_runner_interface(args, False, buildozer_output) assert result == [ "test", @@ -195,6 +222,10 @@ class Tests(unittest.TestCase): "--test_arg=source1|source3|source4", ] + # Verify that the warning for +source2 was captured + stderr_output = stderr_capture.getvalue() + assert "WARNING: Target '+source2' not found" in stderr_output + def test_prefixes(self): def buildozer_output(autocomplete_query): return "//some:test [source1.cpp source2.cpp source3.cpp s+ource4.cpp]" @@ -210,6 +241,64 @@ class Tests(unittest.TestCase): "--test_arg=source1|source2|source3", ] + def test_target_not_found_with_suggestions(self): + """Test that unrecognized targets pass through unchanged (not a test target).""" + + def buildozer_output(autocomplete_query): + return "//some:test [bson_obj_test.cpp bson_element_test.cpp other_test.cpp]" + + args = ["wrapper_hook", "test", "+bsonobj_test"] # Typo: missing underscore + + stderr_capture = StringIO() + with redirect_stderr(stderr_capture): + result = test_runner_interface(args, False, buildozer_output) + + # Should pass through unchanged since it's not a recognized target + assert result == ["test", "+bsonobj_test"] + + # Check that suggestions were printed + stderr_output = stderr_capture.getvalue() + validate_first_suggestion(stderr_output, "+bson_obj_test") + + def test_target_not_found_no_close_matches(self): + """Test that completely unrecognized targets pass through unchanged.""" + + def buildozer_output(autocomplete_query): + return "//some:test [bson_obj_test.cpp other_test.cpp]" + + args = ["wrapper_hook", "test", "+xyz123"] # Completely different target + + stderr_capture = StringIO() + with redirect_stderr(stderr_capture): + result = test_runner_interface(args, False, buildozer_output) + + # Should pass through unchanged + assert result == ["test", "+xyz123"] + + # Check that "no similar targets" message was printed + stderr_output = stderr_capture.getvalue() + assert ( + "and no similar targets" in stderr_output + ), f"Expected 'and no similar targets' in stderr output, got: {stderr_output}" + + def test_target_not_found_partial_match(self): + """Test that partial matches still pass through when not found.""" + + def buildozer_output(autocomplete_query): + return "//some:test [bson_obj_test.cpp bson_element_test.cpp bson_utf8_test.cpp]" + + args = ["wrapper_hook", "test", "+bson_obj"] # Missing '_test' suffix + + stderr_capture = StringIO() + with redirect_stderr(stderr_capture): + result = test_runner_interface(args, False, buildozer_output) + + # Should pass through unchanged + assert result == ["test", "+bson_obj"] + + stderr_output = stderr_capture.getvalue() + validate_first_suggestion(stderr_output, "+bson_obj_test") + if __name__ == "__main__": unittest.main()