Skip to content

Commit e50c116

Browse files
committed
ctest: Add generation, compilation, and running of tests for constants.
1 parent 1d766de commit e50c116

File tree

19 files changed

+567
-21
lines changed

19 files changed

+567
-21
lines changed

ctest-next/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ 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"] }

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/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: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,158 @@
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_else(|| env::var("TARGET").unwrap());
76+
77+
let host = self
78+
.host
79+
.clone()
80+
.unwrap_or_else(|| env::var("HOST").unwrap());
81+
82+
let mut cfg = cc::Build::new();
83+
// FIXME: Cpp not supported.
84+
cfg.file(output_file_path.with_extension("c"));
85+
cfg.host(&host);
86+
if target.contains("msvc") {
87+
cfg.flag("/W3")
88+
.flag("/Wall")
89+
.flag("/WX")
90+
// ignored warnings
91+
.flag("/wd4820") // warning about adding padding?
92+
.flag("/wd4100") // unused parameters
93+
.flag("/wd4996") // deprecated functions
94+
.flag("/wd4296") // '<' being always false
95+
.flag("/wd4255") // converting () to (void)
96+
.flag("/wd4668") // using an undefined thing in preprocessor?
97+
.flag("/wd4366") // taking ref to packed struct field might be unaligned
98+
.flag("/wd4189") // local variable initialized but not referenced
99+
.flag("/wd4710") // function not inlined
100+
.flag("/wd5045") // compiler will insert Spectre mitigation
101+
.flag("/wd4514") // unreferenced inline function removed
102+
.flag("/wd4711"); // function selected for automatic inline
103+
} else {
104+
cfg.flag("-Wall")
105+
.flag("-Wextra")
106+
.flag("-Werror")
107+
.flag("-Wno-unused-parameter")
108+
.flag("-Wno-type-limits")
109+
// allow taking address of packed struct members:
110+
.flag("-Wno-address-of-packed-member")
111+
.flag("-Wno-unknown-warning-option")
112+
.flag("-Wno-deprecated-declarations"); // allow deprecated items
113+
}
114+
115+
for p in &self.includes {
116+
cfg.include(p);
117+
}
118+
119+
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
120+
cfg.target(&target)
121+
.out_dir(output_file_path.parent().unwrap())
122+
.compile(&format!("lib{}.a", stem));
123+
124+
Ok(())
125+
}
126+
127+
/// Generate the Rust and C testing files.
128+
pub(crate) fn generate_files<P: AsRef<Path>>(
129+
&mut self,
130+
crate_path: P,
131+
output_file_path: P,
132+
) -> Result<PathBuf> {
20133
let expanded = expand(crate_path)?;
21134
let ast = syn::parse_file(&expanded)?;
22135

23136
let mut ffi_items = FfiItems::new();
24137
ffi_items.visit_file(&ast);
25138

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

ctest-next/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ mod ast;
1515
mod ffi_items;
1616
mod generator;
1717
mod macro_expansion;
18+
mod runner;
19+
mod rustc_queries;
20+
mod template;
1821
mod translator;
1922

2023
pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union};
2124
pub use generator::TestGenerator;
2225
pub use macro_expansion::expand;
26+
pub use runner::{compile_test, run_test};
27+
pub use rustc_queries::{rustc_host, rustc_version, RustcVersion};
2328

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

ctest-next/src/runner.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use crate::Result;
2+
3+
use std::env;
4+
use std::fs::{canonicalize, File};
5+
use std::io::Write;
6+
use std::path::{Path, PathBuf};
7+
use std::process::Command;
8+
9+
/// Compile the given Rust file with the given static library.
10+
/// All arguments must be valid paths.
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+
18+
let output_dir = output_dir.as_ref();
19+
let crate_path = crate_path.as_ref();
20+
let library_file = library_file.as_ref();
21+
22+
let rust_file = output_dir
23+
.join(crate_path.file_stem().unwrap())
24+
.with_extension("rs");
25+
let binary_path = output_dir.join(rust_file.file_stem().unwrap());
26+
27+
let mut file = File::create(&rust_file)?;
28+
writeln!(
29+
file,
30+
"include!(\"{}\");",
31+
canonicalize(crate_path)?.display()
32+
)?;
33+
writeln!(file, "include!(\"{}.rs\");", library_file.display())?;
34+
35+
let output = Command::new(rustc)
36+
.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("-o")
43+
.arg(&binary_path)
44+
.arg("-Aunused")
45+
.output()?;
46+
47+
if !output.status.success() {
48+
return Err(std::str::from_utf8(&output.stderr)?.into());
49+
}
50+
51+
Ok(binary_path)
52+
}
53+
54+
/// Run the compiled test binary.
55+
pub fn run_test<P: AsRef<Path>>(test_binary: P) -> Result<String> {
56+
let output = Command::new(test_binary.as_ref()).output()?;
57+
58+
if !output.status.success() {
59+
return Err(std::str::from_utf8(&output.stderr)?.into());
60+
}
61+
62+
// The template prints to stderr regardless.
63+
Ok(std::str::from_utf8(&output.stderr)?.to_string())
64+
}

ctest-next/src/rustc_queries.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 (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| s.parse::<u8>())
54+
.collect::<Result<Vec<u8>, ParseIntError>>()?
55+
.try_into()
56+
.unwrap();
57+
58+
Ok(RustcVersion::new(major, minor, patch))
59+
}
60+
61+
/// Return the host triple.
62+
pub fn rustc_host() -> Result<String> {
63+
let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
64+
65+
let output = Command::new(rustc)
66+
.arg("--version")
67+
.arg("--verbose")
68+
.output()?;
69+
70+
if !output.status.success() {
71+
let error = std::str::from_utf8(&output.stderr)?;
72+
return Err(error.into());
73+
}
74+
75+
// eg: rustc 1.87.0 (17067e9ac 2025-05-09)
76+
// binary: rustc
77+
// commit-hash: 17067e9ac6d7ecb70e50f92c1944e545188d2359
78+
// commit-date: 2025-05-09
79+
// host: x86_64-unknown-linux-gnu
80+
// release: 1.87.0
81+
// LLVM version: 20.1.1
82+
// Assume the format does not change.
83+
let host = std::str::from_utf8(&output.stdout)?
84+
.lines()
85+
.nth(4)
86+
.unwrap()
87+
.split(':')
88+
.last()
89+
.map(|s| s.trim())
90+
.unwrap();
91+
92+
Ok(host.to_string())
93+
}

0 commit comments

Comments
 (0)