Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/sequent-core/src/sqlite/results_area_contest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde_json::to_string;
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_area_contests_sqlite(
pub fn create_results_area_contests_sqlite(
sqlite_transaction: &Transaction<'_>,
area_contests: Vec<ResultsAreaContest>,
) -> Result<Vec<ResultsAreaContest>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rusqlite::{params, Transaction};
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_area_contest_candidates_sqlite(
pub fn create_results_area_contest_candidates_sqlite(
sqlite_transaction: &Transaction<'_>,
area_contest_candidates: Vec<ResultsAreaContestCandidate>,
) -> Result<()> {
Expand Down
2 changes: 1 addition & 1 deletion packages/sequent-core/src/sqlite/results_contest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use serde_json::to_string;
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_contest_sqlite(
pub fn create_results_contest_sqlite(
sqlite_transaction: &Transaction<'_>,
contests: Vec<ResultsContest>,
) -> Result<()> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rusqlite::{params, Transaction};
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_contest_candidates_sqlite(
pub fn create_results_contest_candidates_sqlite(
sqlite_transaction: &Transaction<'_>,
contest_candidates: Vec<ResultsContestCandidate>,
) -> Result<()> {
Expand Down
2 changes: 1 addition & 1 deletion packages/sequent-core/src/sqlite/results_election.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde_json::to_string;
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_election_sqlite(
pub fn create_results_election_sqlite(
sqlite_transaction: &Transaction<'_>,
elections: Vec<ResultsElection>,
) -> Result<()> {
Expand Down
2 changes: 1 addition & 1 deletion packages/sequent-core/src/sqlite/results_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use serde_json::to_string;
use tracing::instrument;

#[instrument(err, skip_all)]
pub async fn create_results_event_sqlite(
pub fn create_results_event_sqlite(
sqlite_transaction: &Transaction<'_>,
tenant_id: &str,
election_event_id: &str,
Expand Down
32 changes: 32 additions & 0 deletions packages/velvet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,35 @@ anyhow = "1"
[[bench]]
name = "pdf_generation"
harness = false

[lints.rustdoc]
missing_crate_level_docs = "deny"
broken_intra_doc_links = "deny"

[lints.rust]
missing_docs = "deny"
unsafe_code = "forbid"
private_interfaces = "warn"
private_bounds = "warn"
unnameable_types = "warn"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }

[lints.clippy]
missing_docs_in_private_items = "deny"
missing_errors_doc = "deny"
missing_panics_doc = "deny"
doc_markdown = "deny"
unwrap_used = "deny"
panic = "deny"
shadow_unrelated = "deny"
print_stdout = "deny"
print_stderr = "deny"
indexing_slicing = "deny"
missing_const_for_fn = "deny"
future_not_send = "deny"
arithmetic_side_effects = "deny"
suspicious = "deny"
complexity = "deny"
style = "deny"
perf = "deny"
pedantic = "deny"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new lint settings (pedantic = "deny", unwrap_used = "deny", panic = "deny", etc.) will fail the build with existing unwrap()/expect() usage across the crate (including tests/benches). Either remove/replace all unwrap/expect sites (e.g. convert to Result and propagate) or relax/allow specific lints where panics are acceptable (tests/benches).

Suggested change
pedantic = "deny"
pedantic = "warn"

Copilot uses AI. Check for mistakes.
35 changes: 26 additions & 9 deletions packages/velvet/src/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,57 @@ use super::error::{Error, Result};
use clap::{Parser, Subcommand};
use std::{collections::HashSet, fs::File, path::PathBuf};

/// Velvet command-line interface root.
#[derive(Parser)]
#[command(name = "Velvet")]
pub struct Cli {
/// Subcommand to execute.
#[command(subcommand)]
pub command: Commands,
}

/// Available CLI commands.
#[derive(Subcommand)]
pub enum Commands {
/// Run a pipeline stage.
Run(CliRun),
}

/// Configuration for the Run command.
#[derive(Parser, Debug, Clone)]
pub struct CliRun {
/// Pipeline stage to execute.
pub stage: String,
/// Pipe identifier within the stage.
pub pipe_id: String,

/// Path to the configuration file.
#[arg(short, long)]
pub config: PathBuf,

/// Input directory for ballots and data.
#[arg(short, long)]
pub input_dir: PathBuf,

/// Output directory for results.
#[arg(short, long)]
pub output_dir: PathBuf,
}

impl CliRun {
/// Validate the configuration for this CLI run.
///
/// # Errors
/// Returns an error if the config file is missing, cannot be opened, or is invalid.
pub fn validate(&self) -> Result<Config> {
let config = self.parse_config()?;

Ok(config)
}

/// Parse the configuration file for this CLI run.
///
/// # Errors
/// Returns an error if the config file is missing, cannot be opened, or is invalid.
fn parse_config(&self) -> Result<Config> {
if !self.config.exists() {
return Err(Error::ConfigNotFound);
Expand All @@ -51,17 +68,15 @@ impl CliRun {
let config: Config = parse_file(file)?;

for stage in &config.stages.order {
if !config.stages.stages_def.contains_key(stage) {
let Some(stage_def) = config.stages.stages_def.get(stage) else {
return Err(Error::StageDefinition(format!(
"Stage '{stage}', defined in stages.order, is not defined in stages."
)));
} else {
let stage_def = config.stages.stages_def.get(stage).unwrap();
let pipeline = &stage_def.pipeline;
let hash_set: HashSet<_> = pipeline.iter().map(|p| p.pipe.as_ref()).collect();
if hash_set.len() != pipeline.len() {
return Err(Error::StageDefinition(format!("Pipeline, defined in stages[{stage}].pipeline, should have unique pipe definition")));
}
};
let pipeline = &stage_def.pipeline;
let hash_set: HashSet<_> = pipeline.iter().map(|p| p.pipe.as_ref()).collect();
if hash_set.len() != pipeline.len() {
return Err(Error::StageDefinition(format!("Pipeline, defined in stages[{stage}].pipeline, should have unique pipe definition")));
}
}

Expand Down Expand Up @@ -138,6 +153,8 @@ mod tests {
Ok(())
}

/// # Panics
/// This test will panic if the config file does not exist, as expected.
#[test]
#[should_panic]
fn test_clirun_validate_not_found() {
Expand Down
8 changes: 8 additions & 0 deletions packages/velvet/src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@
use crate::pipes;
use pipes::error::Error as PipesError;

/// Result type alias for CLI operations.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Error types for CLI operations.
#[derive(Debug)]
pub enum Error {
/// Configuration file not found.
ConfigNotFound,
/// Cannot open configuration file.
CannotOpenConfig,
/// JSON serialization/deserialization error.
Json(serde_json::Error),
/// Invalid stage definition.
StageDefinition(String),
/// Pipeline not found.
PipeNotFound,
/// Error from pipeline operation.
FromPipe(PipesError),
}

Expand Down
6 changes: 6 additions & 0 deletions packages/velvet/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
//! Velvet CLI module: error handling, state, and test harness for CLI operations.
// SPDX-FileCopyrightText: 2025 Sequent Tech Inc <legal@sequentech.io>
//
// SPDX-License-Identifier: AGPL-3.0-only

/// Error types for CLI operations.
pub mod error;
/// CLI state and stage configuration.
pub mod state;
/// Test harness for comprehensive validation.
pub mod test_all;

/// Private CLI module containing command handling.
#[allow(clippy::module_inception)]
mod cli;
pub use cli::*;
73 changes: 54 additions & 19 deletions packages/velvet/src/cli/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,33 @@ use crate::pipes::PipeManager;
use crate::{config::Config, pipes::pipe_name::PipeName};
use tracing::instrument;

/// CLI execution state and configuration.
#[derive(Debug, Clone)]
pub struct State {
/// Command-line arguments and configuration.
pub cli: CliRun,
/// Execution stages in the pipeline.
pub stages: Vec<Stage>,
}

/// A single pipeline stage configuration.
#[derive(Debug, Clone)]
pub struct Stage {
/// Stage name.
pub name: String,
/// Pipeline components within this stage.
pub pipeline: Vec<PipeConfig>,
/// Current active pipe in this stage.
pub current_pipe: Option<PipeName>,
/// Previously executed pipe in this stage.
pub previous_pipe: Option<PipeName>,
}

impl State {
/// Creates a new State from the CLI and config.
///
/// # Errors
/// Returns an error if the state cannot be initialized from the given CLI or config.
#[instrument(err, skip(config), name = "State::new")]
pub fn new(cli: &CliRun, config: &Config) -> Result<Self> {
let stages =
Expand All @@ -50,8 +62,8 @@ impl State {
)?;

Ok(Stage {
name: stage_name.to_string(),
pipeline: pipeline.to_vec(),
name: stage_name.clone(),
pipeline: pipeline.clone(),
previous_pipe: None,
current_pipe: Some(current_pipe.pipe),
})
Expand All @@ -65,19 +77,24 @@ impl State {
}

#[instrument(skip_all)]
/// Gets the next pipe to execute in the pipeline.
///
/// Returns `None` if the current pipe is the same as the previous pipe.
pub fn get_next(&self) -> Option<PipeName> {
let stage_name = self.cli.stage.clone();
self.get_stage(&stage_name)
.map(|stage| {
if stage.current_pipe == stage.previous_pipe {
None
} else {
stage.current_pipe
}
})
.flatten()
self.get_stage(&stage_name).and_then(|stage| {
if stage.current_pipe == stage.previous_pipe {
None
} else {
stage.current_pipe
}
})
}

/// Executes the next pipe in the pipeline.
///
/// # Errors
/// Returns an error if the next pipe cannot be executed or if a pipe fails.
#[instrument(skip_all)]
pub fn exec_next(&mut self) -> Result<()> {
let stage_name = self.cli.stage.clone();
Expand All @@ -90,7 +107,7 @@ impl State {

if let Err(e) = res {
if let PipesError::FileAccess(file, _) = e {
println!("File not found: {} -- Not processed", file.display())
tracing::info!("File not found: {} -- Not processed", file.display());
} else {
return Err(Error::FromPipe(e));
}
Expand All @@ -101,11 +118,16 @@ impl State {
Ok(())
}

/// Returns a reference to the stage with the given name, if it exists.
#[instrument(skip(self))]
fn get_stage(&self, stage_name: &str) -> Option<&Stage> {
self.stages.iter().find(|s| s.name == stage_name)
}

/// Sets the current pipe for the given stage.
///
/// # Errors
/// Returns `Error::PipeNotFound` if the stage is not found.
#[instrument(skip(self))]
fn set_current_pipe(&mut self, stage_name: &str, next_pipe: Option<PipeName>) -> Result<()> {
let stage = self
Expand All @@ -114,13 +136,16 @@ impl State {
.find(|s| s.name == stage_name)
.ok_or(Error::PipeNotFound)?;

stage.previous_pipe = stage.current_pipe.clone();
stage.current_pipe = next_pipe;
stage.previous_pipe = stage.current_pipe;
stage.current_pipe = next_pipe;

Ok(())
}

/// Gets the computed results for the current stage.
///
/// # Errors
/// Returns `Error::PipeNotFound` if not all pipelines have been executed and `force` is false, or if the stage is not found.
#[instrument(skip_all, err)]
pub fn get_results(&self, force: bool) -> Result<Vec<ElectionReportDataComputed>> {
let next_pipename = self.get_next();
Expand All @@ -144,36 +169,46 @@ impl State {
}

impl Stage {
/// Returns the previous pipe in the pipeline, if any.
///
/// # Panics
/// Panics if the pipeline is empty.
#[instrument(skip_all)]
pub fn previous_pipe(&self) -> Option<PipeName> {
if let Some(current_pipe) = self.current_pipe {
let curr_index = self.pipeline.iter().position(|p| p.pipe == current_pipe);
if let Some(curr_index) = curr_index {
if curr_index > 0 {
return Some(self.pipeline[curr_index - 1].pipe);
return self
.pipeline
.get(curr_index.wrapping_sub(1))
.map(|p| p.pipe);
}
}
None
} else {
Some(self.pipeline[self.pipeline.len() - 1].pipe)
self.pipeline.last().map(|p| p.pipe)
}
}

/// Returns the next pipe in the pipeline, if any.
#[instrument(skip_all)]
pub fn next_pipe(&self) -> Option<PipeName> {
if let Some(current_pipe) = self.current_pipe {
let curr_index = self.pipeline.iter().position(|p| p.pipe == current_pipe);
if let Some(curr_index) = curr_index {
if curr_index + 1 < self.pipeline.len() {
return Some(self.pipeline[curr_index + 1].pipe);
}
// Avoid arithmetic side effects by checking bounds
let next_index = curr_index.checked_add(1)?;
return self.pipeline.get(next_index).map(|p| p.pipe);
}
None
} else {
None
}
}

/// Returns the configuration for the given pipe, if it exists.
#[must_use]
pub fn pipe_config(&self, pipe: Option<PipeName>) -> Option<PipeConfig> {
if let Some(pipe) = pipe {
self.pipeline.iter().find(|pc| pc.pipe == pipe).cloned()
Expand Down
Loading
Loading