#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapturedError {
pub command: String,
pub args: Vec<String>,
pub error_message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub backtrace: Option<String>,
pub timestamp: String,
pub version: String,
pub os: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
}
impl CapturedError {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(command: &str, args: &[String], error_message: &str) -> Self {
Self {
command: command.to_string(),
args: args.to_vec(),
error_message: error_message.to_string(),
backtrace: None,
timestamp: chrono::Utc::now().to_rfc3339(),
version: env!("CARGO_PKG_VERSION").to_string(),
os: std::env::consts::OS.to_string(),
project_path: std::env::current_dir()
.ok()
.map(|p| p.display().to_string()),
exit_code: None,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_backtrace(mut self, backtrace: &str) -> Self {
self.backtrace = Some(backtrace.to_string());
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_exit_code(mut self, code: i32) -> Self {
self.exit_code = Some(code);
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn redact_paths(&mut self) {
if let Ok(home) = std::env::var("HOME") {
self.error_message = self.error_message.replace(&home, "~");
if let Some(ref mut bt) = self.backtrace {
*bt = bt.replace(&home, "~");
}
if let Some(ref mut path) = self.project_path {
*path = path.replace(&home, "~");
}
}
if let Ok(user) = std::env::var("USER") {
self.error_message = self.error_message.replace(&user, "<user>");
}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn get_error_capture_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let pmat_dir = home.join(".pmat");
if !pmat_dir.exists() {
std::fs::create_dir_all(&pmat_dir).context("Failed to create ~/.pmat directory")?;
}
Ok(pmat_dir.join("last_error.json"))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn save_error(error: &CapturedError) -> Result<()> {
let path = get_error_capture_path()?;
let json = serde_json::to_string_pretty(error).context("Failed to serialize error")?;
std::fs::write(&path, json).context("Failed to write error file")?;
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn load_error() -> Result<Option<CapturedError>> {
let path = get_error_capture_path()?;
if !path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path).context("Failed to read error file")?;
let error: CapturedError =
serde_json::from_str(&content).context("Failed to parse error file")?;
Ok(Some(error))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn clear_error() -> Result<()> {
let path = get_error_capture_path()?;
if path.exists() {
std::fs::remove_file(&path).context("Failed to remove error file")?;
}
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn generate_issue_markdown(error: &CapturedError, title: Option<&str>) -> String {
let default_title = format!(
"Bug: {} fails with error",
error
.command
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join(" ")
);
let title = title.unwrap_or(&default_title);
let mut md = String::new();
md.push_str("## Summary\n\n");
md.push_str(&format!(
"Command `{}` failed with an error.\n\n",
error.command
));
md.push_str("## Environment\n\n");
md.push_str(&format!("- **PMAT Version**: {}\n", error.version));
md.push_str(&format!("- **OS**: {}\n", error.os));
md.push_str(&format!("- **Timestamp**: {}\n", error.timestamp));
if let Some(ref path) = error.project_path {
md.push_str(&format!("- **Project Path**: `{}`\n", path));
}
md.push('\n');
md.push_str("## Command Executed\n\n");
md.push_str("```bash\n");
if error.args.is_empty() {
md.push_str(&error.command);
} else {
md.push_str(&format!("{} {}", error.command, error.args.join(" ")));
}
md.push_str("\n```\n\n");
md.push_str("## Error Output\n\n");
md.push_str("```\n");
md.push_str(&error.error_message);
md.push_str("\n```\n\n");
if let Some(ref backtrace) = error.backtrace {
md.push_str("<details>\n<summary>Backtrace</summary>\n\n");
md.push_str("```\n");
md.push_str(backtrace);
md.push_str("\n```\n\n");
md.push_str("</details>\n\n");
}
if let Some(code) = error.exit_code {
md.push_str(&format!("**Exit Code**: {}\n\n", code));
}
md.push_str("## Steps to Reproduce\n\n");
md.push_str("1. Navigate to project directory\n");
md.push_str(&format!(
"2. Run: `{}`\n",
if error.args.is_empty() {
error.command.clone()
} else {
format!("{} {}", error.command, error.args.join(" "))
}
));
md.push_str("3. Observe error\n\n");
md.push_str("## Expected Behavior\n\n");
md.push_str("Command should complete successfully without errors.\n\n");
md.push_str("---\n");
md.push_str("*Generated automatically by `pmat bug-report`*\n");
format!("TITLE: {}\n---\n{}", title, md)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_captured_error_new() {
let error = CapturedError::new(
"pmat",
&["work".to_string(), "status".to_string()],
"Failed to load roadmap",
);
assert_eq!(error.command, "pmat");
assert_eq!(error.args, vec!["work", "status"]);
assert_eq!(error.error_message, "Failed to load roadmap");
assert!(error.backtrace.is_none());
assert!(!error.version.is_empty());
assert!(!error.os.is_empty());
}
#[test]
fn test_captured_error_with_backtrace() {
let error = CapturedError::new("pmat", &[], "error").with_backtrace("backtrace here");
assert_eq!(error.backtrace, Some("backtrace here".to_string()));
}
#[test]
fn test_captured_error_with_exit_code() {
let error = CapturedError::new("pmat", &[], "error").with_exit_code(1);
assert_eq!(error.exit_code, Some(1));
}
#[test]
#[ignore = "Environment variable manipulation unsafe in parallel tests"]
fn test_redact_paths() {
let mut error = CapturedError::new("pmat", &[], "/home/testuser/project/error");
std::env::set_var("HOME", "/home/testuser");
error.redact_paths();
assert!(error.error_message.contains("~"));
assert!(!error.error_message.contains("/home/testuser"));
}
#[test]
fn test_generate_issue_markdown() {
let error = CapturedError::new(
"pmat",
&["work".to_string(), "status".to_string()],
"Failed",
);
let md = generate_issue_markdown(&error, Some("Test Title"));
assert!(md.contains("TITLE: Test Title"));
assert!(md.contains("## Summary"));
assert!(md.contains("## Environment"));
assert!(md.contains("## Command Executed"));
assert!(md.contains("pmat work status"));
assert!(md.contains("## Error Output"));
}
#[test]
fn test_generate_issue_markdown_default_title() {
let error = CapturedError::new("pmat work", &[], "Failed");
let md = generate_issue_markdown(&error, None);
assert!(md.contains("TITLE: Bug: pmat work fails with error"));
}
#[test]
#[ignore = "Requires HOME directory to be set"]
fn test_error_capture_path() {
let path = get_error_capture_path();
assert!(path.is_ok());
let path = path.unwrap();
assert!(path.to_string_lossy().contains(".pmat"));
assert!(path.to_string_lossy().contains("last_error.json"));
}
#[test]
fn test_generate_issue_markdown_with_backtrace() {
let error = CapturedError::new("pmat", &["cmd".to_string()], "Error message")
.with_backtrace("stack frame 1\nstack frame 2");
let md = generate_issue_markdown(&error, None);
assert!(md.contains("<details>"));
assert!(md.contains("<summary>Backtrace</summary>"));
assert!(md.contains("stack frame 1"));
assert!(md.contains("stack frame 2"));
assert!(md.contains("</details>"));
}
#[test]
fn test_generate_issue_markdown_with_exit_code() {
let error = CapturedError::new("pmat", &[], "Error").with_exit_code(127);
let md = generate_issue_markdown(&error, None);
assert!(md.contains("**Exit Code**: 127"));
}
#[test]
fn test_generate_issue_markdown_no_args() {
let error = CapturedError::new("pmat", &[], "Error");
let md = generate_issue_markdown(&error, None);
assert!(md.contains("```bash\npmat\n```"));
}
#[test]
fn test_captured_error_serialization() {
let error = CapturedError::new("pmat", &["arg1".to_string()], "Error message")
.with_backtrace("trace")
.with_exit_code(42);
let json = serde_json::to_string(&error).unwrap();
let deserialized: CapturedError = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.command, "pmat");
assert_eq!(deserialized.args, vec!["arg1"]);
assert_eq!(deserialized.error_message, "Error message");
assert_eq!(deserialized.backtrace, Some("trace".to_string()));
assert_eq!(deserialized.exit_code, Some(42));
}
#[test]
fn test_captured_error_debug() {
let error = CapturedError::new("pmat", &[], "Error");
let debug_str = format!("{:?}", error);
assert!(debug_str.contains("CapturedError"));
assert!(debug_str.contains("pmat"));
}
#[test]
fn test_captured_error_clone() {
let error = CapturedError::new("pmat", &["arg".to_string()], "Error")
.with_backtrace("trace")
.with_exit_code(1);
let cloned = error.clone();
assert_eq!(error.command, cloned.command);
assert_eq!(error.args, cloned.args);
assert_eq!(error.backtrace, cloned.backtrace);
assert_eq!(error.exit_code, cloned.exit_code);
}
#[test]
#[ignore = "Environment variable manipulation unsafe in parallel tests"]
fn test_redact_paths_with_user() {
let mut error = CapturedError::new(
"pmat",
&[],
"Path /home/johndoe/project has error for johndoe",
);
std::env::set_var("HOME", "/home/johndoe");
std::env::set_var("USER", "johndoe");
error.redact_paths();
assert!(error.error_message.contains("~"));
assert!(error.error_message.contains("<user>"));
assert!(!error.error_message.contains("johndoe"));
}
#[test]
#[ignore = "Environment variable manipulation unsafe in parallel tests"]
fn test_redact_paths_with_backtrace() {
let mut error =
CapturedError::new("pmat", &[], "Error").with_backtrace("/home/testuser/file.rs:42");
std::env::set_var("HOME", "/home/testuser");
error.redact_paths();
assert!(error.backtrace.as_ref().unwrap().contains("~"));
assert!(!error.backtrace.as_ref().unwrap().contains("testuser"));
}
#[test]
#[ignore = "Environment variable manipulation unsafe in parallel tests"]
fn test_redact_paths_with_project_path() {
let mut error = CapturedError::new("pmat", &[], "Error");
error.project_path = Some("/home/testuser/projects/myproject".to_string());
std::env::set_var("HOME", "/home/testuser");
error.redact_paths();
assert!(error.project_path.as_ref().unwrap().contains("~"));
assert!(!error.project_path.as_ref().unwrap().contains("testuser"));
}
#[test]
fn test_generate_issue_markdown_with_project_path() {
let mut error = CapturedError::new("pmat", &[], "Error");
error.project_path = Some("/projects/myproject".to_string());
let md = generate_issue_markdown(&error, None);
assert!(md.contains("**Project Path**: `/projects/myproject`"));
}
}