Skip to content

Commit 4241857

Browse files
committed
ctest: Add translation of types and test generation, compilation and running.
1 parent 9cc3dab commit 4241857

File tree

21 files changed

+850
-66
lines changed

21 files changed

+850
-66
lines changed

ctest-next/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ repository = "https://github.com/rust-lang/libc"
88
publish = false
99

1010
[dependencies]
11+
askama = "0.14.0"
1112
cc = "1.2.25"
13+
quote = "1.0.40"
1214
syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] }
15+
tempfile = "3.20.0"

ctest-next/askama.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[escaper]]
2+
path = "askama::filters::Text"
3+
extensions = ["rs", "c", "cpp"]

ctest-next/build.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use std::env;
2+
3+
fn main() {
4+
let host = env::var("HOST").unwrap();
5+
let target = env::var("TARGET").unwrap();
6+
let target_key = target.replace('-', "_");
7+
8+
println!("cargo:rustc-env=HOST_PLATFORM={host}");
9+
println!("cargo:rustc-env=TARGET_PLATFORM={target}");
10+
11+
let linker = env::var(format!("CARGO_TARGET_{}_LINKER", target_key.to_uppercase()))
12+
.or_else(|_| env::var("CC"))
13+
.or_else(|_| env::var(format!("CC_{target_key}")))
14+
.unwrap_or_default();
15+
16+
let runner =
17+
env::var(format!("CARGO_TARGET_{}_RUNNER", target_key.to_uppercase())).unwrap_or_default();
18+
19+
// As we invoke rustc directly this does not get passed to it, although RUSTFLAGS does.
20+
let flags = env::var(format!(
21+
"CARGO_TARGET_{}_RUSTFLAGS",
22+
target_key.to_uppercase()
23+
))
24+
.unwrap_or_default();
25+
26+
println!("cargo:rustc-env=LINKER={linker}");
27+
println!("cargo:rustc-env=RUNNER={runner}");
28+
println!("cargo:rustc-env=FLAGS={flags}");
29+
30+
println!("cargo:rerun-if-changed-env=TARGET");
31+
}

ctest-next/src/ast/constant.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pub struct Const {
66
#[expect(unused)]
77
pub(crate) public: bool,
88
pub(crate) ident: BoxStr,
9-
#[expect(unused)]
109
pub(crate) ty: syn::Type,
1110
#[expect(unused)]
1211
pub(crate) expr: syn::Expr,

ctest-next/src/ffi_items.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ impl FfiItems {
5454
}
5555

5656
/// Return a list of all constants found.
57-
#[cfg_attr(not(test), expect(unused))]
5857
pub(crate) fn constants(&self) -> &Vec<Const> {
5958
&self.constants
6059
}

ctest-next/src/generator.rs

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,159 @@
1-
use std::path::Path;
1+
use std::{
2+
env,
3+
fs::File,
4+
io::Write,
5+
path::{Path, PathBuf},
6+
};
27

8+
use askama::Template;
39
use syn::visit::Visit;
410

5-
use crate::{expand, ffi_items::FfiItems, Result};
11+
use crate::{
12+
expand,
13+
ffi_items::FfiItems,
14+
template::{CTestTemplate, RustTestTemplate},
15+
Result,
16+
};
617

718
/// A builder used to generate a test suite.
819
#[non_exhaustive]
920
#[derive(Default, Debug, Clone)]
10-
pub struct TestGenerator {}
21+
pub struct TestGenerator {
22+
headers: Vec<String>,
23+
target: Option<String>,
24+
host: Option<String>,
25+
includes: Vec<PathBuf>,
26+
out_dir: Option<PathBuf>,
27+
}
1128

1229
impl TestGenerator {
1330
/// Creates a new blank test generator.
1431
pub fn new() -> Self {
1532
Self::default()
1633
}
1734

35+
/// Add a header to be included as part of the generated C file.
36+
pub fn header(&mut self, header: &str) -> &mut Self {
37+
self.headers.push(header.to_string());
38+
self
39+
}
40+
41+
/// Configures the target to compile C code for.
42+
pub fn target(&mut self, target: &str) -> &mut Self {
43+
self.target = Some(target.to_string());
44+
self
45+
}
46+
47+
/// Configures the host.
48+
pub fn host(&mut self, host: &str) -> &mut Self {
49+
self.host = Some(host.to_string());
50+
self
51+
}
52+
53+
/// Add a path to the C compiler header lookup path.
54+
///
55+
/// This is useful for if the C library is installed to a nonstandard
56+
/// location to ensure that compiling the C file succeeds.
57+
pub fn include<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
58+
self.includes.push(p.as_ref().to_owned());
59+
self
60+
}
61+
62+
/// Configures the output directory of the generated Rust and C code.
63+
pub fn out_dir<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
64+
self.out_dir = Some(p.as_ref().to_owned());
65+
self
66+
}
67+
1868
/// Generate all tests for the given crate and output the Rust side to a file.
19-
pub fn generate<P: AsRef<Path>>(&mut self, crate_path: P, _output_file_path: P) -> Result<()> {
69+
pub fn generate<P: AsRef<Path>>(&mut self, crate_path: P, output_file_path: P) -> Result<()> {
70+
let output_file_path = self.generate_files(crate_path, output_file_path)?;
71+
72+
let target = self
73+
.target
74+
.clone()
75+
.unwrap_or(env::var("TARGET_PLATFORM").unwrap());
76+
let host = self
77+
.host
78+
.clone()
79+
.unwrap_or(env::var("HOST_PLATFORM").unwrap());
80+
81+
let mut cfg = cc::Build::new();
82+
// FIXME: Cpp not supported.
83+
cfg.file(output_file_path.with_extension("c"));
84+
cfg.host(&host);
85+
if target.contains("msvc") {
86+
cfg.flag("/W3")
87+
.flag("/Wall")
88+
.flag("/WX")
89+
// ignored warnings
90+
.flag("/wd4820") // warning about adding padding?
91+
.flag("/wd4100") // unused parameters
92+
.flag("/wd4996") // deprecated functions
93+
.flag("/wd4296") // '<' being always false
94+
.flag("/wd4255") // converting () to (void)
95+
.flag("/wd4668") // using an undefined thing in preprocessor?
96+
.flag("/wd4366") // taking ref to packed struct field might be unaligned
97+
.flag("/wd4189") // local variable initialized but not referenced
98+
.flag("/wd4710") // function not inlined
99+
.flag("/wd5045") // compiler will insert Spectre mitigation
100+
.flag("/wd4514") // unreferenced inline function removed
101+
.flag("/wd4711"); // function selected for automatic inline
102+
} else {
103+
cfg.flag("-Wall")
104+
.flag("-Wextra")
105+
.flag("-Werror")
106+
.flag("-Wno-unused-parameter")
107+
.flag("-Wno-type-limits")
108+
// allow taking address of packed struct members:
109+
.flag("-Wno-address-of-packed-member")
110+
.flag("-Wno-unknown-warning-option")
111+
.flag("-Wno-deprecated-declarations"); // allow deprecated items
112+
}
113+
114+
for p in &self.includes {
115+
cfg.include(p);
116+
}
117+
118+
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
119+
cfg.target(&target)
120+
.out_dir(output_file_path.parent().unwrap())
121+
.compile(stem);
122+
123+
Ok(())
124+
}
125+
126+
/// Generate the Rust and C testing files.
127+
///
128+
/// Returns the path to the generated file.
129+
pub(crate) fn generate_files<P: AsRef<Path>>(
130+
&mut self,
131+
crate_path: P,
132+
output_file_path: P,
133+
) -> Result<PathBuf> {
20134
let expanded = expand(crate_path)?;
21135
let ast = syn::parse_file(&expanded)?;
22136

23137
let mut ffi_items = FfiItems::new();
24138
ffi_items.visit_file(&ast);
25139

26-
Ok(())
140+
let output_directory = self
141+
.out_dir
142+
.clone()
143+
.unwrap_or_else(|| PathBuf::from(env::var_os("OUT_DIR").unwrap()));
144+
let output_file_path = output_directory.join(output_file_path);
145+
146+
// Generate the Rust side of the tests.
147+
File::create(output_file_path.with_extension("rs"))?
148+
.write_all(RustTestTemplate::new(&ffi_items)?.render()?.as_bytes())?;
149+
150+
// Generate the C side of the tests.
151+
// FIXME: Cpp not supported yet.
152+
let c_output_path = output_file_path.with_extension("c");
153+
let headers = self.headers.iter().map(|h| h.as_str()).collect();
154+
File::create(&c_output_path)?
155+
.write_all(CTestTemplate::new(headers, &ffi_items).render()?.as_bytes())?;
156+
157+
Ok(output_file_path)
27158
}
28159
}

ctest-next/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ mod ast;
1515
mod ffi_items;
1616
mod generator;
1717
mod macro_expansion;
18+
mod runner;
19+
mod rustc_version;
20+
mod template;
21+
mod translator;
1822

1923
pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union};
2024
pub use generator::TestGenerator;
2125
pub use macro_expansion::expand;
26+
pub use runner::{compile_test, run_test};
27+
pub use rustc_version::{rustc_version, RustcVersion};
2228

2329
/// A possible error that can be encountered in our library.
2430
pub type Error = Box<dyn std::error::Error>;

ctest-next/src/runner.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::Result;
2+
use std::env;
3+
use std::fs::{canonicalize, File};
4+
use std::io::Write;
5+
use std::path::{Path, PathBuf};
6+
use std::process::Command;
7+
8+
/// Compiles a Rust source file and links it against a static library.
9+
///
10+
/// Returns the path to the generated binary.
11+
pub fn compile_test<P: AsRef<Path>>(
12+
output_dir: P,
13+
crate_path: P,
14+
library_file: P,
15+
) -> Result<PathBuf> {
16+
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
17+
let output_dir = output_dir.as_ref();
18+
let crate_path = crate_path.as_ref();
19+
let library_file = library_file.as_ref();
20+
21+
let rust_file = output_dir
22+
.join(crate_path.file_stem().unwrap())
23+
.with_extension("rs");
24+
let binary_path = output_dir.join(rust_file.file_stem().unwrap());
25+
26+
File::create(&rust_file)?.write_all(
27+
format!(
28+
"include!(r#\"{}\"#);\ninclude!(r#\"{}.rs\"#);",
29+
canonicalize(crate_path)?.display(),
30+
library_file.display()
31+
)
32+
.as_bytes(),
33+
)?;
34+
35+
let mut cmd = Command::new(rustc);
36+
cmd.arg(&rust_file)
37+
.arg(format!("-Lnative={}", output_dir.display()))
38+
.arg(format!(
39+
"-lstatic={}",
40+
library_file.file_stem().unwrap().to_str().unwrap()
41+
))
42+
.arg("--target")
43+
.arg(env::var("TARGET_PLATFORM").unwrap())
44+
.arg("-o")
45+
.arg(&binary_path)
46+
.arg("-Aunused");
47+
48+
let linker = env::var("LINKER").unwrap_or_default();
49+
if !linker.is_empty() {
50+
cmd.arg(format!("-Clinker={linker}"));
51+
}
52+
53+
let flags = env::var("FLAGS").unwrap_or_default();
54+
if !flags.is_empty() {
55+
cmd.args(flags.split_whitespace());
56+
}
57+
58+
let output = cmd.output()?;
59+
if !output.status.success() {
60+
return Err(std::str::from_utf8(&output.stderr)?.into());
61+
}
62+
63+
Ok(binary_path)
64+
}
65+
66+
/// Executes the compiled test binary and returns its output.
67+
pub fn run_test(test_binary: &str) -> Result<String> {
68+
let runner = env::var("RUNNER").unwrap_or_default();
69+
let output = if runner.is_empty() {
70+
Command::new(test_binary).output()?
71+
} else {
72+
let mut args = runner.split_whitespace();
73+
let mut cmd = Command::new(args.next().unwrap());
74+
cmd.args(args).arg(test_binary).output()?
75+
};
76+
77+
if !output.status.success() {
78+
return Err(std::str::from_utf8(&output.stderr)?.into());
79+
}
80+
81+
// The template prints to stderr regardless.
82+
Ok(std::str::from_utf8(&output.stderr)?.to_string())
83+
}

ctest-next/src/rustc_version.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::{env, fmt::Display, num::ParseIntError, process::Command};
2+
3+
use crate::Result;
4+
5+
/// Represents the current version of the rustc compiler globally in use.
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7+
pub struct RustcVersion {
8+
major: u8,
9+
minor: u8,
10+
patch: u8,
11+
}
12+
13+
impl RustcVersion {
14+
/// Define a rustc version with the given major.minor.patch.
15+
pub fn new(major: u8, minor: u8, patch: u8) -> Self {
16+
Self {
17+
major,
18+
minor,
19+
patch,
20+
}
21+
}
22+
}
23+
24+
impl Display for RustcVersion {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
write!(
27+
f,
28+
"RustcVersion({}, {}, {})",
29+
self.major, self.minor, self.patch
30+
)
31+
}
32+
}
33+
34+
/// Return the global rustc version.
35+
pub fn rustc_version() -> Result<RustcVersion> {
36+
let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
37+
38+
let output = Command::new(rustc).arg("--version").output()?;
39+
40+
if !output.status.success() {
41+
let error = std::str::from_utf8(&output.stderr)?;
42+
return Err(error.into());
43+
}
44+
45+
// eg: rustc 1.87.0-(optionally nightly) (17067e9ac 2025-05-09)
46+
// Assume the format does not change.
47+
let [major, minor, patch] = std::str::from_utf8(&output.stdout)?
48+
.split_whitespace()
49+
.nth(1)
50+
.unwrap()
51+
.split('.')
52+
.take(3)
53+
.map(|s| {
54+
s.chars()
55+
.take_while(|c| c.is_ascii_digit())
56+
.collect::<String>()
57+
.trim()
58+
.parse::<u8>()
59+
})
60+
.collect::<Result<Vec<u8>, ParseIntError>>()?
61+
.try_into()
62+
.unwrap();
63+
64+
Ok(RustcVersion::new(major, minor, patch))
65+
}

0 commit comments

Comments
 (0)