#![cfg_attr(coverage_nightly, coverage(off))]
use super::models::PopperCategoryScore;
use std::path::{Path, PathBuf};
pub type PopperScorerResult<T> = Result<T, PopperScorerError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum PopperScorerError {
#[error("Failed to read file: {0}")]
FileReadError(String),
#[error("Failed to parse content: {0}")]
ParseError(String),
#[error("Tool not found: {0}")]
ToolNotFound(String),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Command execution failed: {0}")]
CommandError(String),
}
impl From<std::io::Error> for PopperScorerError {
fn from(e: std::io::Error) -> Self {
PopperScorerError::IoError(e.to_string())
}
}
pub mod workspace {
use super::*;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
pub is_workspace: bool,
pub members: Vec<PathBuf>,
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_workspace(project_path: &Path) -> WorkspaceInfo {
let cargo_path = project_path.join("Cargo.toml");
if !cargo_path.exists() {
return WorkspaceInfo {
is_workspace: false,
members: vec![project_path.to_path_buf()],
};
}
if let Ok(content) = std::fs::read_to_string(&cargo_path) {
if content.contains("[workspace]") {
let members = parse_workspace_members(&content, project_path);
if !members.is_empty() {
return WorkspaceInfo {
is_workspace: true,
members,
};
}
}
}
WorkspaceInfo {
is_workspace: false,
members: vec![project_path.to_path_buf()],
}
}
fn parse_workspace_members(content: &str, project_path: &Path) -> Vec<PathBuf> {
let mut members = Vec::new();
let members_regex = Regex::new(r#"members\s*=\s*\[([\s\S]*?)\]"#).ok();
if let Some(re) = members_regex {
if let Some(captures) = re.captures(content) {
if let Some(members_str) = captures.get(1) {
let quote_regex = Regex::new(r#""([^"]+)""#).ok();
if let Some(qre) = quote_regex {
for cap in qre.captures_iter(members_str.as_str()) {
if let Some(member) = cap.get(1) {
let member_path = project_path.join(member.as_str());
if member_path.exists() {
members.push(member_path);
}
}
}
}
}
}
}
members
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn get_code_paths(project_path: &Path) -> Vec<PathBuf> {
let info = detect_workspace(project_path);
info.members
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn any_member_has_dir(project_path: &Path, dir_name: &str) -> bool {
for member_path in get_code_paths(project_path) {
if member_path.join(dir_name).exists() {
return true;
}
}
false
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn any_member_has_file(project_path: &Path, file_name: &str) -> bool {
for member_path in get_code_paths(project_path) {
if member_path.join(file_name).exists() {
return true;
}
}
false
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn read_member_dir_content(project_path: &Path, dir_name: &str, extension: &str) -> String {
let mut content = String::new();
for member_path in get_code_paths(project_path) {
let dir_path = member_path.join(dir_name);
if dir_path.exists() {
read_dir_recursive(&dir_path, extension, &mut content);
}
}
content
}
fn read_dir_recursive(dir: &Path, extension: &str, content: &mut String) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
read_dir_recursive(&path, extension, content);
} else if path.extension().is_some_and(|e| e == extension) {
if let Ok(file_content) = std::fs::read_to_string(&path) {
content.push_str(&file_content);
content.push('\n');
}
}
}
}
}
}
pub trait PopperScorer: Send + Sync {
fn name(&self) -> &str;
fn category_id(&self) -> char;
fn max_points(&self) -> f64;
fn is_gateway(&self) -> bool {
self.category_id() == 'A'
}
fn score(&self, project_path: &Path) -> PopperScorerResult<PopperCategoryScore>;
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
struct MockScorer {
name: String,
category: char,
max: f64,
}
impl PopperScorer for MockScorer {
fn name(&self) -> &str {
&self.name
}
fn category_id(&self) -> char {
self.category
}
fn max_points(&self) -> f64 {
self.max
}
fn score(&self, _project_path: &Path) -> PopperScorerResult<PopperCategoryScore> {
Ok(PopperCategoryScore::new(&self.name, 10.0, self.max))
}
}
#[test]
fn test_gateway_detection() {
let gateway = MockScorer {
name: "Falsifiability".to_string(),
category: 'A',
max: 25.0,
};
assert!(gateway.is_gateway());
let non_gateway = MockScorer {
name: "Reproducibility".to_string(),
category: 'B',
max: 25.0,
};
assert!(!non_gateway.is_gateway());
}
#[test]
fn test_scorer_interface() {
let scorer = MockScorer {
name: "Test".to_string(),
category: 'T',
max: 10.0,
};
assert_eq!(scorer.name(), "Test");
assert_eq!(scorer.category_id(), 'T');
assert_eq!(scorer.max_points(), 10.0);
}
}