blob: 1c57fe063e659c43907c8f04f7e2b6c17950d773 [file]
# Copyright (c) 2026 The WebRTC project authors. All Rights Reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file in the root of the source
# tree. An additional intellectual property rights grant can be found
# in the file PATENTS. All contributing project authors may
# be found in the AUTHORS file in the root of the source tree.
"""Swaps gtest macro arguments with "Yoda testing"
"Yoda testing" is where the constant comes first, and the value being
tested comes second. This script will detect some cases of that, and
swap the arguments.
It depends on detecting constants, which is a heuristic, and it only handles
order-independent cases (ASSERT_EQ), not comparators like ASSERT_LT.
"""
import re
import sys
def is_constant(arg):
arg = arg.strip()
# Casts
if (arg.startswith('static_cast<') or arg.startswith('reinterpret_cast<')):
match = re.search(r'cast<.*?>\s*\((.*)\)', arg, re.DOTALL)
if match:
return is_constant(match.group(1))
# Casts by brace initialization
match = re.match(r'^\w+\{\s*(.\w*)\}$', arg)
if match:
return is_constant(match.group(1))
# Numeric literals
if re.match(r'^-?\d+[Uu]?[Ll]{0,2}$', arg):
return True
if re.match(r'^0x[0-9a-fA-F]+[Uu]?$', arg):
return True
if re.match(r'^-?\d+\.\d+[fF]?$', arg):
return True
# String literals
if arg.startswith('"') and arg.endswith('"'):
return True
# Boolean literals
if arg in ['true', 'false']:
return True
# Nulls
if arg in ['nullptr', 'NULL', 'std::nullopt']:
return True
# k-prefixed constants
if re.match(r'^k[A-Z]\w*$', arg):
return True
# ALL_CAPS
if re.match(r'^[A-Z][A-Z0-9_]*$', arg):
return True
# Qualified enums
if '::' in arg:
parts = arg.split('::')
last_part = parts[-1].strip()
if (re.match(r'^[A-Z][A-Z0-9_]*$', last_part)
or re.match(r'^k[A-Z][a-zA-Z0-9]*$', last_part)):
return True
return False
def split_args(args_str):
args = []
current = []
depth = 0
in_string = False
i = 0
while i < len(args_str):
char = args_str[i]
if char == '"' and (i == 0 or args_str[i - 1] != '\\'):
in_string = not in_string
if not in_string:
if char in '({[<':
depth += 1
elif char in ')}]>':
depth -= 1
elif char == ',' and depth == 0:
args.append(''.join(current))
current = []
i += 1
continue
current.append(char)
i += 1
args.append(''.join(current))
return args
def find_matching_paren(content, start_idx):
depth = 0
in_string = False
for i in range(start_idx, len(content)):
char = content[i]
if char == '"' and (i == 0 or content[i - 1] != '\\'):
in_string = not in_string
if not in_string:
if char == '(':
depth += 1
elif char == ')':
depth -= 1
if depth == 0:
return i
return -1
def find_macro_calls(content):
macro_names = ['EXPECT_EQ', 'EXPECT_NE', 'ASSERT_EQ', 'ASSERT_NE']
results = []
for name in macro_names:
start_pos = 0
while True:
idx = content.find(name + '(', start_pos)
if idx == -1:
break
end_idx = find_matching_paren(content, idx + len(name))
if end_idx != -1:
args_content = content[idx + len(name) + 1:end_idx]
results.append((idx, end_idx + 1, name, args_content))
start_pos = end_idx + 1
else:
start_pos = idx + len(name)
return sorted(results, key=lambda x: x[0], reverse=True)
def process_content(content):
calls = find_macro_calls(content)
new_content = content
for start, end, macro, args_str in calls:
args = split_args(args_str)
if len(args) != 2:
continue
arg1 = args[0]
arg2 = args[1]
if is_constant(arg1) and not is_constant(arg2):
new_call = f"{macro}({arg2.strip()}, {arg1.strip()})"
new_content = new_content[:start] + new_call + new_content[end:]
return new_content
def process_file(file_path):
with open(file_path, 'r') as f_in:
content = f_in.read()
new_content = process_content(content)
with open(file_path, 'w') as f_out:
f_out.write(new_content)
if __name__ == "__main__":
process_file(sys.argv[1])