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:
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
Generated
+1204
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user