55import json
66import os
77import pathlib
8+ import re
89from typing import TypeVar , Literal , TYPE_CHECKING
910
1011import pytest
@@ -42,24 +43,76 @@ def _get_item_file(item: pytest.Item) -> str:
4243 return item .nodeid .split ("::" , 1 )[0 ]
4344
4445
46+ @dataclasses .dataclass (frozen = True )
47+ class GroupStealOpt :
48+ source_group : int | None
49+ target_group : int
50+ amount : int
51+
52+ def __str__ (self ) -> str :
53+ out = f"g{ self .target_group } :{ self .amount } "
54+ if self .source_group :
55+ out += f":g{ self .source_group } "
56+ return out
57+
58+
59+ def _distribute (
60+ groups : list [list [pytest .Item ]],
61+ from_ : int ,
62+ to : int ,
63+ amount : int ,
64+ ) -> None :
65+ items = groups [from_ ]
66+ num_items_to_move = max (0 , min (len (items ), (len (items ) * amount ) // 100 ))
67+ items_to_move = items [:num_items_to_move ]
68+ groups [to ].extend (items_to_move )
69+ groups [from_ ] = items [num_items_to_move :]
70+
71+
4572def _distribute_with_bias (
46- groups : list [list [pytest .Item ]], target : int , bias : int
73+ groups : list [list [pytest .Item ]],
74+ group_steal : GroupStealOpt ,
4775) -> list [list [pytest .Item ]]:
48- for i , lst in enumerate (groups ):
49- if i != target :
50- num_items_to_move = max (0 , min (len (lst ), (len (lst ) * bias ) // 100 ))
51- items_to_move = lst [:num_items_to_move ]
52- groups [target ].extend (items_to_move )
53- groups [i ] = lst [num_items_to_move :]
76+ source_groups = (
77+ [group_steal .source_group ]
78+ if group_steal .source_group is not None
79+ else range (len (groups ))
80+ )
81+ for source_group in source_groups :
82+ if source_group != group_steal .target_group :
83+ _distribute (
84+ groups ,
85+ from_ = source_group ,
86+ to = group_steal .target_group ,
87+ amount = group_steal .amount ,
88+ )
5489
5590 return groups
5691
5792
58- def _get_group_steal_opt (opt : str | None ) -> tuple [ int , int ] | None :
93+ def _get_group_steal_opt (opt : str | None ) -> list [ GroupStealOpt ] :
5994 if opt is None :
60- return None
61- target_group , amount_to_steal = opt .split (":" )
62- return int (target_group ) - 1 , int (amount_to_steal )
95+ return []
96+
97+ opts = []
98+ for group_opt in opt .split ("," ):
99+ match = re .match (r"g?(\d+):(\d+)(?::g?(\d+))?" , group_opt .strip ())
100+ if match is None :
101+ raise ValueError (f"Invalid group steal option: { group_opt !r} " )
102+ source_group : int | None = None
103+ target_group = int (match .group (1 )) - 1
104+ amount_to_steal = int (match .group (2 ))
105+ if source_group_match := match .group (3 ):
106+ source_group = int (source_group_match ) - 1
107+
108+ opts .append (
109+ GroupStealOpt (
110+ source_group = source_group ,
111+ target_group = target_group ,
112+ amount = amount_to_steal ,
113+ )
114+ )
115+ return opts
63116
64117
65118def _justify_items (
@@ -113,12 +166,12 @@ def _justify_xdist_groups(groups: list[list[pytest.Item]]) -> list[list[pytest.I
113166 return groups
114167
115168
116- @dataclasses .dataclass
169+ @dataclasses .dataclass ( kw_only = True )
117170class CdistConfig :
118171 current_group : int
119172 total_groups : int
120173 justify_items_strategy : JustifyItemsStrategy = "none"
121- group_steal : tuple [ int , int ] | None = None
174+ group_steal : list [ GroupStealOpt ]
122175 write_report : bool = False
123176 report_dir : pathlib .Path = pathlib .Path ("." )
124177
@@ -132,9 +185,8 @@ def cli_options(self, config: pytest.Config) -> str:
132185 opts .append (f"--cdist-justify-items={ self .justify_items_strategy } " )
133186
134187 if self .group_steal and "cdist-group-steal" not in config .inicfg :
135- opts .append (
136- f"--cdist-group-steal={ self .group_steal [0 ] + 1 } :{ self .group_steal [1 ]} "
137- )
188+ steal_opt = "," .join (map (str , self .group_steal ))
189+ opts .append (f"--cdist-group-steal={ steal_opt } " )
138190
139191 if self .write_report :
140192 opts .append ("--cdist-report" )
@@ -202,7 +254,7 @@ def pytest_addoption(parser: pytest.Parser) -> None:
202254 action = "store" ,
203255 default = None ,
204256 help = "make a group steal a percentage of items from other groups. '1:30' would "
205- "make group 1 steal 30%% of items from all other groups) " ,
257+ "make group 1 steal 30% of items from all other groups" ,
206258 )
207259
208260 parser .addini ("cdist-justify-items" , help = "justify items strategy" , default = "none" )
@@ -238,12 +290,10 @@ def pytest_collection_modifyitems(
238290
239291 groups = _partition_list (items , cdist_config .total_groups )
240292
241- if cdist_config .group_steal is not None :
242- target_group , amount_to_steal = cdist_config .group_steal
293+ for group_steal in cdist_config .group_steal :
243294 groups = _distribute_with_bias (
244295 groups ,
245- target = target_group ,
246- bias = amount_to_steal ,
296+ group_steal = group_steal ,
247297 )
248298
249299 if cdist_config .justify_items_strategy != "none" :
0 commit comments