2026-04-24 14:49:57 -03:00
|
|
|
#[cfg(any(not(coverage), test))]
|
|
|
|
|
use anyhow::{Context, Result, bail};
|
2026-04-25 16:48:20 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
use serde::Serialize;
|
2026-05-04 16:58:55 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
use std::collections::BTreeSet;
|
2026-04-30 22:23:29 -03:00
|
|
|
#[cfg(any(not(coverage), test))]
|
|
|
|
|
use std::path::PathBuf;
|
2026-04-25 16:48:20 -03:00
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
use lesavka_client::sync_probe::analyze::{
|
2026-04-30 22:23:29 -03:00
|
|
|
SyncAnalysisOptions, SyncAnalysisReport, SyncAnalysisVerdict, SyncCalibrationRecommendation,
|
|
|
|
|
analyze_capture,
|
2026-04-25 16:48:20 -03:00
|
|
|
};
|
2026-04-24 14:49:57 -03:00
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-04-25 16:48:20 -03:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct SyncAnalyzeOutput<'a> {
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
report: &'a SyncAnalysisReport,
|
2026-05-04 16:58:55 -03:00
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
signature_coverage: Option<SignatureCoverage>,
|
2026-04-25 16:48:20 -03:00
|
|
|
calibration: SyncCalibrationRecommendation,
|
2026-04-30 22:23:29 -03:00
|
|
|
verdict: SyncAnalysisVerdict,
|
2026-04-25 16:48:20 -03:00
|
|
|
}
|
2026-04-24 14:49:57 -03:00
|
|
|
|
2026-05-04 16:58:55 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct SignatureCoverage {
|
|
|
|
|
expected_event_count: usize,
|
|
|
|
|
expected_codes: Vec<u32>,
|
|
|
|
|
paired_event_count: usize,
|
|
|
|
|
paired_server_event_ids: Vec<usize>,
|
|
|
|
|
paired_codes: Vec<u32>,
|
|
|
|
|
missing_event_ids: Vec<usize>,
|
|
|
|
|
missing_codes: Vec<u32>,
|
|
|
|
|
unknown_pair_identity_count: usize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 14:49:57 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `main` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-04-24 14:49:57 -03:00
|
|
|
fn main() -> Result<()> {
|
2026-04-30 22:23:29 -03:00
|
|
|
let args = parse_args(std::env::args().skip(1))?;
|
2026-05-01 02:05:07 -03:00
|
|
|
let report = analyze_capture(&args.capture_path, &args.options)
|
2026-04-30 22:23:29 -03:00
|
|
|
.with_context(|| format!("analyzing sync capture {}", args.capture_path.display()))?;
|
2026-04-25 16:48:20 -03:00
|
|
|
let calibration = report.calibration_recommendation();
|
2026-04-30 22:23:29 -03:00
|
|
|
let verdict = report.verdict();
|
2026-05-04 16:58:55 -03:00
|
|
|
let signature_coverage = signature_coverage(&args.options.event_width_codes, &report);
|
|
|
|
|
let human_report = format_human_report(
|
|
|
|
|
&args.capture_path,
|
|
|
|
|
&report,
|
|
|
|
|
signature_coverage.as_ref(),
|
|
|
|
|
&calibration,
|
|
|
|
|
&verdict,
|
|
|
|
|
);
|
2026-04-30 22:23:29 -03:00
|
|
|
let output = SyncAnalyzeOutput {
|
|
|
|
|
report: &report,
|
2026-05-04 16:58:55 -03:00
|
|
|
signature_coverage,
|
2026-04-30 22:23:29 -03:00
|
|
|
calibration,
|
|
|
|
|
verdict,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(report_dir) = &args.report_dir {
|
|
|
|
|
write_report_dir(report_dir, &human_report, &output)?;
|
|
|
|
|
}
|
2026-04-24 14:49:57 -03:00
|
|
|
|
2026-04-30 22:23:29 -03:00
|
|
|
if args.emit_json {
|
2026-04-24 14:49:57 -03:00
|
|
|
println!(
|
|
|
|
|
"{}",
|
2026-04-25 16:48:20 -03:00
|
|
|
serde_json::to_string_pretty(&output).context("serializing JSON report")?
|
2026-04-24 14:49:57 -03:00
|
|
|
);
|
|
|
|
|
} else {
|
2026-04-30 22:23:29 -03:00
|
|
|
print!("{human_report}");
|
2026-04-24 14:49:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(any(not(coverage), test))]
|
2026-05-01 02:05:07 -03:00
|
|
|
#[derive(Debug, PartialEq)]
|
2026-04-30 22:23:29 -03:00
|
|
|
struct AnalyzeArgs {
|
|
|
|
|
capture_path: PathBuf,
|
|
|
|
|
emit_json: bool,
|
|
|
|
|
report_dir: Option<PathBuf>,
|
2026-05-01 02:05:07 -03:00
|
|
|
options: SyncAnalysisOptions,
|
2026-04-30 22:23:29 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(any(not(coverage), test))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-04-30 22:23:29 -03:00
|
|
|
fn parse_args<I, S>(args: I) -> Result<AnalyzeArgs>
|
2026-04-24 14:49:57 -03:00
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = S>,
|
|
|
|
|
S: Into<String>,
|
|
|
|
|
{
|
|
|
|
|
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
|
|
|
|
|
if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
2026-05-01 02:05:07 -03:00
|
|
|
println!(
|
2026-05-04 12:48:17 -03:00
|
|
|
"Usage: lesavka-sync-analyze <capture.mkv> [--json] [--report-dir <dir>] [--event-width-codes 1,2,1,3] [--analysis-window-s START:END]"
|
2026-05-01 02:05:07 -03:00
|
|
|
);
|
2026-04-24 14:49:57 -03:00
|
|
|
std::process::exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut emit_json = false;
|
2026-04-30 22:23:29 -03:00
|
|
|
let mut report_dir = None::<PathBuf>;
|
|
|
|
|
let mut capture_path = None::<PathBuf>;
|
2026-05-01 02:05:07 -03:00
|
|
|
let mut options = SyncAnalysisOptions::default();
|
2026-04-30 22:23:29 -03:00
|
|
|
let mut iter = args.into_iter();
|
|
|
|
|
while let Some(arg) = iter.next() {
|
2026-04-24 14:49:57 -03:00
|
|
|
if arg == "--json" {
|
|
|
|
|
emit_json = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-30 22:23:29 -03:00
|
|
|
if arg == "--report-dir" {
|
|
|
|
|
let Some(dir) = iter.next() else {
|
|
|
|
|
bail!("--report-dir requires a directory");
|
|
|
|
|
};
|
|
|
|
|
report_dir = Some(PathBuf::from(dir));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(dir) = arg.strip_prefix("--report-dir=") {
|
|
|
|
|
if dir.is_empty() {
|
|
|
|
|
bail!("--report-dir requires a directory");
|
|
|
|
|
}
|
|
|
|
|
report_dir = Some(PathBuf::from(dir));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-01 02:05:07 -03:00
|
|
|
if arg == "--event-width-codes" {
|
|
|
|
|
let Some(raw_codes) = iter.next() else {
|
|
|
|
|
bail!("--event-width-codes requires a comma-separated list");
|
|
|
|
|
};
|
|
|
|
|
options.event_width_codes = parse_event_width_codes(&raw_codes)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(raw_codes) = arg.strip_prefix("--event-width-codes=") {
|
|
|
|
|
if raw_codes.is_empty() {
|
|
|
|
|
bail!("--event-width-codes requires a comma-separated list");
|
|
|
|
|
}
|
|
|
|
|
options.event_width_codes = parse_event_width_codes(raw_codes)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-04 12:48:17 -03:00
|
|
|
if arg == "--analysis-window-s" {
|
|
|
|
|
let Some(raw_window) = iter.next() else {
|
|
|
|
|
bail!("--analysis-window-s requires START:END seconds");
|
|
|
|
|
};
|
|
|
|
|
parse_analysis_window(&raw_window, &mut options)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(raw_window) = arg.strip_prefix("--analysis-window-s=") {
|
|
|
|
|
if raw_window.is_empty() {
|
|
|
|
|
bail!("--analysis-window-s requires START:END seconds");
|
|
|
|
|
}
|
|
|
|
|
parse_analysis_window(raw_window, &mut options)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if arg == "--analysis-start-s" {
|
|
|
|
|
let Some(raw_start) = iter.next() else {
|
|
|
|
|
bail!("--analysis-start-s requires seconds");
|
|
|
|
|
};
|
|
|
|
|
options.analysis_start_s = Some(parse_analysis_seconds(&raw_start, "analysis start")?);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(raw_start) = arg.strip_prefix("--analysis-start-s=") {
|
|
|
|
|
if raw_start.is_empty() {
|
|
|
|
|
bail!("--analysis-start-s requires seconds");
|
|
|
|
|
}
|
|
|
|
|
options.analysis_start_s = Some(parse_analysis_seconds(raw_start, "analysis start")?);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if arg == "--analysis-end-s" {
|
|
|
|
|
let Some(raw_end) = iter.next() else {
|
|
|
|
|
bail!("--analysis-end-s requires seconds");
|
|
|
|
|
};
|
|
|
|
|
options.analysis_end_s = Some(parse_analysis_seconds(&raw_end, "analysis end")?);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(raw_end) = arg.strip_prefix("--analysis-end-s=") {
|
|
|
|
|
if raw_end.is_empty() {
|
|
|
|
|
bail!("--analysis-end-s requires seconds");
|
|
|
|
|
}
|
|
|
|
|
options.analysis_end_s = Some(parse_analysis_seconds(raw_end, "analysis end")?);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-24 14:49:57 -03:00
|
|
|
if capture_path.is_some() {
|
|
|
|
|
bail!("unexpected extra argument `{arg}`");
|
|
|
|
|
}
|
2026-04-30 22:23:29 -03:00
|
|
|
capture_path = Some(PathBuf::from(arg));
|
2026-04-24 14:49:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let capture_path = capture_path.context("capture path is required")?;
|
2026-04-30 22:23:29 -03:00
|
|
|
Ok(AnalyzeArgs {
|
|
|
|
|
capture_path,
|
|
|
|
|
emit_json,
|
|
|
|
|
report_dir,
|
2026-05-01 02:05:07 -03:00
|
|
|
options,
|
2026-04-30 22:23:29 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 02:05:07 -03:00
|
|
|
#[cfg(any(not(coverage), test))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_event_width_codes` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-01 02:05:07 -03:00
|
|
|
fn parse_event_width_codes(raw: &str) -> Result<Vec<u32>> {
|
|
|
|
|
let codes = raw
|
|
|
|
|
.split(',')
|
|
|
|
|
.filter_map(|part| {
|
|
|
|
|
let trimmed = part.trim();
|
|
|
|
|
(!trimmed.is_empty()).then_some(trimmed)
|
|
|
|
|
})
|
|
|
|
|
.map(|part| {
|
|
|
|
|
let code = part
|
|
|
|
|
.parse::<u32>()
|
|
|
|
|
.with_context(|| format!("parsing event width code `{part}`"))?;
|
|
|
|
|
if code == 0 {
|
|
|
|
|
bail!("event width codes must be positive");
|
|
|
|
|
}
|
|
|
|
|
Ok(code)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
if codes.is_empty() {
|
|
|
|
|
bail!("event width code list must not be empty");
|
|
|
|
|
}
|
|
|
|
|
Ok(codes)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 12:48:17 -03:00
|
|
|
#[cfg(any(not(coverage), test))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_analysis_window` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 12:48:17 -03:00
|
|
|
fn parse_analysis_window(raw: &str, options: &mut SyncAnalysisOptions) -> Result<()> {
|
|
|
|
|
let Some((start, end)) = raw.split_once(':') else {
|
|
|
|
|
bail!("--analysis-window-s requires START:END seconds");
|
|
|
|
|
};
|
|
|
|
|
options.analysis_start_s = if start.trim().is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(parse_analysis_seconds(start, "analysis start")?)
|
|
|
|
|
};
|
|
|
|
|
options.analysis_end_s = if end.trim().is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(parse_analysis_seconds(end, "analysis end")?)
|
|
|
|
|
};
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(any(not(coverage), test))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_analysis_seconds` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 12:48:17 -03:00
|
|
|
fn parse_analysis_seconds(raw: &str, label: &str) -> Result<f64> {
|
|
|
|
|
let value = raw
|
|
|
|
|
.trim()
|
|
|
|
|
.parse::<f64>()
|
|
|
|
|
.with_context(|| format!("parsing {label} `{raw}`"))?;
|
|
|
|
|
if !value.is_finite() || value < 0.0 {
|
|
|
|
|
bail!("{label} must be a finite non-negative timestamp");
|
|
|
|
|
}
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
include!("lesavka_sync_analyze/human_report.rs");
|
2026-04-30 22:23:29 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `signature_coverage` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 16:58:55 -03:00
|
|
|
fn signature_coverage(
|
|
|
|
|
expected_codes: &[u32],
|
|
|
|
|
report: &SyncAnalysisReport,
|
|
|
|
|
) -> Option<SignatureCoverage> {
|
|
|
|
|
if expected_codes.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let paired_server_event_ids = report
|
|
|
|
|
.paired_events
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|event| event.server_event_id)
|
|
|
|
|
.collect::<BTreeSet<_>>()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let paired_codes = paired_server_event_ids
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|index| expected_codes.get(*index).copied())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let missing_event_ids = expected_codes
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter_map(|(index, _)| (!paired_server_event_ids.contains(&index)).then_some(index))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let missing_codes = missing_event_ids
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|index| expected_codes.get(*index).copied())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let unknown_pair_identity_count = report
|
|
|
|
|
.paired_events
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|event| event.server_event_id.is_none() || event.event_code.is_none())
|
|
|
|
|
.count();
|
|
|
|
|
Some(SignatureCoverage {
|
|
|
|
|
expected_event_count: expected_codes.len(),
|
|
|
|
|
expected_codes: expected_codes.to_vec(),
|
|
|
|
|
paired_event_count: paired_server_event_ids.len(),
|
|
|
|
|
paired_server_event_ids,
|
|
|
|
|
paired_codes,
|
|
|
|
|
missing_event_ids,
|
|
|
|
|
missing_codes,
|
|
|
|
|
unknown_pair_identity_count,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `format_signature_coverage` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 16:58:55 -03:00
|
|
|
fn format_signature_coverage(coverage: Option<&SignatureCoverage>) -> String {
|
|
|
|
|
let Some(coverage) = coverage else {
|
|
|
|
|
return String::new();
|
|
|
|
|
};
|
|
|
|
|
let missing_ids = format_usize_list(&coverage.missing_event_ids);
|
|
|
|
|
let missing_codes = format_u32_list(&coverage.missing_codes);
|
|
|
|
|
format!(
|
|
|
|
|
"- expected coded signatures: {}\n- paired coded signatures: {}/{}\n- missing paired signature ids: {}\n- missing paired signature codes: {}\n- paired signatures without identity: {}\n",
|
|
|
|
|
coverage.expected_event_count,
|
|
|
|
|
coverage.paired_event_count,
|
|
|
|
|
coverage.expected_event_count,
|
|
|
|
|
missing_ids,
|
|
|
|
|
missing_codes,
|
|
|
|
|
coverage.unknown_pair_identity_count
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 14:59:30 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
fn unpaired_video_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
|
|
|
|
|
unpaired_onsets(
|
|
|
|
|
&report.video_onsets_s,
|
|
|
|
|
&report
|
|
|
|
|
.paired_events
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|event| event.video_time_s)
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
fn unpaired_audio_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
|
|
|
|
|
unpaired_onsets(
|
|
|
|
|
&report.audio_onsets_s,
|
|
|
|
|
&report
|
|
|
|
|
.paired_events
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|event| event.audio_time_s)
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `unpaired_onsets` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-01 14:59:30 -03:00
|
|
|
fn unpaired_onsets(all_onsets: &[f64], paired_onsets: &[f64]) -> Vec<f64> {
|
|
|
|
|
const SAME_ONSET_EPSILON_S: f64 = 0.000_001;
|
|
|
|
|
all_onsets
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.filter(|onset| {
|
|
|
|
|
!paired_onsets
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|paired| (paired - onset).abs() <= SAME_ONSET_EPSILON_S)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `format_onset_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-01 14:59:30 -03:00
|
|
|
fn format_onset_list(onsets: &[f64]) -> String {
|
|
|
|
|
const MAX_PRINTED_ONSETS: usize = 12;
|
|
|
|
|
if onsets.is_empty() {
|
|
|
|
|
return "none".to_string();
|
|
|
|
|
}
|
|
|
|
|
let mut formatted = onsets
|
|
|
|
|
.iter()
|
|
|
|
|
.take(MAX_PRINTED_ONSETS)
|
|
|
|
|
.map(|onset| format!("{onset:.3}s"))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
if onsets.len() > MAX_PRINTED_ONSETS {
|
|
|
|
|
formatted.push(format!("...+{}", onsets.len() - MAX_PRINTED_ONSETS));
|
|
|
|
|
}
|
|
|
|
|
formatted.join(", ")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 16:58:55 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `format_usize_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 16:58:55 -03:00
|
|
|
fn format_usize_list(values: &[usize]) -> String {
|
|
|
|
|
if values.is_empty() {
|
|
|
|
|
return "none".to_string();
|
|
|
|
|
}
|
|
|
|
|
values
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|value| value.to_string())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `format_u32_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-04 16:58:55 -03:00
|
|
|
fn format_u32_list(values: &[u32]) -> String {
|
|
|
|
|
if values.is_empty() {
|
|
|
|
|
return "none".to_string();
|
|
|
|
|
}
|
|
|
|
|
values
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|value| value.to_string())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
include!("lesavka_sync_analyze/report_output_files.rs");
|
2026-05-04 16:58:55 -03:00
|
|
|
|
2026-04-24 14:49:57 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn main() {}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2026-05-06 05:50:59 -03:00
|
|
|
#[path = "tests/lesavka_sync_analyze.rs"]
|
|
|
|
|
mod tests;
|