Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl EthUIApp {
ethui_db::commands::db_get_native_balance,
ethui_db::commands::db_get_erc721_tokens,
ethui_forge::commands::fetch_forge_abis,
ethui_forge::commands::get_abi_for_code,
ethui_ws::commands::ws_peers_by_domain,
ethui_ws::commands::ws_peer_count,
ethui_wallets::commands::wallets_get_all,
Expand Down
113 changes: 105 additions & 8 deletions crates/forge-traces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,34 @@ impl ForgeTestRunner {

let output = cmd.output()?;

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

// Log the status but continue processing traces regardless of test success/failure
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(eyre!("Forge test failed: {}", stderr));
tracing::warn!(
"Forge tests failed (exit code: {:?}), but continuing to process traces. stderr: {}",
output.status.code(),
stderr
);
} else {
tracing::info!("Forge tests completed successfully");
}

let stdout = String::from_utf8_lossy(&output.stdout);
let traces = match self.parse_forge_output(&stdout) {
Ok(traces) => traces,
Err(e) => {
tracing::error!("Failed to parse forge output: {e}");
tracing::debug!("Raw forge stdout: {}", stdout);
tracing::debug!("Raw forge stderr: {}", stderr);

let traces = self.parse_forge_output(&stdout)?;
// Return empty traces instead of failing completely
Vec::new()
}
};

if traces.is_empty() {
tracing::info!("No traces to send (either no tests ran or parsing failed)");
return Ok(());
}

Expand All @@ -58,7 +76,24 @@ impl ForgeTestRunner {
fn parse_forge_output(&self, output: &str) -> Result<Vec<ForgeTrace>> {
let mut traces = Vec::new();

let json_value: serde_json::Value = serde_json::from_str(output)?;
// Handle empty or non-JSON output
if output.trim().is_empty() {
tracing::warn!("Forge output is empty");
return Ok(traces);
}

let json_value: serde_json::Value = serde_json::from_str(output).map_err(|e| {
tracing::error!("Raw forge output that failed to parse: {}", output);
eyre!("Failed to parse forge JSON output: {e}")
})?;

// Log the top-level structure to help debug different output formats
tracing::debug!(
"Forge JSON output keys: {:?}",
json_value
.as_object()
.map(|obj| obj.keys().collect::<Vec<_>>())
);

if let Some(test_contracts) = json_value.as_object() {
for (contract_path, contract_results) in test_contracts {
Expand All @@ -77,8 +112,24 @@ impl ForgeTestRunner {
let success = test_result
.get("status")
.and_then(|v| v.as_str())
.map(|s| s == "Success")
.unwrap_or(false);
.map(|s| match s {
"Success" => true,
"Failure" | "Revert" | "Panic" | "OutOfGas" | "Setup" => false,
_ => {
tracing::warn!(
"Unknown test status: {}, treating as failure",
s
);
false
}
})
.unwrap_or_else(|| {
tracing::warn!(
"Missing test status for {}, treating as failure",
test_name
);
false
});

let gas_used = test_result.get("kind").and_then(|kind| {
if let Some(unit) = kind.get("Unit") {
Expand All @@ -90,11 +141,49 @@ impl ForgeTestRunner {
}
});

// Try to get traces from multiple possible locations
let trace_data = test_result
.get("traces")
.cloned()
.or_else(|| {
// For failed tests, traces might be under different keys
test_result
.get("execution")
.and_then(|exec| exec.get("traces"))
.cloned()
})
.or_else(|| {
// Check if traces are nested under the test kind
test_result
.get("kind")
.and_then(|kind| {
kind.get("Unit")
.or_else(|| kind.get("Fuzz"))
.and_then(|unit| unit.get("traces"))
})
.cloned()
})
.unwrap_or_else(|| json!([]));

let has_traces = !trace_data.as_array().is_none_or(|arr| arr.is_empty());

tracing::debug!(
"Processing test: {} in contract: {}, success: {}, has_traces: {}",
test_name,
contract_name,
success,
has_traces
);

// If we're not finding traces for a failed test, log the full test result structure
if !success && !has_traces {
tracing::debug!(
"Failed test {} has no traces. Full test result: {}",
test_name,
serde_json::to_string_pretty(test_result).unwrap_or_default()
);
}

traces.push(ForgeTrace {
test_name: test_name.clone(),
contract_name: contract_name.clone(),
Expand All @@ -107,7 +196,15 @@ impl ForgeTestRunner {
}
}

info!("Parsed {} traces from forge output", traces.len());
let successful_tests = traces.iter().filter(|t| t.success).count();
let failed_tests = traces.iter().filter(|t| !t.success).count();

info!(
"Parsed {} traces from forge output ({} successful, {} failed)",
traces.len(),
successful_tests,
failed_tests
);
Ok(traces)
}

Expand Down
19 changes: 17 additions & 2 deletions crates/forge/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use color_eyre::eyre::ContextCompat as _;
use ethui_types::TauriResult;
use ethui_types::{prelude::*, TauriResult};
use kameo::actor::ActorRef;

use crate::{
abi::ForgeAbi,
actor::{FetchAbis, Worker},
actor::{FetchAbis, GetAbiFor, Worker},
};

#[tauri::command]
Expand All @@ -17,3 +17,18 @@ pub async fn fetch_forge_abis() -> TauriResult<Vec<ForgeAbi>> {

Ok(inner().await?)
}

#[tauri::command]
pub async fn get_abi_for_code(bytecode: String) -> TauriResult<Option<ForgeAbi>> {
async fn inner(bytecode: String) -> color_eyre::Result<Option<ForgeAbi>> {
let actor =
ActorRef::<Worker>::lookup("forge")?.with_context(|| "Actor not found".to_string())?;

// Convert hex string to bytes using Bytes::from_str
let bytes = Bytes::from_str(&bytecode)?;

Ok(actor.ask(GetAbiFor(bytes)).await?)
}

Ok(inner(bytecode).await?)
}
1 change: 1 addition & 0 deletions crates/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ethui-connections.workspace = true
ethui-simulator.workspace = true
ethui-sync.workspace = true
ethui-forge.workspace = true
ethui-broadcast.workspace = true

alloy.workspace = true
tauri.workspace = true
Expand Down
11 changes: 10 additions & 1 deletion crates/rpc/src/methods/ethui/forge_test_traces.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ethui_types::ui_events::UINotify;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::info;
Expand All @@ -17,7 +18,15 @@ impl ForgeTestTraces {
pub async fn run(self) -> Result<serde_json::Value> {
info!("Processing {} forge test traces", self.traces.len());

// TODO: Forward traces to UI or store for further processing
// Send traces to UI via broadcast system
let trace_data: Vec<serde_json::Value> = self
.traces
.iter()
.map(|trace| serde_json::to_value(trace).unwrap_or_default())
.collect();

ethui_broadcast::ui_notify(UINotify::ForgeTestTracesUpdated(trace_data)).await;
info!("Sent forge test traces notification to UI");

Ok(json!({
"status": "processed",
Expand Down
2 changes: 2 additions & 0 deletions crates/types/src/ui_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub enum UINotify {
BalancesUpdated,
ContractsUpdated,
SettingsChanged,
ForgeTestTracesUpdated(Vec<serde_json::Value>),
}

impl UINotify {
Expand All @@ -47,6 +48,7 @@ impl UINotify {
Self::BalancesUpdated => "balances-updated",
Self::ContractsUpdated => "contracts-updated",
Self::SettingsChanged => "settings-changed",
Self::ForgeTestTracesUpdated(_) => "forge-test-traces-updated",
}
}
}
6 changes: 6 additions & 0 deletions gui/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ReceiptText,
Terminal,
Wifi,
Zap,
} from "lucide-react";
import { useInvoke } from "#/hooks/useInvoke";
import { useSettings } from "#/store/useSettings";
Expand Down Expand Up @@ -214,6 +215,11 @@ const items = [
url: "/home/connections",
icon: <Wifi />,
},
{
title: "Forge Traces",
url: "/home/forge-traces",
icon: <Zap />,
},
];

const defaultSettingsItems = [
Expand Down
3 changes: 2 additions & 1 deletion gui/src/hooks/useEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ type Event =
| "peers-updated"
| "settings-changed"
| "contracts-updated"
| "txs-updated";
| "txs-updated"
| "forge-test-traces-updated";

export function useEventListener(event: Event, callback: () => unknown) {
const view = getCurrentWebviewWindow();
Expand Down
21 changes: 21 additions & 0 deletions gui/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Route as HomeLRouteImport } from './routes/home/_l'
import { Route as DialogLRouteImport } from './routes/dialog/_l'
import { Route as HomeLTransactionsRouteImport } from './routes/home/_l/transactions'
import { Route as HomeLOnboardingRouteImport } from './routes/home/_l/onboarding'
import { Route as HomeLForgeTracesRouteImport } from './routes/home/_l/forge-traces'
import { Route as HomeLConnectionsRouteImport } from './routes/home/_l/connections'
import { Route as HomeLAccountRouteImport } from './routes/home/_l/account'
import { Route as HomeLTransferLRouteImport } from './routes/home/_l/transfer/_l'
Expand Down Expand Up @@ -102,6 +103,11 @@ const HomeLOnboardingRoute = HomeLOnboardingRouteImport.update({
path: '/onboarding',
getParentRoute: () => HomeLRoute,
} as any)
const HomeLForgeTracesRoute = HomeLForgeTracesRouteImport.update({
id: '/forge-traces',
path: '/forge-traces',
getParentRoute: () => HomeLRoute,
} as any)
const HomeLConnectionsRoute = HomeLConnectionsRouteImport.update({
id: '/connections',
path: '/connections',
Expand Down Expand Up @@ -275,6 +281,7 @@ export interface FileRoutesByFullPath {
'/home': typeof HomeLRouteWithChildren
'/home/account': typeof HomeLAccountRoute
'/home/connections': typeof HomeLConnectionsRoute
'/home/forge-traces': typeof HomeLForgeTracesRoute
'/home/onboarding': typeof HomeLOnboardingRoute
'/home/transactions': typeof HomeLTransactionsRoute
'/dialog/chain-add/$id': typeof DialogLChainAddIdRoute
Expand Down Expand Up @@ -312,6 +319,7 @@ export interface FileRoutesByTo {
'/home': typeof HomeLRouteWithChildren
'/home/account': typeof HomeLAccountRoute
'/home/connections': typeof HomeLConnectionsRoute
'/home/forge-traces': typeof HomeLForgeTracesRoute
'/home/onboarding': typeof HomeLOnboardingRoute
'/home/transactions': typeof HomeLTransactionsRoute
'/dialog/chain-add/$id': typeof DialogLChainAddIdRoute
Expand Down Expand Up @@ -349,6 +357,7 @@ export interface FileRoutesById {
'/home/_l': typeof HomeLRouteWithChildren
'/home/_l/account': typeof HomeLAccountRoute
'/home/_l/connections': typeof HomeLConnectionsRoute
'/home/_l/forge-traces': typeof HomeLForgeTracesRoute
'/home/_l/onboarding': typeof HomeLOnboardingRoute
'/home/_l/transactions': typeof HomeLTransactionsRoute
'/dialog/_l/chain-add/$id': typeof DialogLChainAddIdRoute
Expand Down Expand Up @@ -393,6 +402,7 @@ export interface FileRouteTypes {
| '/home'
| '/home/account'
| '/home/connections'
| '/home/forge-traces'
| '/home/onboarding'
| '/home/transactions'
| '/dialog/chain-add/$id'
Expand Down Expand Up @@ -430,6 +440,7 @@ export interface FileRouteTypes {
| '/home'
| '/home/account'
| '/home/connections'
| '/home/forge-traces'
| '/home/onboarding'
| '/home/transactions'
| '/dialog/chain-add/$id'
Expand Down Expand Up @@ -466,6 +477,7 @@ export interface FileRouteTypes {
| '/home/_l'
| '/home/_l/account'
| '/home/_l/connections'
| '/home/_l/forge-traces'
| '/home/_l/onboarding'
| '/home/_l/transactions'
| '/dialog/_l/chain-add/$id'
Expand Down Expand Up @@ -574,6 +586,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof HomeLOnboardingRouteImport
parentRoute: typeof HomeLRoute
}
'/home/_l/forge-traces': {
id: '/home/_l/forge-traces'
path: '/forge-traces'
fullPath: '/home/forge-traces'
preLoaderRoute: typeof HomeLForgeTracesRouteImport
parentRoute: typeof HomeLRoute
}
'/home/_l/connections': {
id: '/home/_l/connections'
path: '/connections'
Expand Down Expand Up @@ -1000,6 +1019,7 @@ const HomeLTransferRouteWithChildren = HomeLTransferRoute._addFileChildren(
interface HomeLRouteChildren {
HomeLAccountRoute: typeof HomeLAccountRoute
HomeLConnectionsRoute: typeof HomeLConnectionsRoute
HomeLForgeTracesRoute: typeof HomeLForgeTracesRoute
HomeLOnboardingRoute: typeof HomeLOnboardingRoute
HomeLTransactionsRoute: typeof HomeLTransactionsRoute
HomeLContractsRoute: typeof HomeLContractsRouteWithChildren
Expand All @@ -1010,6 +1030,7 @@ interface HomeLRouteChildren {
const HomeLRouteChildren: HomeLRouteChildren = {
HomeLAccountRoute: HomeLAccountRoute,
HomeLConnectionsRoute: HomeLConnectionsRoute,
HomeLForgeTracesRoute: HomeLForgeTracesRoute,
HomeLOnboardingRoute: HomeLOnboardingRoute,
HomeLTransactionsRoute: HomeLTransactionsRoute,
HomeLContractsRoute: HomeLContractsRouteWithChildren,
Expand Down
Loading
Loading