test: add cargo-fuzz fuzzing harness

adds lib target + cargo-fuzz fuzz targets exercising pure parsers; backs dynamic_analysis criteria and the Scorecard fuzzing check.
This commit is contained in:
Devid HW
2026-08-26 02:58:06 +07:00
parent d8b0b37919
commit b843146554
7 changed files with 1350 additions and 69 deletions
+4
View File
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
+1204
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "mt5-quant-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.mt5-quant]
path = ".."
[[bin]]
name = "parse_metrics"
path = "fuzz_targets/parse_metrics.rs"
test = false
doc = false
bench = false
# Enable debug/overflow assertions during fuzzing. cargo-fuzz builds fuzz
# targets in release mode by default, so we explicitly turn debug-assertions
# back on for the release profile. This backs the
# `dynamic_analysis_enable_assertions` CII criterion: any debug_assert! or
# arithmetic-overflow check inside the crate is active while libFuzzer feeds
# arbitrary input, so crashes from invariant violations are caught as bugs.
[profile.release]
debug-assertions = true
[profile.dev]
debug-assertions = true
+36
View File
@@ -0,0 +1,36 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use mt5_quant::models::Metrics;
// Fuzz target for the pure, I/O-free MT5 backtest report metrics parser.
//
// `Metrics::from_html` takes an arbitrary `&str` and extracts the numeric
// summary metrics from an MT5 HTML/XML report. It is panic-free on bad input:
// every numeric parse uses `unwrap_or`, regex patterns are compile-time
// constants (never panicking), and the function returns `Option<Metrics>`
// (None when no metrics are found). We return early on non-UTF-8 input and on
// the None case, so the fuzzer only ever exercises the parser itself.
fuzz_target!(|data: &[u8]| {
let Ok(text) = std::str::from_utf8(data) else {
// Parser only accepts UTF-8 text; arbitrary byte sequences are
// out of scope and must not cause a crash.
return;
};
if let Some(metrics) = Metrics::from_html(text) {
// Force the computed fields to be materialized so any logic error
// inside the parser surfaces during fuzzing.
let _ = (
metrics.net_profit,
metrics.profit_factor,
metrics.max_dd_pct,
metrics.sharpe_ratio,
metrics.total_trades,
metrics.recovery_factor,
metrics.win_rate_pct,
metrics.gross_profit,
metrics.gross_loss,
);
}
});
+4 -1
View File
@@ -29,5 +29,8 @@
"vulnerabilities_critical_fixed": "No known unpatched critical vulnerabilities; `cargo-deny` advisories and dependency-review run in CI and dependency updates are monitored.",
"vulnerabilities_fixed_60_days": "Dependency vulnerabilities are surfaced by `cargo-deny` (advisories) and the dependency-review action in CI, and are updated promptly.",
"warnings": "CI enforces a warning-free build via `cargo clippy --all-targets -- -D warnings` plus `cargo fmt --check`.",
"warnings_fixed": "Because Clippy uses `-D warnings`, any warning fails CI and must be fixed before a change can merge."
"warnings_fixed": "Because Clippy uses `-D warnings`, any warning fails CI and must be fixed before a change can merge.",
"dynamic_analysis": "Dynamic analysis is performed via cargo-fuzz (libFuzzer) fuzz targets in fuzz/ that exercise the report/metrics parsers with arbitrary input.",
"dynamic_analysis_enable_assertions": "The cargo-fuzz build enables debug/overflow assertions: fuzz/Cargo.toml sets `[profile.release] debug-assertions = true` (and `[profile.dev] debug-assertions = true`), so debug_assert! checks and integer-overflow traps remain active while libFuzzer feeds arbitrary input; crashes that violate internal invariants are caught as bugs.",
"dynamic_analysis_fixed": "Fuzzing findings are triaged and fixed; crashes found by the harness are treated as bugs."
}
+60
View File
@@ -0,0 +1,60 @@
pub mod analytics;
pub mod compile;
pub mod models;
pub mod optimization;
pub mod pipeline;
pub mod storage;
pub mod tools;
pub mod mcp_server;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpRequest {
pub jsonrpc: String,
pub id: Option<serde_json::Value>,
pub method: String,
pub params: Option<serde_json::Value>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpResponse {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<McpError>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, serde::Serialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, serde::Serialize)]
pub struct InitializeResult {
pub protocol_version: String,
pub capabilities: ServerCapabilities,
pub server_info: ServerInfo,
}
#[derive(Debug, serde::Serialize)]
pub struct ServerCapabilities {
pub experimental: serde_json::Value,
pub tools: ToolCapabilities,
}
#[derive(Debug, serde::Serialize)]
pub struct ToolCapabilities {
pub list_changed: bool,
}
+9 -68
View File
@@ -1,24 +1,16 @@
mod analytics;
mod compile;
mod models;
mod optimization;
mod pipeline;
mod storage;
mod tools;
mod mcp_server;
use anyhow::Result;
use clap::Parser;
use serde_json::{json, Value};
use serde_json::json;
use std::io::{stdout, Write};
use std::time::Instant;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tracing::{error, info};
use crate::models::Config;
use crate::pipeline::backtest::{BacktestParams, BacktestPipeline};
use mt5_quant::models::Config;
use mt5_quant::pipeline::backtest::{BacktestParams, BacktestPipeline};
use mt5_quant::mcp_server::{McpServer, Notification};
use mt5_quant::{McpRequest, McpResponse, McpError};
#[derive(Parser)]
#[command(name = "mt5-quant")]
@@ -45,57 +37,6 @@ struct Cli {
startup_delay: Option<u64>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpRequest {
jsonrpc: String,
id: Option<Value>,
method: String,
params: Option<Value>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpResponse {
jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<McpError>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct McpError {
code: i32,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
#[derive(Debug, serde::Serialize)]
pub struct ServerInfo {
name: String,
version: String,
}
#[derive(Debug, serde::Serialize)]
pub struct InitializeResult {
protocol_version: String,
capabilities: ServerCapabilities,
server_info: ServerInfo,
}
#[derive(Debug, serde::Serialize)]
pub struct ServerCapabilities {
experimental: Value,
tools: ToolCapabilities,
}
#[derive(Debug, serde::Serialize)]
pub struct ToolCapabilities {
list_changed: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
@@ -178,9 +119,9 @@ async fn run_test_launch(ea: Option<String>, startup_delay: Option<u64>) -> Resu
async fn run_stdio_server() -> Result<()> {
info!("Starting MT5-Quant MCP server on stdio");
let server = std::sync::Arc::new(mcp_server::McpServer::new());
let server = std::sync::Arc::new(McpServer::new());
let (notification_tx, mut notification_rx) =
tokio::sync::mpsc::unbounded_channel::<mcp_server::Notification>();
tokio::sync::mpsc::unbounded_channel::<Notification>();
server.set_notification_sender(notification_tx).await;
// Spawn notification sender task
@@ -254,7 +195,7 @@ async fn run_tcp_server(port: u16) -> Result<()> {
info!("Starting MT5-Quant MCP server on TCP port {}", port);
let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).await?;
info!("Listening on 127.0.0.1:{}", port);
info!("Running TCP server on port {}", port);
loop {
let (socket, addr) = listener.accept().await?;
@@ -272,7 +213,7 @@ async fn handle_connection(socket: tokio::net::TcpStream) -> Result<()> {
let (reader, mut writer) = socket.into_split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
let server = mcp_server::McpServer::new();
let server = McpServer::new();
loop {
line.clear();