use std::collections::BTreeMap;
use super::build_config::BuildConfiguration;
use super::language::{Language, LanguageSelection};
pub const NORMALIZATION_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnalysisMode {
Fast,
Structural,
Semantic,
}
impl AnalysisMode {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Fast => "fast",
Self::Structural => "structural",
Self::Semantic => "semantic",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildVariant {
pub mode: AnalysisMode,
pub languages: LanguageSelection,
pub headers: Option<Language>,
pub normalization_version: u32,
pub builds: Vec<BuildConfiguration>,
}
impl BuildVariant {
#[must_use]
pub const fn fast(languages: LanguageSelection, headers: Language) -> Self {
Self {
mode: AnalysisMode::Fast,
languages,
headers: Self::headers_of(languages, headers),
normalization_version: NORMALIZATION_VERSION,
builds: Vec::new(),
}
}
#[must_use]
pub const fn structural(languages: LanguageSelection, headers: Language) -> Self {
Self {
mode: AnalysisMode::Structural,
languages,
headers: Self::headers_of(languages, headers),
normalization_version: NORMALIZATION_VERSION,
builds: Vec::new(),
}
}
#[must_use]
pub fn semantic(
languages: LanguageSelection,
headers: Language,
mut builds: Vec<BuildConfiguration>,
) -> Self {
builds.sort_by_cached_key(BuildConfiguration::fingerprint);
Self {
mode: AnalysisMode::Semantic,
languages,
headers: Self::headers_of(languages, headers),
normalization_version: NORMALIZATION_VERSION,
builds,
}
}
const fn headers_of(languages: LanguageSelection, headers: Language) -> Option<Language> {
if languages.includes(Language::C) || languages.includes(Language::Cpp) {
Some(headers)
} else {
None
}
}
#[must_use]
pub fn canonical(&self) -> String {
let langs = self
.languages
.enabled()
.into_iter()
.map(Language::name)
.collect::<Vec<_>>()
.join(",");
let mut canonical = format!(
"mode={};languages={};headers={};normalization={}",
self.mode.name(),
langs,
self.headers.map_or("none", Language::name),
self.normalization_version,
);
if !self.builds.is_empty() {
canonical.push_str(";build=");
canonical.push_str(
&self
.builds
.iter()
.map(BuildConfiguration::fingerprint)
.collect::<Vec<_>>()
.join(","),
);
}
canonical
}
#[must_use]
pub fn fingerprint(&self) -> String {
blake3::hash(self.canonical().as_bytes())
.to_hex()
.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Partition<T> {
pub variant: BuildVariant,
pub units: Vec<T>,
}
#[must_use]
pub fn partition<T>(
units: impl IntoIterator<Item = (BuildVariant, T)>,
) -> BTreeMap<String, Partition<T>> {
let mut partitions: BTreeMap<String, Partition<T>> = BTreeMap::new();
for (variant, unit) in units {
partitions
.entry(variant.fingerprint())
.or_insert_with(|| Partition {
variant,
units: Vec::new(),
})
.units
.push(unit);
}
partitions
}
#[cfg(test)]
mod tests {
use super::super::build_config::{CppBuild, RustBuild};
use super::*;
#[test]
fn fast_variant_carries_mode_and_normalization_version() {
let variant = BuildVariant::fast(LanguageSelection::default(), Language::C);
assert_eq!(variant.mode, AnalysisMode::Fast);
assert_eq!(variant.normalization_version, NORMALIZATION_VERSION);
}
#[test]
fn structural_variant_differs_from_fast_only_in_mode() {
let languages = LanguageSelection::default();
let fast = BuildVariant::fast(languages, Language::C);
let structural = BuildVariant::structural(languages, Language::C);
assert_eq!(structural.mode, AnalysisMode::Structural);
assert_eq!(structural.languages, fast.languages);
assert_eq!(structural.headers, fast.headers);
assert_eq!(structural.normalization_version, fast.normalization_version);
assert_ne!(fast.fingerprint(), structural.fingerprint());
}
#[test]
fn canonical_reflects_enabled_languages_in_fixed_order() {
let variant = BuildVariant::fast(
LanguageSelection {
rust: true,
c: false,
cpp: true,
},
Language::Cpp,
);
assert_eq!(
variant.canonical(),
"mode=fast;languages=rust,cpp;headers=cpp;normalization=1"
);
}
#[test]
fn distinct_variants_have_distinct_fingerprints() {
let all = BuildVariant::fast(LanguageSelection::default(), Language::C);
let rust_only = BuildVariant::fast(
LanguageSelection {
rust: true,
c: false,
cpp: false,
},
Language::C,
);
assert_ne!(all.fingerprint(), rust_only.fingerprint());
assert_eq!(
all.fingerprint(),
BuildVariant::fast(LanguageSelection::default(), Language::C).fingerprint()
);
}
#[test]
fn reading_headers_with_a_different_grammar_is_a_different_variant() {
let languages = LanguageSelection::default();
let as_c = BuildVariant::fast(languages, Language::C);
let as_cpp = BuildVariant::fast(languages, Language::Cpp);
assert_ne!(as_c, as_cpp);
assert_ne!(as_c.fingerprint(), as_cpp.fingerprint());
}
#[test]
fn a_run_that_enumerates_no_c_records_no_header_grammar() {
let rust_only = LanguageSelection {
rust: true,
c: false,
cpp: false,
};
let with_c = BuildVariant::fast(rust_only, Language::C);
let with_cpp = BuildVariant::fast(rust_only, Language::Cpp);
assert_eq!(with_c.headers, None);
assert_eq!(with_c, with_cpp);
assert_eq!(
with_c.canonical(),
"mode=fast;languages=rust;headers=none;normalization=1"
);
}
#[test]
fn a_run_that_resolved_no_build_configuration_is_identified_as_it_always_was() {
let variant = BuildVariant::fast(LanguageSelection::default(), Language::C);
assert!(variant.builds.is_empty());
assert!(
!variant.canonical().contains("build="),
"{}",
variant.canonical()
);
}
#[test]
fn two_builds_of_one_source_tree_are_two_variants() {
let languages = LanguageSelection::default();
let narrow = BuildVariant::semantic(
languages,
Language::Cpp,
vec![BuildConfiguration::Cpp(Box::new(CppBuild {
compiler: "clang++".into(),
..CppBuild::default()
}))],
);
let wide = BuildVariant::semantic(
languages,
Language::Cpp,
vec![BuildConfiguration::Cpp(Box::new(CppBuild {
compiler: "clang++".into(),
macros: vec!["-DACCUM_WIDTH=64".into()],
..CppBuild::default()
}))],
);
assert_ne!(narrow, wide);
assert_ne!(narrow.fingerprint(), wide.fingerprint());
}
#[test]
fn a_tree_answered_by_two_compilers_is_one_variant_naming_both() {
let languages = LanguageSelection::default();
let rust = BuildConfiguration::Rust(Box::new(RustBuild {
compiler_version: "rustc 1.85.0".into(),
..RustBuild::default()
}));
let cpp = BuildConfiguration::Cpp(Box::new(CppBuild {
compiler: "clang++".into(),
..CppBuild::default()
}));
let both =
BuildVariant::semantic(languages, Language::Cpp, vec![rust.clone(), cpp.clone()]);
let rust_only = BuildVariant::semantic(languages, Language::Cpp, vec![rust]);
assert_eq!(both.builds.len(), 2);
assert_ne!(both.fingerprint(), rust_only.fingerprint());
assert!(
both.canonical().contains(&cpp.fingerprint()),
"{}",
both.canonical()
);
}
#[test]
fn the_order_the_compilers_were_reached_in_is_not_part_of_the_identity() {
let languages = LanguageSelection::default();
let rust = || BuildConfiguration::Rust(Box::default());
let cpp = || BuildConfiguration::Cpp(Box::default());
let one = BuildVariant::semantic(languages, Language::Cpp, vec![rust(), cpp()]);
let other = BuildVariant::semantic(languages, Language::Cpp, vec![cpp(), rust()]);
assert_eq!(one, other);
assert_eq!(one.fingerprint(), other.fingerprint());
}
#[test]
fn one_language_building_differently_moves_the_whole_run() {
let languages = LanguageSelection::default();
let variant = |macros: Vec<String>| {
BuildVariant::semantic(
languages,
Language::Cpp,
vec![
BuildConfiguration::Rust(Box::default()),
BuildConfiguration::Cpp(Box::new(CppBuild {
compiler: "clang++".into(),
macros,
..CppBuild::default()
})),
],
)
};
assert_ne!(
variant(Vec::new()).fingerprint(),
variant(vec!["-DACCUM_WIDTH=64".into()]).fingerprint()
);
}
#[test]
fn resolving_one_configuration_identifies_a_run_as_it_always_did() {
let build = BuildConfiguration::Rust(Box::default());
let variant = BuildVariant::semantic(
LanguageSelection::default(),
Language::Cpp,
vec![build.clone()],
);
assert!(
variant
.canonical()
.ends_with(&format!(";build={}", build.fingerprint())),
"{}",
variant.canonical()
);
}
#[test]
fn a_rust_variant_and_a_cpp_variant_are_never_the_same_variant() {
let languages = LanguageSelection::default();
let rust = BuildVariant::semantic(
languages,
Language::Cpp,
vec![BuildConfiguration::Rust(Box::default())],
);
let cpp = BuildVariant::semantic(
languages,
Language::Cpp,
vec![BuildConfiguration::Cpp(Box::default())],
);
assert_ne!(rust.fingerprint(), cpp.fingerprint());
}
#[test]
fn units_are_grouped_by_the_variant_they_were_analysed_under() {
let languages = LanguageSelection::default();
let variant = |macros: Vec<String>| {
BuildVariant::semantic(
languages,
Language::Cpp,
vec![BuildConfiguration::Cpp(Box::new(CppBuild {
compiler: "clang++".into(),
macros,
..CppBuild::default()
}))],
)
};
let narrow = variant(Vec::new());
let wide = variant(vec!["-DACCUM_WIDTH=64".into()]);
let partitions = partition([
(narrow.clone(), "narrow.cpp"),
(wide.clone(), "wide.cpp"),
(narrow.clone(), "also-narrow.cpp"),
]);
assert_eq!(partitions.len(), 2);
assert_eq!(
partitions[&narrow.fingerprint()].units,
vec!["narrow.cpp", "also-narrow.cpp"]
);
assert_eq!(partitions[&wide.fingerprint()].units, vec!["wide.cpp"]);
}
#[test]
fn the_grouping_does_not_depend_on_the_order_units_arrive_in() {
let languages = LanguageSelection::default();
let fast = BuildVariant::fast(languages, Language::C);
let structural = BuildVariant::structural(languages, Language::C);
let forwards = partition([(fast.clone(), 1), (structural.clone(), 2)]);
let backwards = partition([(structural, 2), (fast, 1)]);
assert_eq!(
forwards.keys().collect::<Vec<_>>(),
backwards.keys().collect::<Vec<_>>()
);
}
}