From d563d4fb65d0065ad3d61dd5c4390cd21adcb4f2 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:53:38 +0100 Subject: [PATCH 1/4] Add multi-file support in Cpp2RustTest.py --- tests/lit/lit/formats/Cpp2RustTest.py | 516 +++++++++++++++++--------- 1 file changed, 341 insertions(+), 175 deletions(-) diff --git a/tests/lit/lit/formats/Cpp2RustTest.py b/tests/lit/lit/formats/Cpp2RustTest.py index c6375902..f3795ad8 100644 --- a/tests/lit/lit/formats/Cpp2RustTest.py +++ b/tests/lit/lit/formats/Cpp2RustTest.py @@ -4,127 +4,130 @@ import lit.Test import lit.util from .base import TestFormat +from dataclasses import dataclass +from typing import Optional, Tuple import difflib import os import re import shutil -import random +import tomli -def read_rust_version(): - toolchain_path = os.path.join( - os.path.dirname(__file__), "../../../../cmake/rust-toolchain.cmake" - ) - with open(toolchain_path, "r") as f: - for line in f: - m = re.match(r'set\(RUST_STABLE_VERSION\s+"([^"]+)', line) - if m: - return m.group(1) - raise Exception("could not find rust version in " + toolchain_path) +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) -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())) +@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 -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 + def needs_cpp(self): + return not ( + self.should_panic or self.is_nondet_result or self.should_not_compile ) - 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" - - 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): + @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()) + os.path.join(os.path.dirname(__file__), "../../../../build/tmp/cargo-target") + ) - 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") +def cargo_env(): + return dict(os.environ, CARGO_TARGET_DIR=os.path.abspath(shared_target_dir())) + + 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] ) - rs_file = tmp_dir + "/src/main.rs" + tmp_dir = "tmp/" + fname + "-" + model + shutil.rmtree(tmp_dir, True) os.makedirs(tmp_dir + "/src") - def fail(str, code=fail_code): - shutil.rmtree(tmp_dir, True) - return code, str + 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)), + ) - cmd = ["./cpp2rust/cpp2rust", "-file", cc_input, "-model", model, "-o", rs_file] + 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(rs_file): - return fail( + if not os.path.exists(self.rs_file): + return ( + exp.fail_code, "no out file (rc=" + str(returncode) + ")\n" @@ -134,54 +137,92 @@ def fail(str, code=fail_code): + "\nstderr: " + err + "\nstdout: " - + out + + out, ) - with open(rs_file, "r") as f: - generated = f.read() + with open(self.rs_file, "r") as f: + self.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", - ) + 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 fail("different output\n" + diff) - - pkg_name = "test_" + re.sub(r"[^a-zA-Z0-9_]", "_", os.path.basename(tmp_dir)) + ) + 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) + ) - # Check if we can compile the rust file - with open(tmp_dir + "/Cargo.toml", "w") as f: + 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 = "{rust_version}" [[bin]] -name = "{pkg_name}" +name = "{self.pkg_name}" path = "src/main.rs" [dependencies] @@ -189,56 +230,181 @@ def fail(str, code=fail_code): libcc2rs = {{ path = "../../../libcc2rs" }} """) - cmd = ["cargo", "+" + self.rust_version, "build", "--release", "--quiet"] - _, err, returncode = lit.util.executeCommand(cmd, tmp_dir, env=cargo_env()) - if should_not_compile: + 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: - shutil.rmtree(tmp_dir, True) - return lit.Test.XFAIL, "" - return fail("expected no-compile but compiled successfully") + return (lit.Test.XFAIL, "") + return (exp.fail_code, "expected no-compile but compiled successfully") if returncode != 0: - return fail("cargo failed\n" + err) + 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 - rust_bin = os.path.join(shared_target_dir(), "release", pkg_name) + def finalize(self, result): + shutil.rmtree(self.tmp_dir, True) + return result - 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 + +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 ): - return fail( - "different output\n" + out_cpp + err_cpp + out_rs + err_rs - ) + yield t - if should_fail: - return fail("did not fail as expected", lit.Test.FAIL) - shutil.rmtree(tmp_dir, True) - return lit.Test.PASS, "" +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 2f5fb5f2b2ce323bbf1e598ab460f83997df7170 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 11:54:32 +0100 Subject: [PATCH 2/4] 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 592934f0..ea38b1c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,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 07937bcc437a1d356d976d4e1b3a61fa4f66fe86 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 11 May 2026 14:22:26 +0100 Subject: [PATCH 3/4] 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 f3795ad8..6232824a 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): @@ -53,62 +49,63 @@ def matches(match): def cargo_env(): return dict(os.environ, CARGO_TARGET_DIR=os.path.abspath(shared_target_dir())) - 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)), @@ -125,7 +122,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=" @@ -140,9 +137,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, "") @@ -150,22 +144,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( @@ -181,24 +177,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") @@ -207,14 +200,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" @@ -231,7 +222,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, "") @@ -239,46 +232,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 @@ -288,7 +296,6 @@ def __init__(self): def execute(self, test, litConfig): ctx = TestContext.setup(test) - result = ( ctx.translate() or ctx.check_expected() @@ -296,13 +303,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) @@ -312,72 +316,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) @@ -391,20 +379,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) From dc8afd8eb07714b2afeda22e237ce7e3bb54a3b4 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sat, 16 May 2026 12:27:09 +0100 Subject: [PATCH 4/4] Delete rust-toolchain setup --- tests/lit/lit/formats/Cpp2RustTest.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/lit/lit/formats/Cpp2RustTest.py b/tests/lit/lit/formats/Cpp2RustTest.py index 6232824a..e4df3b55 100644 --- a/tests/lit/lit/formats/Cpp2RustTest.py +++ b/tests/lit/lit/formats/Cpp2RustTest.py @@ -11,7 +11,6 @@ import os import re import shutil -import tomli MODELS = ("refcount", "unsafe") @@ -42,12 +41,6 @@ def matches(match): if models is None or models.strip() == "": return True return model in re.split(r"\s*,\s*", models.strip()) - 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())) xfail_m = RE_XFAIL.search(text) xfail = xfail_m is not None and model in re.split(r"\s*,\s*", xfail_m.group(1)) @@ -199,18 +192,13 @@ 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_]", "_", self.tmp_dir.name) - (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" edition = "2021" -rust-version = "{rust_version}" [[bin]] name = "{self.pkg_name}" @@ -221,7 +209,7 @@ def build_rust(self): libcc2rs = {{ path = "../../../libcc2rs" }} """) - cmd = ["cargo", "build", "--release", "--quiet"] + cmd = ["cargo", "+" + read_rust_version(), "build", "--release", "--quiet"] _, err, returncode = lit.util.executeCommand( cmd, str(self.tmp_dir), env=cargo_env() ) @@ -332,8 +320,15 @@ def getTestsInDirectory(self, testSuite, path_in_suite, litConfig, localConfig): def read_rust_version(): - toolchain_path = Path(__file__).parent / "../../../../libcc2rs/rust-toolchain.toml" - return tomli.loads(toolchain_path.read_text())["toolchain"]["channel"] + toolchain_path = os.path.join( + os.path.dirname(__file__), "../../../../cmake/rust-toolchain.cmake" + ) + with open(toolchain_path, "r") as f: + for line in f: + m = re.match(r'set\(RUST_STABLE_VERSION\s+"([^"]+)', line) + if m: + return m.group(1) + raise Exception("could not find rust version in " + toolchain_path) def shared_target_dir():