use std::path::PathBuf;
use std::sync::Arc;
use crate::{
analysis::CallGraph,
metadata::token::Token,
project::ProjectLoader,
test::{
analysis::{
templates::{AnalysisTestCase, ANALYSIS_TEST_CASES, ANALYSIS_TEST_SOURCE},
verification::{
build_analysis, verify_callgraph, verify_cfg, verify_dataflow, verify_ssa,
VerificationError,
},
},
mono::{compilation::compile_debug, Architecture, TestCapabilities},
},
CilObject, Result,
};
fn mono_framework_path() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/mono_4.8")
}
#[derive(Debug)]
pub struct AnalysisTestResult {
pub test_name: String,
pub passed: bool,
pub errors: Vec<VerificationError>,
pub run_error: Option<String>,
}
impl AnalysisTestResult {
#[must_use]
pub fn success(name: impl Into<String>) -> Self {
Self {
test_name: name.into(),
passed: true,
errors: Vec::new(),
run_error: None,
}
}
#[must_use]
pub fn verification_failed(name: impl Into<String>, errors: Vec<VerificationError>) -> Self {
Self {
test_name: name.into(),
passed: false,
errors,
run_error: None,
}
}
#[must_use]
pub fn run_failed(name: impl Into<String>, error: impl Into<String>) -> Self {
Self {
test_name: name.into(),
passed: false,
errors: Vec::new(),
run_error: Some(error.into()),
}
}
#[must_use]
pub fn error_message(&self) -> String {
if let Some(ref run_error) = self.run_error {
return run_error.clone();
}
if self.errors.is_empty() {
return String::new();
}
self.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("; ")
}
}
pub struct AnalysisTestRunner {
capabilities: TestCapabilities,
temp_dir: tempfile::TempDir,
assembly_path: Option<PathBuf>,
assembly: Option<Arc<CilObject>>,
callgraph: Option<CallGraph>,
}
impl AnalysisTestRunner {
pub fn new() -> Result<Self> {
let capabilities = TestCapabilities::detect();
if !capabilities.can_test() {
return Err(crate::Error::Other(
"No C# compiler available for analysis tests".to_string(),
));
}
let temp_dir = tempfile::TempDir::new()
.map_err(|e| crate::Error::Other(format!("Failed to create temp dir: {}", e)))?;
Ok(Self {
capabilities,
temp_dir,
assembly_path: None,
assembly: None,
callgraph: None,
})
}
pub fn compile(&mut self) -> Result<PathBuf> {
if let Some(ref path) = self.assembly_path {
return Ok(path.clone());
}
let arch = self
.capabilities
.supported_architectures
.first()
.cloned()
.unwrap_or(Architecture::ANYCPU);
let result = compile_debug(
&self.capabilities,
ANALYSIS_TEST_SOURCE,
self.temp_dir.path(),
"AnalysisTests",
&arch,
)?;
if !result.is_success() {
return Err(crate::Error::Other(format!(
"Compilation failed: {}",
result.error.unwrap_or_else(|| "Unknown error".to_string())
)));
}
let path = result.assembly_path().to_path_buf();
self.assembly_path = Some(path.clone());
Ok(path)
}
pub fn load(&mut self) -> Result<Arc<CilObject>> {
if let Some(ref assembly) = self.assembly {
return Ok(assembly.clone());
}
let path = self.compile()?;
let project_result = ProjectLoader::new()
.primary_file(&path)?
.with_search_path(mono_framework_path())?
.auto_discover(true)
.build()?;
let assembly = project_result
.project
.get_primary()
.ok_or_else(|| crate::Error::Other("Failed to get primary assembly".to_string()))?;
self.assembly = Some(assembly.clone());
Ok(assembly)
}
pub fn build_callgraph(&mut self) -> Result<&CallGraph> {
if let Some(ref cg) = self.callgraph {
return Ok(cg);
}
let assembly = self.load()?;
let callgraph = CallGraph::build(&assembly)?;
self.callgraph = Some(callgraph);
Ok(self.callgraph.as_ref().expect("just inserted"))
}
pub fn find_method(&self, class_name: &str, method_name: &str) -> Option<Token> {
let assembly = self.assembly.as_ref()?;
for type_info in assembly.types().all_types() {
if type_info.name == class_name {
for (_, method_ref) in type_info.methods.iter() {
if let Some(method) = method_ref.upgrade() {
if method.name == method_name {
return Some(method.token);
}
}
}
}
}
None
}
pub fn run_test(&mut self, test_case: &AnalysisTestCase) -> AnalysisTestResult {
let assembly = match self.load() {
Ok(a) => a,
Err(e) => {
return AnalysisTestResult::run_failed(
test_case.name,
format!("Failed to load assembly: {}", e),
)
}
};
let token = match self.find_method(test_case.class_name, test_case.method_name) {
Some(t) => t,
None => {
return AnalysisTestResult::run_failed(
test_case.name,
format!(
"Method not found: {}::{}",
test_case.class_name, test_case.method_name
),
)
}
};
let method = match assembly.method(&token) {
Some(method) => method,
None => {
return AnalysisTestResult::run_failed(
test_case.name,
format!("Method with token {:?} not in method table", token),
)
}
};
let (cfg, ssa) = match build_analysis(&method, Some(&assembly)) {
Ok(result) => result,
Err(e) => return AnalysisTestResult::run_failed(test_case.name, e),
};
let mut all_errors = Vec::new();
let cfg_errors = verify_cfg(&cfg, &test_case.cfg);
all_errors.extend(cfg_errors);
let ssa_errors = verify_ssa(&ssa, &cfg, &test_case.ssa);
all_errors.extend(ssa_errors);
if let Some(ref cg_expectation) = test_case.callgraph {
match self.build_callgraph() {
Ok(callgraph) => {
let cg_errors = verify_callgraph(callgraph, &method, cg_expectation);
all_errors.extend(cg_errors);
}
Err(e) => {
all_errors.push(VerificationError::new(
"CallGraph",
"build",
"success",
format!("error: {}", e),
));
}
}
}
if let Some(ref df_expectation) = test_case.dataflow {
let df_errors = verify_dataflow(&ssa, &cfg, df_expectation);
all_errors.extend(df_errors);
}
if all_errors.is_empty() {
AnalysisTestResult::success(test_case.name)
} else {
AnalysisTestResult::verification_failed(test_case.name, all_errors)
}
}
pub fn run_all_tests(&mut self) -> Result<Vec<AnalysisTestResult>> {
self.load()?;
let mut results = Vec::with_capacity(ANALYSIS_TEST_CASES.len());
for test_case in ANALYSIS_TEST_CASES {
let result = self.run_test(test_case);
results.push(result);
}
Ok(results)
}
pub fn run_tests_matching(&mut self, prefix: &str) -> Result<Vec<AnalysisTestResult>> {
self.load()?;
let mut results = Vec::new();
for test_case in ANALYSIS_TEST_CASES {
if test_case.name.starts_with(prefix) {
let result = self.run_test(test_case);
results.push(result);
}
}
Ok(results)
}
#[must_use]
pub fn capabilities(&self) -> &TestCapabilities {
&self.capabilities
}
#[must_use]
pub fn test_count() -> usize {
ANALYSIS_TEST_CASES.len()
}
#[must_use]
pub fn test_cases() -> &'static [AnalysisTestCase] {
ANALYSIS_TEST_CASES
}
}