| # 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]) |