#![no_std]
#![doc(test(
no_crate_inject,
attr(allow(
dead_code,
unused_variables,
clippy::undocumented_unsafe_blocks,
clippy::unused_trait_names,
))
))]
#![forbid(unsafe_code)]
#![warn(
// Lints that may help when writing public library.
missing_debug_implementations,
// missing_docs,
clippy::alloc_instead_of_core,
clippy::exhaustive_enums,
clippy::exhaustive_structs,
clippy::impl_trait_in_params,
clippy::std_instead_of_alloc,
clippy::std_instead_of_core,
// clippy::missing_inline_in_public_items,
)]
#![allow(clippy::missing_panics_doc)]
extern crate alloc;
extern crate std;
#[macro_use]
mod process;
mod cargo;
mod objdump;
#[cfg(windows)]
use alloc::borrow::ToOwned as _;
#[cfg(not(windows))]
use alloc::format;
use alloc::{string::String, vec, vec::Vec};
use std::{
env, eprintln,
ffi::OsString,
fs,
io::{self, IsTerminal as _, Write as _},
path::{Path, PathBuf},
process::Stdio,
};
use cargo_config2::{
TargetTripleRef,
cfg::{TargetArch, TargetEndian},
};
use self::process::ProcessBuilder;
#[derive(Debug, Default)]
struct CommonConfig {
cargo_args: Vec<String>,
rustc_args: Vec<String>,
objdump_args: Vec<String>,
att_syntax: bool,
}
#[derive(Debug)]
#[must_use]
pub struct Revision {
name: String,
target: String,
config: CommonConfig,
}
impl Revision {
pub fn new<N: Into<String>, T: Into<String>>(name: N, target: T) -> Self {
Self { name: name.into(), target: target.into(), config: CommonConfig::default() }
}
pub fn cargo_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.cargo_args.extend(args.into_iter().map(Into::into));
self
}
pub fn rustc_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.rustc_args.extend(args.into_iter().map(Into::into));
self
}
pub fn objdump_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.objdump_args.extend(args.into_iter().map(Into::into));
self
}
pub fn att_syntax(mut self) -> Self {
self.config.att_syntax = true;
self
}
}
#[derive(Debug)]
#[must_use]
pub struct Tester {
config: CommonConfig,
}
impl Tester {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self { config: CommonConfig::default() }
}
pub fn dump<M: AsRef<Path>, D: AsRef<Path>>(
&self,
manifest_dir: M,
dump_dir: D,
revisions: &[Revision],
) {
dump(self, manifest_dir.as_ref(), dump_dir.as_ref(), revisions);
}
pub fn cargo_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.cargo_args.extend(args.into_iter().map(Into::into));
self
}
pub fn rustc_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.rustc_args.extend(args.into_iter().map(Into::into));
self
}
pub fn objdump_args<I: IntoIterator<Item = S>, S: Into<String>>(mut self, args: I) -> Self {
self.config.objdump_args.extend(args.into_iter().map(Into::into));
self
}
pub fn att_syntax(mut self) -> Self {
self.config.att_syntax = true;
self
}
}
fn dump(tester: &Tester, manifest_dir: &Path, dump_dir: &Path, revisions: &[Revision]) {
let tcx = &TesterContext::new(tester, manifest_dir);
let manifest_dir = Path::new(&tcx.manifest_path).parent().unwrap();
let dump_dir = manifest_dir.join(dump_dir);
let raw_dump_dir = tcx
.metadata
.target_directory
.join("tests/asmtest/raw")
.join(dump_dir.strip_prefix(manifest_dir).unwrap());
let mut cargo_base_args = vec!["rustc", "--release", "--manifest-path", &tcx.manifest_path];
let mut cargo_base_rest_args = vec!["--", "--emit=obj"];
if !tcx.tester.config.cargo_args.is_empty() {
let mut base_args = &mut cargo_base_args;
for arg in &tcx.tester.config.cargo_args {
if arg == "--" {
base_args = &mut cargo_base_rest_args;
} else {
base_args.push(arg);
}
}
}
fs::create_dir_all(&dump_dir).unwrap();
fs::create_dir_all(&raw_dump_dir).unwrap();
for revision in revisions {
eprintln!("testing revision {}", revision.name);
let target = TargetTripleRef::from(&revision.target);
let target_name = target.triple();
let target_arch = tcx.config.cfg::<TargetArch, _>(&target).unwrap();
let is_powerpc64be = target_arch == TargetArch::powerpc64
&& tcx.config.cfg::<TargetEndian, _>(&target).unwrap() == TargetEndian::big;
let mut cx = RevisionContext {
tcx,
prefer_gnu: false, revision,
target_name,
arch_family: ArchFamily::new(&target_arch),
is_powerpc64be,
obj_path: PathBuf::new(),
verbose_function_names: vec![],
out: String::new(),
};
cargo::build(&mut cx, &cargo_base_args, &cargo_base_rest_args);
let raw_out = objdump::disassemble(&mut cx);
fs::write(raw_dump_dir.join(revision.name.clone() + ".asm"), &raw_out).unwrap();
objdump::handle_asm(&mut cx, &raw_out);
assert_diff(cx.tcx, dump_dir.join(revision.name.clone() + ".asm"), cx.out);
}
}
struct TesterContext<'a> {
tester: &'a Tester,
manifest_path: String,
config: cargo::Config,
nightly: bool,
metadata: cargo::Metadata,
user: String,
}
impl<'a> TesterContext<'a> {
fn new(tester: &'a Tester, manifest_dir: &Path) -> Self {
let manifest_path = cargo::locate_project(&manifest_dir.join("Cargo.toml")).unwrap(); let metadata = cargo::metadata(&manifest_path).unwrap();
let config = cargo::config(manifest_dir).unwrap();
let rustc_version = config.rustc_version().unwrap();
#[cfg(not(windows))]
let user = {
format!("{}:{}", rustix::process::getuid().as_raw(), rustix::process::getgid().as_raw())
};
#[cfg(windows)]
let user = "1000:1000".to_owned();
Self { tester, manifest_path, config, nightly: rustc_version.nightly, metadata, user }
}
fn docker_cmd(&self, workdir: &Path) -> ProcessBuilder {
const IMAGE: &str = "ghcr.io/taiki-e/objdump:binutils-2.46.0-llvm-22";
let mount = {
const PRE: &str = "type=bind,source=";
const MID: &str = ",target=";
const POST: &str = ",readonly";
let mut m = OsString::with_capacity(
workdir.as_os_str().len() * 2 + PRE.len() + MID.len() + POST.len(),
);
m.push(PRE);
m.push(workdir);
m.push(MID);
m.push(workdir);
m.push(POST);
m
};
cmd!(
"docker",
"run",
"--rm",
"--init",
"-i",
"--user",
&self.user,
"--mount",
mount,
"--workdir",
workdir,
"--cap-drop=all",
"--security-opt=no-new-privileges",
"--read-only",
IMAGE,
)
}
}
struct RevisionContext<'a> {
tcx: &'a TesterContext<'a>,
prefer_gnu: bool, revision: &'a Revision,
target_name: &'a str,
arch_family: ArchFamily<'a>,
is_powerpc64be: bool,
obj_path: PathBuf,
verbose_function_names: Vec<&'a str>,
out: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ArchFamily<'a> {
X86,
Hexagon,
Arm,
Avr,
CSky,
LoongArch,
Msp430,
PowerPC,
Sparc,
Mips,
M68k,
S390x,
Xtensa,
Other(&'a TargetArch),
}
impl<'a> ArchFamily<'a> {
fn new(target_arch: &'a TargetArch) -> Self {
match target_arch {
TargetArch::x86 | TargetArch::x86_64 => ArchFamily::X86,
TargetArch::hexagon => ArchFamily::Hexagon,
TargetArch::arm => ArchFamily::Arm,
TargetArch::avr => ArchFamily::Avr,
TargetArch::csky => ArchFamily::CSky,
TargetArch::loongarch32 | TargetArch::loongarch64 => ArchFamily::LoongArch,
TargetArch::sparc | TargetArch::sparc64 => ArchFamily::Sparc,
TargetArch::msp430 => ArchFamily::Msp430,
TargetArch::m68k => ArchFamily::M68k,
TargetArch::s390x => ArchFamily::S390x,
TargetArch::mips | TargetArch::mips64 | TargetArch::mips32r6 | TargetArch::mips64r6 => {
ArchFamily::Mips
}
TargetArch::powerpc | TargetArch::powerpc64 => ArchFamily::PowerPC,
TargetArch::xtensa => ArchFamily::Xtensa,
_ => ArchFamily::Other(target_arch),
}
}
}
#[track_caller]
fn assert_diff(tcx: &TesterContext<'_>, expected_path: impl AsRef<Path>, actual: impl AsRef<[u8]>) {
let actual = actual.as_ref();
let expected_path = expected_path.as_ref();
if !expected_path.is_file() {
fs::create_dir_all(expected_path.parent().unwrap()).unwrap();
fs::write(expected_path, "").unwrap();
}
let expected = fs::read(expected_path).unwrap();
if expected != actual {
if env::var_os("CI").is_some() {
let color = if env::var_os("GITHUB_ACTIONS").is_some() || io::stdout().is_terminal() {
&["-c", "color.ui=always"][..]
} else {
&[]
};
let mut child = tcx
.docker_cmd(&env::current_dir().unwrap())
.into_std()
.arg("git")
.arg("--no-pager")
.args(color)
.args(["diff", "--no-index", "--"])
.arg(expected_path)
.arg("-")
.stdin(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(actual).unwrap();
assert!(!child.wait().unwrap().success());
panic!(
"assertion failed; please run test locally and commit resulting changes, or apply the above diff as patch (e.g., `patch -p1 <<'EOF' ... EOF`)"
);
} else {
fs::write(expected_path, actual).unwrap();
}
}
}