#![doc = include_str!("../README.md")]
use std::env::var_os as env;
use std::ffi::OsString;
use std::{collections::BTreeMap, sync::OnceLock};
pub use std::process::{Command, Stdio};
pub use cargo_metadata::camino::Utf8PathBuf;
use cargo_metadata::Message;
#[must_use]
#[derive(Debug, PartialEq, Clone, Copy)]
#[allow(clippy::struct_excessive_bools)]
pub struct BinTestBuilder {
workspace: bool,
quiet: bool,
release: bool,
offline: bool,
all_targets: bool,
features: Option<&'static str>,
profile: Option<&'static str>,
binaries: MaybeMany<'static, &'static str>,
examples: MaybeMany<'static, &'static str>,
}
#[derive(Debug)]
pub struct BinTest {
configured_with: BinTestBuilder,
build_executables: BTreeMap<String, Utf8PathBuf>,
}
#[cfg(not(debug_assertions))]
const RELEASE_BUILD: bool = true;
#[cfg(debug_assertions)]
const RELEASE_BUILD: bool = false;
impl BinTestBuilder {
const fn new() -> BinTestBuilder {
Self {
workspace: false,
quiet: false,
release: RELEASE_BUILD,
offline: false,
all_targets: false,
features: None,
profile: None,
binaries: MaybeMany::None,
examples: MaybeMany::None,
}
}
pub const fn workspace(self) -> Self {
Self {
workspace: true,
..self
}
}
pub const fn quiet(self) -> Self {
Self {
quiet: true,
..self
}
}
pub const fn release(self) -> Self {
Self {
release: true,
..self
}
}
pub const fn debug(self) -> Self {
Self {
release: false,
..self
}
}
pub const fn offline(self) -> Self {
Self {
offline: true,
..self
}
}
pub const fn all_targets(self) -> Self {
Self {
all_targets: true,
..self
}
}
pub const fn features(self, features: &'static str) -> Self {
assert!(self.features.is_none(), "features() can only be used once");
Self {
features: Some(features),
..self
}
}
pub const fn profile(self, profile: &'static str) -> Self {
assert!(self.profile.is_none(), "profile() can only be used once");
Self {
profile: Some(profile),
..self
}
}
pub const fn binary(self, binary: &'static str) -> Self {
assert!(
self.binaries.is_none(),
"binary()/binaries() can only be used once"
);
Self {
binaries: MaybeMany::One(binary),
..self
}
}
pub const fn binaries(self, binaries: &'static [&'static str]) -> Self {
assert!(
self.binaries.is_none(),
"binary()/binaries() can only be used once"
);
Self {
binaries: MaybeMany::Many(binaries),
..self
}
}
pub const fn example(self, example: &'static str) -> Self {
assert!(
self.examples.is_none(),
"example()/examples() can only be used once"
);
Self {
examples: MaybeMany::One(example),
..self
}
}
pub const fn examples(self, examples: &'static [&'static str]) -> Self {
assert!(self.examples.is_none(), "examples() can only be used once");
Self {
examples: MaybeMany::Many(examples),
..self
}
}
#[must_use]
pub fn build(self) -> &'static BinTest {
BinTest::new_with_builder(&self)
}
}
impl BinTest {
#[must_use]
pub fn new() -> &'static Self {
Self::new_with_builder(&BinTestBuilder::new())
}
pub const fn with() -> BinTestBuilder {
BinTestBuilder::new()
}
#[must_use]
pub fn list_executables(&self) -> std::collections::btree_map::Iter<'_, String, Utf8PathBuf> {
self.build_executables.iter()
}
#[must_use]
pub fn command(&self, name: &str) -> Command {
Command::new(
self.build_executables
.get(name)
.unwrap_or_else(|| panic!("no such executable <<{name}>>")),
)
}
fn new_with_builder(builder: &BinTestBuilder) -> &'static Self {
static SINGLETON: OnceLock<BinTest> = OnceLock::new();
let singleton = SINGLETON.get_or_init(|| {
let mut cargo_build =
Command::new(env("CARGO").unwrap_or_else(|| OsString::from("cargo")));
cargo_build
.args(["build", "--message-format", "json"])
.stdout(Stdio::piped());
if builder.workspace {
cargo_build.arg("--workspace");
}
if builder.quiet {
cargo_build.arg("--quiet");
}
if builder.release {
cargo_build.arg("--release");
}
if builder.offline {
cargo_build.arg("--offline");
}
if builder.all_targets {
cargo_build.arg("--all-targets");
}
if let Some(features) = builder.features {
cargo_build.args(["--features", features]);
}
if let Some(profile) = builder.profile {
cargo_build.args(["--profile", profile]);
}
match builder.binaries {
MaybeMany::None => {}
MaybeMany::One(binary) => {
cargo_build.args(["--bin", binary]);
}
MaybeMany::Many(binaries) => {
for binary in binaries {
cargo_build.args(["--bin", binary]);
}
}
}
match builder.examples {
MaybeMany::None => {}
MaybeMany::One(example) => {
cargo_build.args(["--example", example]);
}
MaybeMany::Many(examples) => {
for example in examples {
cargo_build.args(["--bin", example]);
}
}
}
let mut cargo_result = cargo_build.spawn().expect("'cargo build' success");
let mut build_executables = BTreeMap::<String, Utf8PathBuf>::default();
let reader = std::io::BufReader::new(cargo_result.stdout.take().unwrap());
for message in cargo_metadata::Message::parse_stream(reader) {
if let Message::CompilerArtifact(artifact) = message.unwrap() {
if let Some(executable) = artifact.executable {
build_executables.insert(
String::from(executable.file_stem().expect("filename")),
executable.to_path_buf(),
);
}
}
}
BinTest {
configured_with: *builder,
build_executables,
}
});
assert_eq!(
singleton.configured_with, *builder,
"All calls to BinTest must be configured with the same values"
);
singleton
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
enum MaybeMany<'a, T> {
None,
One(T),
Many(&'a [T]),
}
impl<'a, T> MaybeMany<'a, T> {
const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
#[test]
#[should_panic(expected = "All calls to BinTest must be configured with the same values")]
fn different_config() {
let _executables1 = BinTest::new();
let _executables2 = BinTest::with().workspace().build();
}