Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
13 changes: 10 additions & 3 deletions crates/forge/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::compatibility_check::{Requirement, RequirementsChecker, create_version_parser};
use crate::partition::Partition;
use anyhow::Result;
use camino::Utf8PathBuf;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
Expand All @@ -25,6 +26,7 @@ mod clean;
mod combine_configs;
mod compatibility_check;
mod new;
mod partition;
mod profile_validation;
pub mod run_tests;
pub mod scarb;
Expand Down Expand Up @@ -83,7 +85,7 @@ enum ForgeSubcommand {
/// Run tests for a project in the current directory
Test {
#[command(flatten)]
args: TestArgs,
args: Box<TestArgs>,
},
/// Create a new Forge project at <PATH>
New {
Expand Down Expand Up @@ -149,7 +151,7 @@ pub struct TestArgs {
run_native: bool,

/// Use exact matches for `test_filter`
#[arg(short, long)]
#[arg(short, long, conflicts_with = "partition")]
exact: bool,

/// Skips any tests whose name contains the given SKIP string.
Expand Down Expand Up @@ -213,6 +215,11 @@ pub struct TestArgs {
#[arg(long, value_enum, default_value_t)]
tracked_resource: ForgeTrackedResource,

/// If specified, divides tests into partitions and runs specified partition.
/// <PARTITION> is in the format INDEX/TOTAL, where INDEX is the 1-based index of the partition to run, and TOTAL is the number of partitions.
#[arg(long, conflicts_with = "exact")]
partition: Option<Partition>,

/// Additional arguments for cairo-coverage or cairo-profiler
#[arg(last = true)]
additional_args: Vec<OsString>,
Expand Down Expand Up @@ -327,7 +334,7 @@ pub fn main_execution(ui: Arc<UI>) -> Result<ExitStatus> {
.enable_all()
.build()?;

rt.block_on(run_for_workspace(args, ui))
rt.block_on(run_for_workspace(*args, ui))
}
ForgeSubcommand::CheckRequirements => {
check_requirements(true, &ui)?;
Expand Down
111 changes: 111 additions & 0 deletions crates/forge/src/partition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use serde::Serialize;
use std::{collections::HashMap, str::FromStr};

#[derive(Debug, Clone, Copy, Serialize)]
pub struct Partition {
index: usize,
total: usize,
}

#[allow(dead_code)] // TODO: Removed in later PRs
impl Partition {
#[must_use]
pub fn index(&self) -> usize {
self.index
}

#[must_use]
pub fn total(&self) -> usize {
self.total
}
}

impl FromStr for Partition {
type Err = String;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let parts: Vec<&str> = s.split('/').collect();

if parts.len() != 2 {
return Err("Partition must be in the format <INDEX>/<TOTAL>".to_string());
}

let index = parts[0]
.parse::<usize>()
.map_err(|_| "INDEX must be a positive integer".to_string())?;
let total = parts[1]
.parse::<usize>()
.map_err(|_| "TOTAL must be a positive integer".to_string())?;

if index == 0 || total == 0 || index > total {
return Err("Invalid partition values: ensure 1 <= INDEX <= TOTAL".to_string());
}

Ok(Partition { index, total })
}
}

/// A mapping between test full paths and their assigned partition indices.
#[derive(Serialize)]
pub struct TestPartitionMap(HashMap<String, usize>);

#[derive(Serialize)]
pub struct PartitionConfig {
partition: Partition,
test_partition_map: TestPartitionMap,
}

#[allow(dead_code)] // TODO: Removed in later PRs
impl PartitionConfig {
pub fn new(partition: Partition, test_partition_map: TestPartitionMap) -> Self {
Self {
partition,
test_partition_map,
}
}

pub fn partition(&self) -> Partition {
self.partition
}

pub fn test_partition_map(&self) -> &TestPartitionMap {
&self.test_partition_map
}
}

#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;

#[test]
fn test_happy_case() {
let partition = "2/5".parse::<Partition>().unwrap();
assert_eq!(partition.index(), 2);
assert_eq!(partition.total(), 5);
}

#[test]
fn test_invalid_format() {
let err = "2-5".parse::<Partition>().unwrap_err();
assert_eq!(err, "Partition must be in the format <INDEX>/<TOTAL>");
}

// #[test]
#[test_case("-1/5", "INDEX")]
Comment on lines +65 to +66
Copy link
Member

Choose a reason for hiding this comment

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

Leftover?

Suggested change
// #[test]
#[test_case("-1/5", "INDEX")]
#[test_case("-1/5", "INDEX")]

#[test_case("2/-5", "TOTAL")]
#[test_case("a/5", "INDEX")]
#[test_case("2/b", "TOTAL")]
fn test_invalid_integer(partition: &str, invalid_part: &str) {
let err = partition.parse::<Partition>().unwrap_err();
assert_eq!(err, format!("{invalid_part} must be a positive integer"));
}

#[test_case("0/5")]
#[test_case("6/5")]
#[test_case("2/0")]
fn test_out_of_bounds(partition: &str) {
let err = partition.parse::<Partition>().unwrap_err();
assert_eq!(err, "Invalid partition values: ensure 1 <= INDEX <= TOTAL");
}
}
1 change: 1 addition & 0 deletions crates/forge/tests/e2e/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod fuzzing;
mod io_operations;
mod new;
mod package_warnings;
mod partitioning;
mod plugin_diagnostics;
mod plugin_versions;
mod profiles;
Expand Down
19 changes: 19 additions & 0 deletions crates/forge/tests/e2e/partitioning.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::e2e::common::runner::{setup_package, test_runner};
use indoc::indoc;
use shared::test_utils::output_assert::assert_stderr_contains;

#[test]
fn test_does_not_work_with_exact_flag() {
let temp = setup_package("simple_package");
let output = test_runner(&temp)
.args(["--partition", "3/3", "--workspace", "--exact"])
.assert()
.code(2);

assert_stderr_contains(
output,
indoc! {r"
error: the argument '--partition <PARTITION>' cannot be used with '--exact'
"},
);
}
Loading