libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit f8e02d9730b6fd44e7b2cfb5104b610d921479ef
parent aa49980281add0cef16b2c1efda3eef2b9e5ae9c
Author: Antoine A <>
Date:   Thu, 30 Jul 2026 12:38:56 +0200

harness: add compact cmd

Diffstat:
MCargo.lock | 1+
Mtestbench/Cargo.toml | 1+
Mtestbench/src/main.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 105 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3583,6 +3583,7 @@ name = "testbench" version = "1.5.0" dependencies = [ "anyhow", + "aws-lc-rs", "axum", "clap", "globset", diff --git a/testbench/Cargo.toml b/testbench/Cargo.toml @@ -19,6 +19,7 @@ taler-test-utils.workspace = true sqlx.workspace = true http-client.workspace = true axum.workspace = true +aws-lc-rs.workspace = true libeufin-bank = { path = "../libeufin-bank", features = ["test-utils"] } libeufin-nexus = { path = "../libeufin-nexus" } libeufin-ebisync = { path = "../libeufin-ebisync" } diff --git a/testbench/src/main.rs b/testbench/src/main.rs @@ -17,9 +17,15 @@ * <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, path::PathBuf, str::FromStr}; +use std::{ + collections::HashSet, + fmt::Display, + path::{Path, PathBuf}, + str::FromStr, +}; use anyhow::bail; +use aws_lc_rs::digest::SHA256; use clap::{Parser, ValueEnum}; use globset::{Glob, GlobSetBuilder}; use jiff::Timestamp; @@ -105,6 +111,7 @@ pub enum NexusCmd { path: PathBuf, glob: Vec<Glob>, }, + Compact, } #[derive(Parser)] @@ -125,6 +132,7 @@ pub enum SyncCmd { raw_args: Vec<String>, }, Serve, + Compact, } fn step(name: impl Display) { @@ -289,7 +297,8 @@ async fn main() -> anyhow::Result<()> { msg(format!("Init {name}")); run("dbinit").await; - let prompt = format!("{:?} {} {name}", cmd.component, cmd.platform); + let platform = cmd.platform; + let prompt = format!("{:?} {platform} {name}", cmd.component); match cmd.component { Component::Nexus => { @@ -304,6 +313,9 @@ async fn main() -> anyhow::Result<()> { NexusCmd::ResetDb => { run("dbinit -r").await; } + NexusCmd::Compact => { + compact(format!("testbench/test/{platform}")); + } NexusCmd::Fetch { raw_args } => { run(&format!("ebics-fetch {ebics_log} {}", raw_args.join(" "))).await; } @@ -366,6 +378,9 @@ async fn main() -> anyhow::Result<()> { auto_setup().await; if let Some(cmd) = repl.read_line(&prompt, "") { match cmd { + SyncCmd::Compact => { + compact(format!("testbench/test/{platform}")); + } SyncCmd::Setup => { run(&format!("setup {ebics_log}")).await; } @@ -397,3 +412,89 @@ async fn main() -> anyhow::Result<()> { } Ok(()) } + +/// Remove duplicated files and empty dir +fn compact(p: String) { + /// Announce and recursively remove file or directory + fn remove(p: &Path, reason: &str) { + println!("rm {} {}", reason, p.display()); + if p.is_dir() { + std::fs::remove_dir_all(p).unwrap(); + } else { + std::fs::remove_file(p).unwrap() + } + } + + /// Collect directory entries into a Vec upfront + /// and avoids surprises from mutating a directory while iterating it). + fn read_dir_paths(dir: &Path) -> Vec<PathBuf> { + let mut v = Vec::new(); + for entry in std::fs::read_dir(dir).unwrap() { + v.push(entry.unwrap().path()); + } + v + } + + fn is_dir_empty(dir: &Path) -> bool { + std::fs::read_dir(dir).unwrap().next().is_none() + } + + /// Delete file if another file has the same (whitespace-stripped) content + fn rm_if_similar(p: &Path, content: &str, content_hashes: &mut HashSet<Vec<u8>>) { + let normalized: String = content.chars().filter(|c| c.is_whitespace()).collect(); + + let digest = aws_lc_rs::digest::digest(&SHA256, normalized.as_bytes()); + + if content_hashes.contains(digest.as_ref()) { + remove(p, "similar"); + } else { + content_hashes.insert(digest.as_ref().to_vec()); + } + } + + fn file_name_str(p: &Path) -> String { + p.file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default() + } + + let mut content_hashes: HashSet<Vec<u8>> = HashSet::new(); + + for date in read_dir_paths(p.as_ref()) { + if !date.is_dir() { + continue; + } + + for request in read_dir_paths(&date) { + let payload_file_path = request.join("payload.xml"); + let payload_dir_path = request.join("payload"); + let request_name = file_name_str(&request); + + if payload_file_path.exists() { + let content = std::fs::read_to_string(&payload_file_path).unwrap(); + + if request_name.contains("HAC") && !content.contains("ORDER_HAC_FINAL_NEG") { + remove(&request, "simple hac"); + } else if request_name.contains("HAA") && !content.contains("<Service>") { + remove(&request, "empty haa"); + } else if request_name.contains("wssparam") { + remove(&request, "wssparam"); + } else { + rm_if_similar(&payload_file_path, &content, &mut content_hashes); + } + } else if payload_dir_path.exists() { + for file in read_dir_paths(&payload_dir_path) { + let content = std::fs::read_to_string(&file).unwrap(); + rm_if_similar(&file, &content, &mut content_hashes); + } + } + if request.exists() && !payload_file_path.exists() && !payload_dir_path.exists() { + remove(&request, "empty request"); + } + } + + if is_dir_empty(&date) { + remove(&date, "empty dir"); + } + } +}