From 20458ad81a542b1bb0df29955148913fb16a1658 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:53:38 +0100 Subject: [PATCH 1/5] Add multi-file support in Cpp2RustTest.py --- tests/lit/lit/formats/Cpp2RustTest.py | 564 +++++++++++++++++--------- 1 file changed, 382 insertions(+), 182 deletions(-) diff --git a/tests/lit/lit/formats/Cpp2RustTest.py b/tests/lit/lit/formats/Cpp2RustTest.py index b66ade16..beefdc7a 100644 --- a/tests/lit/lit/formats/Cpp2RustTest.py +++ b/tests/lit/lit/formats/Cpp2RustTest.py @@ -4,153 +4,219 @@ import lit.Test import lit.util from .base import TestFormat -import difflib, os, re, shutil, random +from dataclasses import dataclass +from typing import Optional, Tuple +import difflib +import os +import re +import shutil import tomli -def read_rust_version(): - toolchain_path = os.path.join(os.path.dirname(__file__), - '../../../../libcc2rs/rust-toolchain.toml') - with open(toolchain_path, 'rb') as f: - return tomli.load(f)['toolchain']['channel'] - -def shared_target_dir(): - return os.path.abspath(os.path.join( - os.path.dirname(__file__), - '../../../../build/tmp/cargo-target')) - -def cargo_env(): - return dict(os.environ, CARGO_TARGET_DIR=os.path.abspath(shared_target_dir())) - -class Cpp2RustTest(TestFormat): - def __init__(self): - self.regex_xfail = re.compile(r"//\s*XFAIL:\s*(.*)") - self.regex_panic = re.compile(r"//\s*panic\s*(?::\s*(.*))?$", re.MULTILINE) - self.regex_nocompile = re.compile(r"//\s*no-compile\s*(?::\s*(.*))?$", re.MULTILINE) - self.regex_translation_fail = re.compile(r"//\s*translation-fail\s*(?::\s*(.*))?$", re.MULTILINE) - self.regex_nondet_result = re.compile(r"//\s*nondet-result\s*(?::\s*(.*))?$", re.MULTILINE) - self.rust_version = read_rust_version() - os.environ['RUSTFLAGS'] = '-Awarnings' - - def updateExpected(self, generated, expected_path): - os.makedirs(os.path.dirname(expected_path), exist_ok=True) - with open(expected_path, 'w') as f: - f.write(generated) - def getExpectedFile(self, filepath, model, fname): - return filepath + "/out/" + model + "/" + fname + ".rs" +MODELS = ("refcount", "unsafe") +PTR_RE = re.compile(r"0x[0-9a-fA-F]+") + +_RE_XFAIL = re.compile(r"//\s*XFAIL:\s*(.*)") +_RE_PANIC = re.compile(r"//\s*panic\s*(?::\s*(.*))?$", re.MULTILINE) +_RE_NOCOMPILE = re.compile(r"//\s*no-compile\s*(?::\s*(.*))?$", re.MULTILINE) +_RE_TRANS_FAIL = re.compile(r"//\s*translation-fail\s*(?::\s*(.*))?$", re.MULTILINE) +_RE_NONDET = re.compile(r"//\s*nondet-result\s*(?::\s*(.*))?$", re.MULTILINE) + + +@dataclass +class TestExpectations: + should_panic: bool = False + should_not_compile: bool = False + should_not_translate: bool = False + is_nondet_result: bool = False + xfail: bool = False + fail_code: int = lit.Test.FAIL + + def needs_cpp(self): + return not ( + self.should_panic or self.is_nondet_result or self.should_not_compile + ) + + @classmethod + def parse(cls, text, model): + def matches(match): + if match is None: + return False + models = match.group(1) + if models is None or models.strip() == "": + return True + return model in re.split(r"\s*,\s*", models.strip()) + + e = cls() + xfail_m = _RE_XFAIL.search(text) + if xfail_m: + models = re.split(r"\s*,\s*", xfail_m.group(1)) + e.xfail = model in models + if e.xfail: + e.fail_code = lit.Test.XFAIL + e.should_panic = matches(_RE_PANIC.search(text)) + e.should_not_compile = matches(_RE_NOCOMPILE.search(text)) + e.should_not_translate = matches(_RE_TRANS_FAIL.search(text)) + e.is_nondet_result = matches(_RE_NONDET.search(text)) + return e + + +@dataclass +class TestContext: + cc_input: str + is_multi: bool + fname: str + filepath: str + model: str + tmp_dir: str + rs_file: str + expectations: TestExpectations + replace_expected: bool = False + skip_run: bool = False + build_dir: Optional[str] = None + generated: Optional[str] = None + pkg_name: Optional[str] = None + cpp_bin: Optional[str] = None + rust_bin: Optional[str] = None + cpp_result: Optional[Tuple[str, str, int]] = None + + @classmethod + def setup(cls, test): + cc_input = test.getFilePath() + is_multi = is_multi_file_test(cc_input) + model = test.getSourcePath().split("/")[-1] + fname = ( + os.path.basename(cc_input) + if is_multi + else os.path.splitext(os.path.basename(cc_input))[0] + ) + tmp_dir = "tmp/" + fname + "-" + model - def getTestsForPath(self, testSuite, path_in_suite, litConfig, localConfig): - source_path = testSuite.getSourcePath(path_in_suite) - for model in ["refcount", "unsafe"]: - yield lit.Test.Test(testSuite, path_in_suite + (model,), - localConfig, source_path) - - def getTestsInDirectory(self, testSuite, path_in_suite, - litConfig, localConfig): - source_path = testSuite.getSourcePath(path_in_suite) - for filename in os.listdir(source_path): - if filename.endswith('.cpp') or filename.endswith('.c'): - for t in self.getTestsForPath(testSuite, path_in_suite + (filename,), litConfig, localConfig): - yield t - - - def execute(self, test, litConfig): - replace_expected = os.environ.get('REPLACE_EXPECTED', False) - skip_run = os.environ.get('SKIP_RUN', False) - - cc_input = test.getFilePath() - fname = os.path.splitext(os.path.basename(cc_input))[0] - filepath = os.path.dirname(cc_input) - model = test.getSourcePath().split('/')[-1] - - with open(cc_input, 'r') as f: - text = f.read() - - def matches_model(match, model): - if match is None: - return False - models = match.group(1) - if models is None or models.strip() == '': - return True - return model in re.split(r'\s*,\s*', models.strip()) - - should_fail = False - fail_code = lit.Test.FAIL - xfail = self.regex_xfail.search(text) - if xfail: - xfail = re.split(r'\s*,\s*', xfail.group(1)) - should_fail = model in xfail - fail_code = lit.Test.XFAIL - - should_panic = matches_model(self.regex_panic.search(text), model) - should_not_compile = matches_model(self.regex_nocompile.search(text), model) - should_not_translate = matches_model(self.regex_translation_fail.search(text), model) - is_nondet_result = matches_model(self.regex_nondet_result.search(text), model) - - tmp_dir = "tmp/" + fname + "-" + model + "_" + format(random.getrandbits(64), "x") - rs_file = tmp_dir + "/src/main.rs" - shutil.rmtree(tmp_dir, True) - os.makedirs(tmp_dir + "/src") - - def fail(str, code = fail_code): - shutil.rmtree(tmp_dir, True) - return code, str - - cmd = ['./cpp2rust/cpp2rust', '-file', cc_input, '-model', model, - '-o', rs_file] - - out, err, returncode = lit.util.executeCommand(cmd) - - if not os.path.exists(rs_file): - return fail('no out file (rc=' + str(returncode) + ')\n' - + 'cmd: ' + ' '.join(cmd) + '\n' - + '\nstderr: ' + err - + '\nstdout: ' + out) - - with open(rs_file, 'r') as f: - generated = f.read() - - if returncode != 0: - if should_not_translate: - return lit.Test.XFAIL, '' - return fail('cpp2rust failed\n' + err) - - if should_not_translate: - return fail('expected translation-fail but cpp2rust succeeded') - - if not should_not_compile: - expected_file = self.getExpectedFile(filepath, model, fname) - if not os.path.exists(expected_file) and not replace_expected: - return fail('no expected file') - - if replace_expected: - self.updateExpected(generated, expected_file) - - with open(expected_file, 'r') as f: - expected = f.read() - - if generated != expected: - diff = ''.join(difflib.unified_diff( - expected.splitlines(keepends=True), - generated.splitlines(keepends=True), - fromfile='expected', tofile='generated')) - return fail('different output\n' + diff) - - pkg_name = "test_" + re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(tmp_dir)) - - # Check if we can compile the rust file - with open(tmp_dir + "/rust-toolchain.toml", 'w') as f: - f.write(f'[toolchain]\nchannel = "{self.rust_version}"\n') - with open(tmp_dir + "/Cargo.toml", 'w') as f: - f.write(f""" + shutil.rmtree(tmp_dir, True) + os.makedirs(tmp_dir + "/src") + + return cls( + cc_input=cc_input, + is_multi=is_multi, + fname=fname, + filepath=cc_input if is_multi else os.path.dirname(cc_input), + model=model, + tmp_dir=tmp_dir, + rs_file=tmp_dir + "/src/main.rs", + expectations=TestExpectations.parse(load_source_text(cc_input), model), + replace_expected=bool(os.environ.get("REPLACE_EXPECTED", False)), + skip_run=bool(os.environ.get("SKIP_RUN", False)), + ) + + def translate(self): + exp = self.expectations + if self.is_multi: + build_dir, err = setup_build_dir(self.tmp_dir, self.cc_input) + if err is not None: + return (exp.fail_code, err) + self.build_dir = build_dir + + cmd = cpp2rust_command(self.cc_input, self.build_dir, self.model, self.rs_file) + out, err, returncode = lit.util.executeCommand(cmd) + + if not os.path.exists(self.rs_file): + return ( + exp.fail_code, + "no out file (rc=" + + str(returncode) + + ")\n" + + "cmd: " + + " ".join(cmd) + + "\n" + + "\nstderr: " + + err + + "\nstdout: " + + out, + ) + + with open(self.rs_file, "r") as f: + self.generated = f.read() + + if returncode != 0: + if exp.should_not_translate: + return (lit.Test.XFAIL, "") + return (exp.fail_code, "cpp2rust failed\n" + err) + + if exp.should_not_translate: + return (exp.fail_code, "expected translation-fail but cpp2rust succeeded") + return None + + def check_expected(self): + exp = self.expectations + if exp.should_not_compile: + return None + + expected_file = get_expected_file(self.filepath, self.model, self.fname) + if not os.path.exists(expected_file) and not self.replace_expected: + return (exp.fail_code, "no expected file") + + if self.replace_expected: + update_expected(self.generated, expected_file) + + with open(expected_file, "r") as f: + expected = f.read() + + if self.generated != expected: + diff = "".join( + difflib.unified_diff( + expected.splitlines(keepends=True), + self.generated.splitlines(keepends=True), + fromfile="expected", + tofile="generated", + ) + ) + return (exp.fail_code, "different output\n" + diff) + return None + + def build_cpp(self): + exp = self.expectations + if self.skip_run or not exp.needs_cpp(): + return None + + if self.build_dir is not None: + cmd = ["cmake", "--build", self.build_dir] + _, _, rc = lit.util.executeCommand(cmd) + if rc != 0: + return (exp.fail_code, "cmake build failed") + self.cpp_bin = os.path.join(self.build_dir, "app") + return None + + cc = ( + os.environ.get("CC", "clang") + if self.cc_input.endswith(".c") + else os.environ.get("CXX", "clang++") + ) + self.cpp_bin = self.tmp_dir + "/cpp" + cmd = [cc, "-O3", "-o", self.cpp_bin, self.cc_input] + _, _, rc = lit.util.executeCommand(cmd) + if rc != 0: + return (exp.fail_code, cc + " failed") + return None + + def build_rust(self): + exp = self.expectations + rust_version = read_rust_version() + self.pkg_name = "test_" + re.sub( + r"[^a-zA-Z0-9_]", "_", os.path.basename(self.tmp_dir) + ) + + with open(self.tmp_dir + "/rust-toolchain.toml", "w") as f: + f.write(f'[toolchain]\nchannel = "{rust_version}"\n') + with open(self.tmp_dir + "/Cargo.toml", "w") as f: + f.write(f""" [package] -name = "{pkg_name}" +name = "{self.pkg_name}" version = "0.1.0" edition = "2021" -rust-version = "{self.rust_version}" +rust-version = "{rust_version}" [[bin]] -name = "{pkg_name}" +name = "{self.pkg_name}" path = "src/main.rs" [dependencies] @@ -158,47 +224,181 @@ def fail(str, code = fail_code): libcc2rs = {{ path = "../../../libcc2rs" }} """) - cmd = ['cargo', 'build', '--release', '--quiet'] - _, err, returncode = lit.util.executeCommand(cmd, tmp_dir, env=cargo_env()) - if should_not_compile: - if returncode != 0: - shutil.rmtree(tmp_dir, True) - return lit.Test.XFAIL, '' - return fail('expected no-compile but compiled successfully') - if returncode != 0: - return fail('cargo failed\n' + err) - - rust_bin = os.path.join(shared_target_dir(), "release", pkg_name) - - if not skip_run: - if should_panic: - _, err, returncode = lit.util.executeCommand(rust_bin) - err = str(err) - if not re.search(r"thread 'main' \(\d+\) panicked at", err) or returncode != 101: - return fail('expected panic\n' + err) - elif is_nondet_result: - lit.util.executeCommand(rust_bin) - else: - if cc_input.endswith('.c'): - cc = os.environ.get('CC', 'clang') - else: - cc = os.environ.get('CXX', 'clang++') - cmd = [cc, '-O3', '-o', tmp_dir + '/cpp', cc_input] - _, _, code = lit.util.executeCommand(cmd) - if code != 0: - return fail(cc + ' failed') - - out_cpp, err_cpp, code_cpp = lit.util.executeCommand(tmp_dir + '/cpp') - out_rs, err_rs, code_rs = lit.util.executeCommand(rust_bin) - # Normalize pointer addresses (0x...) since they differ between C++ and Rust - ptr_re = re.compile(r'0x[0-9a-fA-F]+') - out_cpp_cmp = ptr_re.sub('0xPTR', out_cpp) - out_rs_cmp = ptr_re.sub('0xPTR', out_rs) - if out_cpp_cmp != out_rs_cmp or code_cpp != code_rs or err_rs != err_cpp: - return fail('different output\n' + out_cpp + err_cpp + out_rs + err_rs) - - if should_fail: - return fail('did not fail as expected', lit.Test.FAIL) - - shutil.rmtree(tmp_dir, True) - return lit.Test.PASS, '' + cmd = ["cargo", "build", "--release", "--quiet"] + _, err, returncode = lit.util.executeCommand(cmd, self.tmp_dir, env=cargo_env()) + if exp.should_not_compile: + if returncode != 0: + return (lit.Test.XFAIL, "") + return (exp.fail_code, "expected no-compile but compiled successfully") + if returncode != 0: + return (exp.fail_code, "cargo failed\n" + err) + + self.rust_bin = os.path.join(shared_target_dir(), "release", self.pkg_name) + return None + + def run_cpp(self): + exp = self.expectations + if self.skip_run or not exp.needs_cpp(): + return None + out, err, rc = lit.util.executeCommand(self.cpp_bin) + self.cpp_result = (out, err, rc) + return None + + def run_rust(self): + exp = self.expectations + if self.skip_run: + return None + + if exp.should_panic: + _, err, rc = lit.util.executeCommand(self.rust_bin) + err = str(err) + if not re.search(r"thread 'main' \(\d+\) panicked at", err) or rc != 101: + return (exp.fail_code, "expected panic\n" + err) + return None + + if exp.is_nondet_result: + lit.util.executeCommand(self.rust_bin) + return None + + out_rs, err_rs, rc_rs = lit.util.executeCommand(self.rust_bin) + out_cpp, err_cpp, rc_cpp = self.cpp_result + out_cpp_cmp = PTR_RE.sub("0xPTR", out_cpp) + out_rs_cmp = PTR_RE.sub("0xPTR", out_rs) + if out_cpp_cmp != out_rs_cmp or rc_cpp != rc_rs or err_rs != err_cpp: + return ( + exp.fail_code, + "different output\n" + out_cpp + err_cpp + out_rs + err_rs, + ) + return None + + def finalize(self, result): + shutil.rmtree(self.tmp_dir, True) + return result + + +class Cpp2RustTest(TestFormat): + def __init__(self): + os.environ["RUSTFLAGS"] = "-Awarnings" + + def execute(self, test, litConfig): + ctx = TestContext.setup(test) + + result = ( + ctx.translate() + or ctx.check_expected() + or ctx.build_cpp() + or ctx.build_rust() + or ctx.run_cpp() + or ctx.run_rust() + ) + if result is not None: + return ctx.finalize(result) + + if ctx.expectations.xfail: + return ctx.finalize((lit.Test.FAIL, "did not fail as expected")) + return ctx.finalize((lit.Test.PASS, "")) + + def getTestsForPath(self, testSuite, path_in_suite, litConfig, localConfig): + source_path = testSuite.getSourcePath(path_in_suite) + for model in MODELS: + yield lit.Test.Test( + testSuite, path_in_suite + (model,), localConfig, source_path + ) + + def getTestsInDirectory(self, testSuite, path_in_suite, litConfig, localConfig): + source_path = testSuite.getSourcePath(path_in_suite) + if is_multi_file_test(source_path): + for t in self.getTestsForPath( + testSuite, path_in_suite, litConfig, localConfig + ): + yield t + return + for entry in os.listdir(source_path): + full = os.path.join(source_path, entry) + if os.path.isfile(full) and ( + entry.endswith(".cpp") or entry.endswith(".c") + ): + for t in self.getTestsForPath( + testSuite, path_in_suite + (entry,), litConfig, localConfig + ): + yield t + + +def read_rust_version(): + toolchain_path = os.path.join( + os.path.dirname(__file__), "../../../../libcc2rs/rust-toolchain.toml" + ) + with open(toolchain_path, "rb") as f: + return tomli.load(f)["toolchain"]["channel"] + + +def shared_target_dir(): + return os.path.abspath( + os.path.join(os.path.dirname(__file__), "../../../../build/tmp/cargo-target") + ) + + +def cargo_env(): + return dict(os.environ, CARGO_TARGET_DIR=os.path.abspath(shared_target_dir())) + + +def is_multi_file_test(path): + return os.path.isdir(path) and os.path.exists(os.path.join(path, "CMakeLists.txt")) + + +def load_source_text(cc_input): + if os.path.isdir(cc_input): + expectations_path = os.path.join(cc_input, "test.expectations") + if not os.path.exists(expectations_path): + return "" + lines = [] + with open(expectations_path, "r") as f: + for line in f: + s = line.strip() + if not s or s.startswith("#"): + continue + lines.append("// " + s) + return "\n".join(lines) + with open(cc_input, "r") as f: + return f.read() + + +def setup_build_dir(tmp_dir, cc_input): + build_dir = os.path.abspath(os.path.join(tmp_dir, "cmake-build")) + os.makedirs(build_dir, exist_ok=True) + cmd = [ + "cmake", + "-S", + cc_input, + "-B", + build_dir, + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + ] + _, err, rc = lit.util.executeCommand(cmd) + if rc != 0: + return None, "cmake configure failed\n" + err + return build_dir, None + + +def cpp2rust_command(cc_input, build_dir, model, rs_file): + if build_dir is not None: + return [ + "./cpp2rust/cpp2rust", + "-dir", + build_dir, + "-model", + model, + "-o", + rs_file, + ] + return ["./cpp2rust/cpp2rust", "-file", cc_input, "-model", model, "-o", rs_file] + + +def get_expected_file(filepath, model, fname): + return filepath + "/out/" + model + "/" + fname + ".rs" + + +def update_expected(generated, expected_path): + os.makedirs(os.path.dirname(expected_path), exist_ok=True) + with open(expected_path, "w") as f: + f.write(generated) From 167ce95c3e5002e78e9e445bfae2e061ae0da124 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:53:57 +0100 Subject: [PATCH 2/5] Check formatting of python file --- .github/workflows/format.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 4ce36d73..f2fa5775 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -54,3 +54,12 @@ jobs: cargo +nightly clippy --manifest-path rule-preprocessor/Cargo.toml --all-targets --all-features -- -Dwarnings cargo clippy --manifest-path libcc2rs/Cargo.toml --all-targets --all-features -- -Dwarnings cargo clippy --manifest-path libcc2rs-macros/Cargo.toml --all-targets --all-features -- -Dwarnings + + - name: Check Python (ruff) + uses: astral-sh/ruff-action@v3 + with: + args: 'check tests/lit/lit/formats/Cpp2RustTest.py' + - name: Check Python formatting (ruff) + uses: astral-sh/ruff-action@v3 + with: + args: 'format --check tests/lit/lit/formats/Cpp2RustTest.py' From 2f87bcae4b987737b428a5de2f3017c641d7a302 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:54:32 +0100 Subject: [PATCH 3/5] Add extern_functions multi-file test --- CMakeLists.txt | 1 + .../extern_functions/CMakeLists.txt | 3 ++ tests/multi-file/extern_functions/a.c | 8 ++++ tests/multi-file/extern_functions/b.c | 10 +++++ .../out/refcount/extern_functions.rs | 40 ++++++++++++++++++ .../out/unsafe/extern_functions.rs | 41 +++++++++++++++++++ .../extern_functions/test.expectations | 1 + 7 files changed, 104 insertions(+) create mode 100644 tests/multi-file/extern_functions/CMakeLists.txt create mode 100644 tests/multi-file/extern_functions/a.c create mode 100644 tests/multi-file/extern_functions/b.c create mode 100644 tests/multi-file/extern_functions/out/refcount/extern_functions.rs create mode 100644 tests/multi-file/extern_functions/out/unsafe/extern_functions.rs create mode 100644 tests/multi-file/extern_functions/test.expectations diff --git a/CMakeLists.txt b/CMakeLists.txt index 92b42487..6cfc450b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ add_custom_target("check-unit" ${LIT_FLAGS} "${PROJECT_SOURCE_DIR}/tests/unit" "${PROJECT_SOURCE_DIR}/tests/ub" + "${PROJECT_SOURCE_DIR}/tests/multi-file" DEPENDS cpp2rust libcc2rs USES_TERMINAL ) diff --git a/tests/multi-file/extern_functions/CMakeLists.txt b/tests/multi-file/extern_functions/CMakeLists.txt new file mode 100644 index 00000000..d4ca928c --- /dev/null +++ b/tests/multi-file/extern_functions/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +project(extern_functions LANGUAGES C) +add_executable(app a.c b.c) diff --git a/tests/multi-file/extern_functions/a.c b/tests/multi-file/extern_functions/a.c new file mode 100644 index 00000000..275393a7 --- /dev/null +++ b/tests/multi-file/extern_functions/a.c @@ -0,0 +1,8 @@ +#include + +extern int helper(int x); + +int main(void) { + assert(helper(42) == 43); + return 0; +} diff --git a/tests/multi-file/extern_functions/b.c b/tests/multi-file/extern_functions/b.c new file mode 100644 index 00000000..31197743 --- /dev/null +++ b/tests/multi-file/extern_functions/b.c @@ -0,0 +1,10 @@ +static int unrelated1(void) { return 1; } +static int unrelated2(void) { return 2; } +static int unrelated3(void) { return 3; } + +int helper(int x) { + (void)unrelated1(); + (void)unrelated2(); + (void)unrelated3(); + return x + 1; +} diff --git a/tests/multi-file/extern_functions/out/refcount/extern_functions.rs b/tests/multi-file/extern_functions/out/refcount/extern_functions.rs new file mode 100644 index 00000000..0e8f5a77 --- /dev/null +++ b/tests/multi-file/extern_functions/out/refcount/extern_functions.rs @@ -0,0 +1,40 @@ +extern crate libcc2rs; +use libcc2rs::*; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::io::prelude::*; +use std::io::{Read, Seek, Write}; +use std::os::fd::AsFd; +use std::rc::{Rc, Weak}; + +// a.rs +pub fn main() { + std::process::exit(main_0()); +} +fn main_0() -> i32 { + assert!( + (((({ + let _x: i32 = 42; + helper_0(_x) + }) == 43) as i32) + != 0) + ); + return 0; +} +// b.rs +pub fn unrelated1_1() -> i32 { + return 1; +} +pub fn unrelated2_2() -> i32 { + return 2; +} +pub fn unrelated3_3() -> i32 { + return 3; +} +pub fn helper_4(x: i32) -> i32 { + let x: Value = Rc::new(RefCell::new(x)); + ({ unrelated1_1() }); + ({ unrelated2_2() }); + ({ unrelated3_3() }); + return ((*x.borrow()) + 1); +} diff --git a/tests/multi-file/extern_functions/out/unsafe/extern_functions.rs b/tests/multi-file/extern_functions/out/unsafe/extern_functions.rs new file mode 100644 index 00000000..567f8b7c --- /dev/null +++ b/tests/multi-file/extern_functions/out/unsafe/extern_functions.rs @@ -0,0 +1,41 @@ +extern crate libc; +use libc::*; +extern crate libcc2rs; +use libcc2rs::*; +use std::collections::BTreeMap; +use std::io::{Read, Seek, Write}; +use std::os::fd::{AsFd, FromRawFd, IntoRawFd}; +use std::rc::Rc; + +// a.rs +pub fn main() { + unsafe { + std::process::exit(main_0() as i32); + } +} +unsafe fn main_0() -> i32 { + assert!( + ((((unsafe { + let _x: i32 = 42; + helper_0(_x) + }) == (43)) as i32) + != 0) + ); + return 0; +} +// b.rs +pub unsafe fn unrelated1_1() -> i32 { + return 1; +} +pub unsafe fn unrelated2_2() -> i32 { + return 2; +} +pub unsafe fn unrelated3_3() -> i32 { + return 3; +} +pub unsafe fn helper_4(mut x: i32) -> i32 { + (unsafe { unrelated1_1() }); + (unsafe { unrelated2_2() }); + (unsafe { unrelated3_3() }); + return ((x) + (1)); +} diff --git a/tests/multi-file/extern_functions/test.expectations b/tests/multi-file/extern_functions/test.expectations new file mode 100644 index 00000000..791ea66f --- /dev/null +++ b/tests/multi-file/extern_functions/test.expectations @@ -0,0 +1 @@ +no-compile From a838dc58fa509623b3e82d15fcac6d967fa1e501 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:58:49 +0100 Subject: [PATCH 4/5] Only check Cpp2RustTest.py --- .github/workflows/format.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f2fa5775..4ef33ade 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -58,8 +58,10 @@ jobs: - name: Check Python (ruff) uses: astral-sh/ruff-action@v3 with: - args: 'check tests/lit/lit/formats/Cpp2RustTest.py' + args: 'check' + src: tests/lit/lit/formats/Cpp2RustTest.py - name: Check Python formatting (ruff) uses: astral-sh/ruff-action@v3 with: - args: 'format --check tests/lit/lit/formats/Cpp2RustTest.py' + args: 'format --check' + src: tests/lit/lit/formats/Cpp2RustTest.py From bf0a4121ad6c92c9fc0ceb4125e1822962b91c8f Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 14:22:26 +0100 Subject: [PATCH 5/5] Various edits Drop _ from RE_ global variables Add compare step that compares cpp_result and rust_result Use Path instead of str for TestContext fields where it makes sense Simplify preconditions for each TestContext pass --- tests/lit/lit/formats/Cpp2RustTest.py | 257 +++++++++++++------------- 1 file changed, 126 insertions(+), 131 deletions(-) diff --git a/tests/lit/lit/formats/Cpp2RustTest.py b/tests/lit/lit/formats/Cpp2RustTest.py index beefdc7a..5e0c60cc 100644 --- a/tests/lit/lit/formats/Cpp2RustTest.py +++ b/tests/lit/lit/formats/Cpp2RustTest.py @@ -5,7 +5,8 @@ import lit.util from .base import TestFormat from dataclasses import dataclass -from typing import Optional, Tuple +from pathlib import Path +from typing import NamedTuple, Optional import difflib import os import re @@ -16,11 +17,11 @@ MODELS = ("refcount", "unsafe") PTR_RE = re.compile(r"0x[0-9a-fA-F]+") -_RE_XFAIL = re.compile(r"//\s*XFAIL:\s*(.*)") -_RE_PANIC = re.compile(r"//\s*panic\s*(?::\s*(.*))?$", re.MULTILINE) -_RE_NOCOMPILE = re.compile(r"//\s*no-compile\s*(?::\s*(.*))?$", re.MULTILINE) -_RE_TRANS_FAIL = re.compile(r"//\s*translation-fail\s*(?::\s*(.*))?$", re.MULTILINE) -_RE_NONDET = re.compile(r"//\s*nondet-result\s*(?::\s*(.*))?$", re.MULTILINE) +RE_XFAIL = re.compile(r"//\s*XFAIL:\s*(.*)") +RE_PANIC = re.compile(r"//\s*panic\s*(?::\s*(.*))?$", re.MULTILINE) +RE_NOCOMPILE = re.compile(r"//\s*no-compile\s*(?::\s*(.*))?$", re.MULTILINE) +RE_TRANS_FAIL = re.compile(r"//\s*translation-fail\s*(?::\s*(.*))?$", re.MULTILINE) +RE_NONDET = re.compile(r"//\s*nondet-result\s*(?::\s*(.*))?$", re.MULTILINE) @dataclass @@ -32,11 +33,6 @@ class TestExpectations: xfail: bool = False fail_code: int = lit.Test.FAIL - def needs_cpp(self): - return not ( - self.should_panic or self.is_nondet_result or self.should_not_compile - ) - @classmethod def parse(cls, text, model): def matches(match): @@ -47,62 +43,63 @@ def matches(match): return True return model in re.split(r"\s*,\s*", models.strip()) - e = cls() - xfail_m = _RE_XFAIL.search(text) - if xfail_m: - models = re.split(r"\s*,\s*", xfail_m.group(1)) - e.xfail = model in models - if e.xfail: - e.fail_code = lit.Test.XFAIL - e.should_panic = matches(_RE_PANIC.search(text)) - e.should_not_compile = matches(_RE_NOCOMPILE.search(text)) - e.should_not_translate = matches(_RE_TRANS_FAIL.search(text)) - e.is_nondet_result = matches(_RE_NONDET.search(text)) - return e + xfail_m = RE_XFAIL.search(text) + xfail = xfail_m is not None and model in re.split(r"\s*,\s*", xfail_m.group(1)) + return cls( + should_panic=matches(RE_PANIC.search(text)), + should_not_compile=matches(RE_NOCOMPILE.search(text)), + should_not_translate=matches(RE_TRANS_FAIL.search(text)), + is_nondet_result=matches(RE_NONDET.search(text)), + xfail=xfail, + fail_code=lit.Test.XFAIL if xfail else lit.Test.FAIL, + ) + + +class RunResult(NamedTuple): + stdout: str + stderr: str + returncode: int @dataclass class TestContext: - cc_input: str + cc_input: Path is_multi: bool fname: str - filepath: str + filepath: Path model: str - tmp_dir: str - rs_file: str + tmp_dir: Path + rs_file: Path expectations: TestExpectations replace_expected: bool = False skip_run: bool = False - build_dir: Optional[str] = None + build_dir: Optional[Path] = None generated: Optional[str] = None pkg_name: Optional[str] = None - cpp_bin: Optional[str] = None - rust_bin: Optional[str] = None - cpp_result: Optional[Tuple[str, str, int]] = None + cpp_bin: Optional[Path] = None + rust_bin: Optional[Path] = None + cpp_result: Optional[RunResult] = None + rust_result: Optional[RunResult] = None @classmethod def setup(cls, test): - cc_input = test.getFilePath() + cc_input = Path(test.getFilePath()) is_multi = is_multi_file_test(cc_input) model = test.getSourcePath().split("/")[-1] - fname = ( - os.path.basename(cc_input) - if is_multi - else os.path.splitext(os.path.basename(cc_input))[0] - ) - tmp_dir = "tmp/" + fname + "-" + model + fname = cc_input.name if is_multi else cc_input.stem + tmp_dir = Path("tmp") / f"{fname}-{model}" - shutil.rmtree(tmp_dir, True) - os.makedirs(tmp_dir + "/src") + shutil.rmtree(tmp_dir, ignore_errors=True) + (tmp_dir / "src").mkdir(parents=True) return cls( cc_input=cc_input, is_multi=is_multi, fname=fname, - filepath=cc_input if is_multi else os.path.dirname(cc_input), + filepath=cc_input if is_multi else cc_input.parent, model=model, tmp_dir=tmp_dir, - rs_file=tmp_dir + "/src/main.rs", + rs_file=tmp_dir / "src" / "main.rs", expectations=TestExpectations.parse(load_source_text(cc_input), model), replace_expected=bool(os.environ.get("REPLACE_EXPECTED", False)), skip_run=bool(os.environ.get("SKIP_RUN", False)), @@ -119,7 +116,7 @@ def translate(self): cmd = cpp2rust_command(self.cc_input, self.build_dir, self.model, self.rs_file) out, err, returncode = lit.util.executeCommand(cmd) - if not os.path.exists(self.rs_file): + if not self.rs_file.exists(): return ( exp.fail_code, "no out file (rc=" @@ -134,9 +131,6 @@ def translate(self): + out, ) - with open(self.rs_file, "r") as f: - self.generated = f.read() - if returncode != 0: if exp.should_not_translate: return (lit.Test.XFAIL, "") @@ -144,22 +138,24 @@ def translate(self): if exp.should_not_translate: return (exp.fail_code, "expected translation-fail but cpp2rust succeeded") + + self.generated = self.rs_file.read_text() return None def check_expected(self): exp = self.expectations + # We don't care if no-compile tests have a corresponding generated file. if exp.should_not_compile: return None expected_file = get_expected_file(self.filepath, self.model, self.fname) - if not os.path.exists(expected_file) and not self.replace_expected: + if not expected_file.exists() and not self.replace_expected: return (exp.fail_code, "no expected file") if self.replace_expected: update_expected(self.generated, expected_file) - with open(expected_file, "r") as f: - expected = f.read() + expected = expected_file.read_text() if self.generated != expected: diff = "".join( @@ -175,24 +171,21 @@ def check_expected(self): def build_cpp(self): exp = self.expectations - if self.skip_run or not exp.needs_cpp(): - return None - if self.build_dir is not None: - cmd = ["cmake", "--build", self.build_dir] + cmd = ["cmake", "--build", str(self.build_dir)] _, _, rc = lit.util.executeCommand(cmd) if rc != 0: return (exp.fail_code, "cmake build failed") - self.cpp_bin = os.path.join(self.build_dir, "app") + self.cpp_bin = self.build_dir / "app" return None cc = ( os.environ.get("CC", "clang") - if self.cc_input.endswith(".c") + if self.cc_input.suffix == ".c" else os.environ.get("CXX", "clang++") ) - self.cpp_bin = self.tmp_dir + "/cpp" - cmd = [cc, "-O3", "-o", self.cpp_bin, self.cc_input] + self.cpp_bin = self.tmp_dir / "cpp" + cmd = [cc, "-O3", "-o", str(self.cpp_bin), str(self.cc_input)] _, _, rc = lit.util.executeCommand(cmd) if rc != 0: return (exp.fail_code, cc + " failed") @@ -201,14 +194,12 @@ def build_cpp(self): def build_rust(self): exp = self.expectations rust_version = read_rust_version() - self.pkg_name = "test_" + re.sub( - r"[^a-zA-Z0-9_]", "_", os.path.basename(self.tmp_dir) - ) + self.pkg_name = "test_" + re.sub(r"[^a-zA-Z0-9_]", "_", self.tmp_dir.name) - with open(self.tmp_dir + "/rust-toolchain.toml", "w") as f: - f.write(f'[toolchain]\nchannel = "{rust_version}"\n') - with open(self.tmp_dir + "/Cargo.toml", "w") as f: - f.write(f""" + (self.tmp_dir / "rust-toolchain.toml").write_text( + f'[toolchain]\nchannel = "{rust_version}"\n' + ) + (self.tmp_dir / "Cargo.toml").write_text(f""" [package] name = "{self.pkg_name}" version = "0.1.0" @@ -225,7 +216,9 @@ def build_rust(self): """) cmd = ["cargo", "build", "--release", "--quiet"] - _, err, returncode = lit.util.executeCommand(cmd, self.tmp_dir, env=cargo_env()) + _, err, returncode = lit.util.executeCommand( + cmd, str(self.tmp_dir), env=cargo_env() + ) if exp.should_not_compile: if returncode != 0: return (lit.Test.XFAIL, "") @@ -233,46 +226,61 @@ def build_rust(self): if returncode != 0: return (exp.fail_code, "cargo failed\n" + err) - self.rust_bin = os.path.join(shared_target_dir(), "release", self.pkg_name) + self.rust_bin = shared_target_dir() / "release" / self.pkg_name return None def run_cpp(self): - exp = self.expectations - if self.skip_run or not exp.needs_cpp(): + if self.skip_run: return None - out, err, rc = lit.util.executeCommand(self.cpp_bin) - self.cpp_result = (out, err, rc) + self.cpp_result = RunResult(*lit.util.executeCommand(str(self.cpp_bin))) return None def run_rust(self): exp = self.expectations if self.skip_run: return None + self.rust_result = RunResult(*lit.util.executeCommand(str(self.rust_bin))) if exp.should_panic: - _, err, rc = lit.util.executeCommand(self.rust_bin) - err = str(err) - if not re.search(r"thread 'main' \(\d+\) panicked at", err) or rc != 101: + err = str(self.rust_result.stderr) + if ( + not re.search(r"thread 'main' \(\d+\) panicked at", err) + or self.rust_result.returncode != 101 + ): return (exp.fail_code, "expected panic\n" + err) - return None + return self.success_result() if exp.is_nondet_result: - lit.util.executeCommand(self.rust_bin) + return self.success_result() + return None + + def compare(self): + exp = self.expectations + if self.skip_run: return None - out_rs, err_rs, rc_rs = lit.util.executeCommand(self.rust_bin) - out_cpp, err_cpp, rc_cpp = self.cpp_result - out_cpp_cmp = PTR_RE.sub("0xPTR", out_cpp) - out_rs_cmp = PTR_RE.sub("0xPTR", out_rs) - if out_cpp_cmp != out_rs_cmp or rc_cpp != rc_rs or err_rs != err_cpp: + cpp = self.cpp_result + rs = self.rust_result + out_cpp_cmp = PTR_RE.sub("0xPTR", cpp.stdout) + out_rs_cmp = PTR_RE.sub("0xPTR", rs.stdout) + if ( + out_cpp_cmp != out_rs_cmp + or cpp.returncode != rs.returncode + or cpp.stderr != rs.stderr + ): return ( exp.fail_code, - "different output\n" + out_cpp + err_cpp + out_rs + err_rs, + "different output\n" + cpp.stdout + cpp.stderr + rs.stdout + rs.stderr, ) return None + def success_result(self): + if self.expectations.xfail: + return (lit.Test.FAIL, "did not fail as expected") + return (lit.Test.PASS, "") + def finalize(self, result): - shutil.rmtree(self.tmp_dir, True) + shutil.rmtree(self.tmp_dir, ignore_errors=True) return result @@ -282,7 +290,6 @@ def __init__(self): def execute(self, test, litConfig): ctx = TestContext.setup(test) - result = ( ctx.translate() or ctx.check_expected() @@ -290,13 +297,10 @@ def execute(self, test, litConfig): or ctx.build_rust() or ctx.run_cpp() or ctx.run_rust() + or ctx.compare() + or ctx.success_result() ) - if result is not None: - return ctx.finalize(result) - - if ctx.expectations.xfail: - return ctx.finalize((lit.Test.FAIL, "did not fail as expected")) - return ctx.finalize((lit.Test.PASS, "")) + return ctx.finalize(result) def getTestsForPath(self, testSuite, path_in_suite, litConfig, localConfig): source_path = testSuite.getSourcePath(path_in_suite) @@ -306,72 +310,56 @@ def getTestsForPath(self, testSuite, path_in_suite, litConfig, localConfig): ) def getTestsInDirectory(self, testSuite, path_in_suite, litConfig, localConfig): - source_path = testSuite.getSourcePath(path_in_suite) + source_path = Path(testSuite.getSourcePath(path_in_suite)) if is_multi_file_test(source_path): for t in self.getTestsForPath( testSuite, path_in_suite, litConfig, localConfig ): yield t return - for entry in os.listdir(source_path): - full = os.path.join(source_path, entry) - if os.path.isfile(full) and ( - entry.endswith(".cpp") or entry.endswith(".c") - ): + for entry in source_path.iterdir(): + if entry.is_file() and entry.suffix in (".cpp", ".c"): for t in self.getTestsForPath( - testSuite, path_in_suite + (entry,), litConfig, localConfig + testSuite, path_in_suite + (entry.name,), litConfig, localConfig ): yield t def read_rust_version(): - toolchain_path = os.path.join( - os.path.dirname(__file__), "../../../../libcc2rs/rust-toolchain.toml" - ) - with open(toolchain_path, "rb") as f: - return tomli.load(f)["toolchain"]["channel"] + toolchain_path = Path(__file__).parent / "../../../../libcc2rs/rust-toolchain.toml" + return tomli.loads(toolchain_path.read_text())["toolchain"]["channel"] def shared_target_dir(): - return os.path.abspath( - os.path.join(os.path.dirname(__file__), "../../../../build/tmp/cargo-target") - ) + return (Path(__file__).parent / "../../../../build/tmp/cargo-target").resolve() def cargo_env(): - return dict(os.environ, CARGO_TARGET_DIR=os.path.abspath(shared_target_dir())) + return dict(os.environ, CARGO_TARGET_DIR=str(shared_target_dir())) -def is_multi_file_test(path): - return os.path.isdir(path) and os.path.exists(os.path.join(path, "CMakeLists.txt")) +def is_multi_file_test(p): + return p.is_dir() and (p / "CMakeLists.txt").exists() def load_source_text(cc_input): - if os.path.isdir(cc_input): - expectations_path = os.path.join(cc_input, "test.expectations") - if not os.path.exists(expectations_path): + if cc_input.is_dir(): + expectations_path = cc_input / "test.expectations" + if not expectations_path.exists(): return "" - lines = [] - with open(expectations_path, "r") as f: - for line in f: - s = line.strip() - if not s or s.startswith("#"): - continue - lines.append("// " + s) - return "\n".join(lines) - with open(cc_input, "r") as f: - return f.read() + return "// " + expectations_path.read_text() + return cc_input.read_text() def setup_build_dir(tmp_dir, cc_input): - build_dir = os.path.abspath(os.path.join(tmp_dir, "cmake-build")) - os.makedirs(build_dir, exist_ok=True) + build_dir = (tmp_dir / "cmake-build").resolve() + build_dir.mkdir(parents=True, exist_ok=True) cmd = [ "cmake", "-S", - cc_input, + str(cc_input), "-B", - build_dir, + str(build_dir), "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ] _, err, rc = lit.util.executeCommand(cmd) @@ -385,20 +373,27 @@ def cpp2rust_command(cc_input, build_dir, model, rs_file): return [ "./cpp2rust/cpp2rust", "-dir", - build_dir, + str(build_dir), "-model", model, "-o", - rs_file, + str(rs_file), ] - return ["./cpp2rust/cpp2rust", "-file", cc_input, "-model", model, "-o", rs_file] + return [ + "./cpp2rust/cpp2rust", + "-file", + str(cc_input), + "-model", + model, + "-o", + str(rs_file), + ] def get_expected_file(filepath, model, fname): - return filepath + "/out/" + model + "/" + fname + ".rs" + return filepath / "out" / model / f"{fname}.rs" def update_expected(generated, expected_path): - os.makedirs(os.path.dirname(expected_path), exist_ok=True) - with open(expected_path, "w") as f: - f.write(generated) + expected_path.parent.mkdir(parents=True, exist_ok=True) + expected_path.write_text(generated)