8501fd58 Andy McCall 11 days ago ↶
1
2
3
4
5
1ec29691 Andy McCall 9 days ago ↶
6
use crate :: db:: runner_updates:: { self as rdb, Group, Policy, Soak} ;
8501fd58 Andy McCall 11 days ago ↶
7
use crate :: db:: runners:: Runner;
8
use crate :: db:: { self , Pool} ;
9
use flow_core:: api:: { UpdateOffer, UpdatePolicy} ;
10
use std:: collections:: { BTreeSet, HashMap} ;
11
12
13
14
pub const SETTING_REPO : & str = " runner_updates_repo" ;
1ec29691 Andy McCall 9 days ago ↶
15
16
17
18
pub const SETTING_SOAK_JOBS : & str = " runner_updates_soak_jobs" ;
19
pub const SETTING_SOAK_HOURS : & str = " runner_updates_soak_hours" ;
20
pub const SOAK_JOBS : i64 = 5 ;
21
pub const SOAK_HOURS : i64 = 24 ;
8501fd58 Andy McCall 11 days ago ↶
22
23
24
25
26
pub fn bucket ( runner_id : i64 ) -> i64 {
27
let h = blake3:: hash( format! ( " runner {runner_id} " ) .as_bytes ( ) ) ;
28
let mut first = [ 0 u8 ; 8 ] ;
29
first.copy_from_slice ( & h.as_bytes ( ) [ .. 8 ] ) ;
30
( u64 :: from_le_bytes( first) % 100 ) as i64
31
}
32
33
34
35
pub fn in_reach ( g : & Group, r : & Runner) -> bool {
36
g.owner.is_empty ( ) | | g.owner = = r.scope_owner
37
}
38
39
40
# [ derive ( Clone, Debug, Default ) ]
41
pub struct Levels {
42
pub policies : HashMap< ( String, String) , Policy> ,
43
pub groups : Vec< Group> ,
44
45
pub chosen : BTreeSet< ( i64 , i64 ) > ,
46
}
47
48
impl Levels {
49
pub async fn load ( pool : & Pool) -> sqlx:: Result< Levels> {
50
let policies = rdb:: policies( pool) .await? .into_iter ( ) .map ( | p | ( ( p.scope.clone ( ) , p.key.clone ( ) ) , p) ) .collect ( ) ;
51
Ok ( Levels { policies, groups: rdb:: groups( pool) .await? , chosen: rdb:: members( pool) .await? .into_iter ( ) .collect ( ) } )
52
}
53
54
pub fn member ( & self , g : & Group, r : & Runner) -> bool {
55
in_reach ( g, r) & & ( self .chosen.contains ( & ( g.id, r.id) ) | | g.percent.is_some_and ( | p | bucket ( r.id) < p) )
56
}
57
58
59
pub fn groups_of ( & self , r : & Runner) -> Vec< & Group> {
60
let mut gs: Vec< & Group> = self .groups.iter ( ) .filter ( | g | self .member ( g, r) ) .collect ( ) ;
61
gs.sort_by_key ( | g | ( g.owner.is_empty ( ) , g.name.clone ( ) ) ) ;
62
gs
63
}
64
65
fn get ( & self , scope : & str , key : & str ) -> Option< & Policy> {
66
self .policies.get ( & ( scope.to_string ( ) , key.to_string ( ) ) )
67
}
68
1ec29691 Andy McCall 9 days ago ↶
69
70
71
pub fn wave_of ( & self , r : & Runner) -> Option< i64 > {
72
self .groups_of ( r) .iter ( ) .filter_map ( | g | g.wave ) .min ( )
73
}
74
75
76
77
78
pub fn waves_before < 'a > ( & self , r : & Runner, all : & 'a [Runner]) -> Vec< ( i64 , Vec< & 'a Runner> ) > {
79
let mine = self .wave_of ( r) ;
80
let mut waves: Vec< i64 > =
81
self .groups.iter ( ) .filter ( | g | in_reach ( g, r) ) .filter_map ( | g | g.wave ) .filter ( | w | mine.is_none_or ( | m | * w < m) ) .collect ( ) ;
82
waves.sort ( ) ;
83
waves.dedup ( ) ;
84
waves
85
.into_iter ( )
86
.map ( | w | {
87
let members = all
88
.iter ( )
89
.filter ( | o | self .wave_of ( o) = = Some ( w) & & self .groups_of ( o) .iter ( ) .any ( | g | g.wave = = Some ( w) & & in_reach ( g, r) ) )
90
.collect ( ) ;
91
( w, members)
92
} )
93
.collect ( )
94
}
95
8501fd58 Andy McCall 11 days ago ↶
96
97
fn chain ( & self , r : & Runner) -> Vec< ( String, & Policy) > {
98
let mut out = vec! [ ] ;
99
if let Some ( p) = self .get ( " runner" , & r.id.to_string ( ) ) {
100
out.push ( ( " runner" .to_string ( ) , p) ) ;
101
}
102
for g in self .groups_of ( r) {
103
if let Some ( p) = self .get ( " group" , & g.id.to_string ( ) ) {
104
out.push ( ( format! ( " group {} " , g.name) , p) ) ;
105
}
106
}
107
if ! r.scope_owner.is_empty ( ) {
108
if let Some ( p) = self .get ( " owner" , & r.scope_owner) {
109
out.push ( ( format! ( " owner {} " , r.scope_owner) , p) ) ;
110
}
111
}
112
if let Some ( p) = self .get ( " site" , " " ) {
113
out.push ( ( " site" .to_string ( ) , p) ) ;
114
}
115
out
116
}
117
118
119
pub fn resolve ( & self , r : & Runner) -> UpdatePolicy {
120
let chain = self .chain ( r) ;
121
fn pick < T> ( chain : & [( String , &Policy ) ], f : impl Fn( &Policy ) -> Option< T> ) -> ( Option< T> , String) {
122
chain.iter ( ) .find_map ( | ( name , p ) | f ( p) .map ( | v | ( Some ( v) , name.clone ( ) ) ) ) .unwrap_or ( ( None , " default" .into ( ) ) )
123
}
124
let ( updates, u_from) = pick ( & chain, | p | p.updates ) ;
125
let ( channel, c_from ) = pick ( & chain, | p | p.channel.clone ( ) .filter ( | c | ! c.is_empty ( ) ) ) ;
126
let ( window, w_from) = pick ( & chain, | p | Some ( ( p.window_from? as u8 , p.window_to? as u8 ) ) ) ;
127
let ( dry_run, d_from) = pick ( & chain, | p | p.dry_run ) ;
128
UpdatePolicy {
129
updates: updates.unwrap_or ( false ) ,
130
channel: channel.unwrap_or_else ( | | " stable" .into ( ) ) ,
131
window,
132
dry_run: dry_run.unwrap_or ( false ) ,
133
from: vec! [ u_from, c_from , w_from, d_from] ,
134
}
135
}
136
}
137
138
139
pub fn in_window ( ( from , to ) : ( u8 , u8 ) , h : u8 ) -> bool {
140
if from < = to {
141
from < = h & & h < to
142
} else {
143
h > = from | | h < to
144
}
145
}
146
147
148
pub fn parse_window ( s : & str ) -> Result< Option< ( u8 , u8 ) > , String> {
149
let s = s.trim ( ) ;
150
if s.is_empty ( ) {
151
return Ok ( None ) ;
152
}
153
let ( a, b) = s.split_once ( ' -' ) .ok_or ( " a window is two hours, from-to, e.g. 02-05" ) ? ;
154
let hour = | x : & str | x.trim ( ) .trim_end_matches ( " :00" ) .parse:: < u8 > ( ) .ok ( ) .filter ( | h | * h < 24 ) ;
155
match ( hour ( a) , hour ( b) ) {
156
( Some ( a) , Some ( b) ) if a ! = b => Ok ( Some ( ( a, b) ) ) ,
157
_ => Err ( " a window is two different hours from 0 to 23, from-to, e.g. 02-05" .into ( ) ) ,
158
}
159
}
160
161
162
pub fn valid_channel ( c : & str ) -> bool {
163
matches! ( c, " stable" | " canary" ) | | ( ! c.is_empty ( ) & & c.split ( ' .' ) .all ( | p | ! p.is_empty ( ) & & p.bytes ( ) .all ( | b | b.is_ascii_digit ( ) ) ) )
164
}
165
166
167
168
pub fn version_of ( s : & str ) -> String {
169
s.split_whitespace ( ) .next ( ) .unwrap_or ( " " ) .trim_start_matches ( ' v' ) .to_string ( )
170
}
171
172
fn older ( a : & str , b : & str ) -> bool {
173
let parts = | v : & str | v.split ( ' .' ) .map ( | p | p.parse:: < u64 > ( ) .unwrap_or ( 0 ) ) .collect:: < Vec< _ > > ( ) ;
174
parts ( a) < parts ( b)
175
}
176
faf5246c Andy McCall 11 days ago ↶
177
178
179
pub fn rolled_back_version ( note : & str ) -> Option< String> {
180
let v = note.strip_prefix ( " rolled back from flow " ) ? .split_whitespace ( ) .next ( ) ? ;
181
v.split ( ' .' ) .all ( | p | ! p.is_empty ( ) & & p.bytes ( ) .all ( | b | b.is_ascii_digit ( ) ) ) .then ( | | v.to_string ( ) )
182
}
183
b6dfcb61 Andy McCall 11 days ago ↶
184
185
186
pub fn asset_for ( os : & str , arch : & str ) -> String {
187
format! ( " flow-{os} -{arch} {} " , if os = = " windows" { " .exe" } else { " " } )
1ec29691 Andy McCall 9 days ago ↶
188
}
189
190
191
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
192
pub enum WaveState {
193
194
195
196
Proved,
197
198
Soaking { runners: usize , on: usize , jobs: i64 } ,
199
200
Stopped { runner_id: i64 } ,
201
}
202
203
204
205
pub fn wave_state ( members : & [i64 ], soaks : & [Soak], soak_jobs : i64 , soak_ms : i64 , now : i64 ) -> WaveState {
206
let mine: Vec< & Soak> = soaks.iter ( ) .filter ( | s | members.contains ( & s.runner_id) & & s.jobs > 0 ) .collect ( ) ;
207
if let Some ( s) = mine.iter ( ) .find ( | s | s.errors > 0 ) {
208
return WaveState:: Stopped { runner_id: s.runner_id } ;
209
}
210
if members.is_empty ( ) {
211
return WaveState:: Proved;
212
}
213
let jobs = mine.iter ( ) .map ( | s | s.jobs ) .sum ( ) ;
214
let soaked = mine.iter ( ) .filter_map ( | s | s.first_ms ) .min ( ) .is_some_and ( | first | now - first > = soak_ms ) ;
215
if ! mine.is_empty ( ) & & ( soaked | | mine.iter ( ) .all ( | s | s.jobs > = soak_jobs ) ) {
216
WaveState:: Proved
217
} else {
218
WaveState:: Soaking { runners: members.len ( ) , on: mine.len ( ) , jobs }
219
}
220
}
221
222
223
224
async fn wave_gate (
225
pool : & Pool,
226
levels : & Levels,
227
r : & Runner,
228
version : & str ,
229
now : i64 ,
230
) -> sqlx:: Result< Result< Option< String> , ( String, String) > > {
231
let all = db:: runners:: list( pool) .await? ;
232
let waves = levels.waves_before ( r, & all) ;
233
if waves.is_empty ( ) {
234
return Ok ( Ok ( None ) ) ;
235
}
236
let setting = | v : Option< String> , d : i64 | v.and_then ( | s | s.trim ( ) .parse:: < i64 > ( ) .ok ( ) ) .filter ( | n | * n > 0 ) .unwrap_or ( d) ;
237
let soak_jobs = setting ( db:: accounts:: setting( pool, SETTING_SOAK_JOBS ) .await? , SOAK_JOBS ) ;
238
let soak_hours = setting ( db:: accounts:: setting( pool, SETTING_SOAK_HOURS ) .await? , SOAK_HOURS ) ;
239
let soaks = rdb:: soaks( pool, version) .await? ;
240
for ( w, members) in waves {
241
let ids: Vec< i64 > = members.iter ( ) .map ( | m | m.id ) .collect ( ) ;
242
match wave_state ( & ids, & soaks, soak_jobs, soak_hours * 3_600_000 , now) {
243
WaveState:: Proved => continue ,
244
WaveState:: Stopped { runner_id } => {
245
let name = members.iter ( ) .find ( | m | m.id = = runner_id ) .map ( | m | m.name.clone ( ) ) .unwrap_or_default ( ) ;
246
let job = rdb:: first_error( pool, version, runner_id) .await? .unwrap_or_else ( | | " a job" .into ( ) ) ;
247
return Ok ( Err ( ( name.clone ( ) , format! ( " wave {w} stopped it on {name} : {job} ended in error" ) ) ) ) ;
248
}
249
WaveState:: Soaking { runners, on, jobs } => {
250
return Ok ( Ok ( Some ( format! (
251
" flow {version} waits for wave {w} to prove it: {on} of {runners} runner{} on it, {jobs} job{} (each needs {soak_jobs} without an error, or {soak_hours} h)" ,
252
if runners = = 1 { " " } else { " s" } ,
253
if jobs = = 1 { " " } else { " s" } ,
254
) ) ) ) ;
255
}
256
}
257
}
258
Ok ( Ok ( None ) )
b6dfcb61 Andy McCall 11 days ago ↶
259
}
260
8501fd58 Andy McCall 11 days ago ↶
261
1ec29691 Andy McCall 9 days ago ↶
262
263
264
pub async fn offer (
265
pool : & Pool,
266
levels : & Levels,
267
r : & Runner,
268
policy : & UpdatePolicy,
269
hour : u8 ,
270
now : i64 ,
271
act : bool ,
272
) -> sqlx:: Result< ( Option< UpdateOffer> , String) > {
8501fd58 Andy McCall 11 days ago ↶
273
if ! policy.updates {
274
return Ok ( ( None , format! ( " updates are off ({} )" , policy.from[ 0 ] ) ) ) ;
275
}
276
let Some ( spec) = db:: accounts:: setting( pool, SETTING_REPO ) .await? .filter ( | s | ! s.trim ( ) .is_empty ( ) ) else {
277
return Ok ( ( None , " no runner releases repository is set (Admin › Runners › Updates)" .into ( ) ) ) ;
278
} ;
279
let Some ( ( owner, name) ) = spec.trim ( ) .split_once ( ' /' ) else {
280
return Ok ( ( None , format! ( " the runner releases repository {spec:?} is not owner/name" ) ) ) ;
281
} ;
282
let Some ( repo) = db:: repos:: get( pool, owner, name) .await? else {
283
return Ok ( ( None , format! ( " the runner releases repository {spec} does not exist" ) ) ) ;
284
} ;
285
let releases = db:: releases:: list( pool, repo.id) .await? ;
286
let release = match policy.channel.as_str ( ) {
287
" stable" => releases.iter ( ) .find ( | x | ! x.prerelease) ,
288
" canary" => releases.first ( ) ,
289
v => releases.iter ( ) .find ( | x | version_of ( & x.tag) = = v) ,
290
} ;
291
let Some ( release) = release else {
292
return Ok ( ( None , format! ( " {spec} has no release for the {} channel" , policy.channel) ) ) ;
293
} ;
294
let ( want, have) = ( version_of ( & release.tag) , version_of ( & r.version) ) ;
295
if want = = have {
296
return Ok ( ( None , format! ( " up to date: flow {have} ({} channel)" , policy.channel) ) ) ;
faf5246c Andy McCall 11 days ago ↶
297
}
298
if let Some ( h) = rdb:: held( pool, & want) .await? {
1ec29691 Andy McCall 9 days ago ↶
299
return Ok ( ( None , format! ( " flow {want} is held: {} ; lift the hold on Admin › Runners › Updates" , h.why) ) ) ;
8501fd58 Andy McCall 11 days ago ↶
300
}
301
let going_back = older ( & want, & have) ;
302
if going_back & & matches! ( policy.channel.as_str ( ) , " stable" | " canary" ) {
303
return Ok ( ( None , format! ( " runs flow {have} , newer than the {} channel's {want} " , policy.channel) ) ) ;
1ec29691 Andy McCall 9 days ago ↶
304
}
305
306
if ! going_back {
307
match wave_gate ( pool, levels, r, & want, now) .await? {
308
Ok ( None ) => { }
309
Ok ( Some ( why) ) => return Ok ( ( None , why) ) ,
310
Err ( ( runner, why) ) => {
311
if act {
312
rdb:: hold( pool, & want, & runner, & why) .await? ;
313
db:: accounts:: audit( pool, None , " runner.update.held" , & want, & why) .await.ok ( ) ;
314
}
315
return Ok ( ( None , format! ( " flow {want} is held: {why} ; lift the hold on Admin › Runners › Updates" ) ) ) ;
316
}
317
}
8501fd58 Andy McCall 11 days ago ↶
318
}
b6dfcb61 Andy McCall 11 days ago ↶
319
let file = asset_for ( & r.os, & r.arch) ;
8501fd58 Andy McCall 11 days ago ↶
320
let assets = db:: releases:: assets( pool, release.id) .await? ;
321
let Some ( a) = assets.iter ( ) .find ( | a | a.name = = file ) else {
322
return Ok ( ( None , format! ( " {spec} {} has no {file} " , release.tag) ) ) ;
323
} ;
324
if let Some ( w) = policy.window.filter ( | w | ! in_window ( * w, hour) ) {
325
return Ok ( ( None , format! ( " flow {want} waits for its window, {:02} :00–{:02} :00 UTC" , w.0 , w.1 ) ) ) ;
326
}
327
let note = format! (
328
" flow {want} ({} channel){} " ,
329
policy.channel,
330
if policy.dry_run { " : dry run, check it and say what would happen" } else { " " }
331
) ;
332
Ok ( (
333
Some ( UpdateOffer {
334
version: want,
335
tag: release.tag.clone ( ) ,
336
asset: a.name.clone ( ) ,
337
url: format! ( " /{owner} /{name} /asset/{} /{} " , a.id, a.name) ,
338
blake3: a.hash.clone ( ) ,
339
size: a.size as u64 ,
340
older: going_back,
341
} ) ,
342
note,
343
) )
344
}
345
346
# [ cfg ( test ) ]
347
mod tests {
348
use super :: * ;
349
350
fn runner ( id : i64 , owner : & str ) -> Runner {
351
Runner { id, scope_owner: owner.into ( ) , version: " 0.3.1 (build)" .into ( ) , .. Runner:: default( ) }
352
}
353
354
# [ test ]
355
fn a_smaller_share_sits_inside_a_larger_one_and_is_about_its_size ( ) {
356
let ten: Vec< i64 > = ( 1 .. = 1000 ) .filter ( | id | bucket ( * id) < 10 ) .collect ( ) ;
357
let fifty: Vec< i64 > = ( 1 .. = 1000 ) .filter ( | id | bucket ( * id) < 50 ) .collect ( ) ;
358
assert! ( ten.iter ( ) .all ( | id | fifty.contains ( id) ) ) ;
359
assert! ( ( 60 .. = 140 ) .contains ( & ten.len ( ) ) , " {}" , ten.len ( ) ) ;
360
assert_eq! ( bucket ( 7 ) , bucket ( 7 ) , " stable" ) ;
361
}
362
363
# [ test ]
364
fn the_nearest_level_wins_each_setting ( ) {
365
let mut l = Levels:: default( ) ;
366
let pol = | scope : & str , key : & str , f : & dyn Fn( & mut Policy) | {
367
let mut p = Policy { scope: scope.into ( ) , key: key.into ( ) , .. Policy:: default( ) } ;
368
f ( & mut p) ;
369
( ( scope.to_string ( ) , key.to_string ( ) ) , p)
370
} ;
371
l.policies.extend ( [
372
pol ( " site" , " " , & | p| {
373
p.updates = Some ( false ) ;
374
p.channel = Some ( " stable" .into ( ) ) ;
375
} ) ,
376
pol ( " owner" , " acme" , & | p| p.updates = Some ( true ) ) ,
377
pol ( " group" , " 1" , & | p| p.channel = Some ( " canary" .into ( ) ) ) ,
378
pol ( " runner" , " 3" , & | p| p.updates = Some ( false ) ) ,
379
] ) ;
1ec29691 Andy McCall 9 days ago ↶
380
l.groups.push ( Group { id: 1 , owner: " acme" .into ( ) , name: " canary" .into ( ) , percent: None , created_ms: 0 , wave: None } ) ;
8501fd58 Andy McCall 11 days ago ↶
381
l.chosen.insert ( ( 1 , 2 ) ) ;
382
l.chosen.insert ( ( 1 , 9 ) ) ;
383
384
let site_runner = l.resolve ( & runner ( 1 , " " ) ) ;
385
assert! ( ! site_runner.updates, " off at the site" ) ;
386
assert_eq! ( site_runner.from, [ " site" , " site" , " default" , " default" ] ) ;
387
388
let canary = l.resolve ( & runner ( 2 , " acme" ) ) ;
389
assert! ( canary.updates & & canary.channel = = " canary" ) ;
390
assert_eq! ( canary.from[ .. 2 ] , [ " owner acme" .to_string ( ) , " group canary" .to_string ( ) ] ) ;
391
392
let held = l.resolve ( & runner ( 3 , " acme" ) ) ;
393
assert! ( ! held.updates, " the runner's own setting wins" ) ;
394
assert_eq! ( held.channel, " stable" ) ;
395
396
assert! ( l.groups_of ( & runner ( 9 , " other" ) ) .is_empty ( ) , " an owner's group holds only its own runners" ) ;
397
assert! ( ! Levels:: default( ) .resolve ( & runner ( 4 , " " ) ) .updates, " off unless chosen" ) ;
398
}
399
400
# [ test ]
401
fn a_percentage_group_takes_its_share_of_the_runners_in_reach ( ) {
402
let mut l = Levels:: default( ) ;
1ec29691 Andy McCall 9 days ago ↶
403
l.groups.push ( Group { id: 5 , owner: String :: new( ) , name: " farm 20%" .into ( ) , percent: Some ( 20 ) , created_ms: 0 , wave: None } ) ;
8501fd58 Andy McCall 11 days ago ↶
404
let n = ( 1 .. = 500 ) .filter ( | id | ! l.groups_of ( & runner ( * id, " " ) ) .is_empty ( ) ) .count ( ) ;
405
assert! ( ( 60 .. = 140 ) .contains ( & n) , " {n}" ) ;
1ec29691 Andy McCall 9 days ago ↶
406
}
407
408
# [ test ]
409
fn a_wave_proves_a_version_by_its_jobs_or_its_time_and_an_error_stops_it ( ) {
410
let soak = | runner_id , jobs , errors , first_ms | Soak { runner_id, jobs, errors, first_ms: Some ( first_ms) } ;
411
let hour = 3_600_000 ;
412
413
assert_eq! ( wave_state ( & [ 1 , 2 ] , & [ ] , 5 , 24 * hour, 0 ) , WaveState:: Soaking { runners: 2 , on: 0 , jobs: 0 } ) ;
414
415
let s = [ soak ( 1 , 3 , 0 , 0 ) , soak ( 9 , 50 , 0 , 0 ) ] ;
416
assert_eq! ( wave_state ( & [ 1 , 2 ] , & s, 5 , 24 * hour, hour) , WaveState:: Soaking { runners: 2 , on: 1 , jobs: 3 } ) ;
417
418
assert_eq! ( wave_state ( & [ 1 , 2 ] , & [ soak ( 1 , 5 , 0 , 0 ) ] , 5 , 24 * hour, hour) , WaveState:: Proved) ;
419
420
assert_eq! ( wave_state ( & [ 1 , 2 ] , & [ soak ( 1 , 1 , 0 , 0 ) ] , 5 , 24 * hour, 24 * hour) , WaveState:: Proved) ;
421
422
assert_eq! (
423
wave_state ( & [ 1 , 2 ] , & [ soak ( 1 , 40 , 0 , 0 ) , soak ( 2 , 2 , 1 , 0 ) ] , 5 , 24 * hour, 48 * hour) ,
424
WaveState:: Stopped { runner_id: 2 }
425
) ;
426
427
assert_eq! ( wave_state ( & [ 1 ] , & [ soak ( 1 , 5 , 0 , 0 ) , soak ( 2 , 2 , 1 , 0 ) ] , 5 , 24 * hour, hour) , WaveState:: Proved) ;
428
429
assert_eq! ( wave_state ( & [ ] , & [ ] , 5 , 24 * hour, 0 ) , WaveState:: Proved) ;
430
}
431
432
# [ test ]
433
fn a_runner_waits_for_the_waves_before_its_own ( ) {
434
let mut l = Levels:: default( ) ;
435
let group = | id , name : & str , wave | Group { id, owner: String :: new( ) , name: name.into ( ) , percent: None , created_ms: 0 , wave } ;
436
l.groups.extend ( [ group ( 1 , " first" , Some ( 1 ) ) , group ( 2 , " second" , Some ( 2 ) ) , group ( 3 , " watchers" , None ) ] ) ;
437
l.chosen.extend ( [ ( 1 , 1 ) , ( 2 , 2 ) , ( 2 , 1 ) , ( 3 , 3 ) ] ) ;
438
let all: Vec< Runner> = ( 1 .. = 4 ) .map ( | id | runner ( id, " " ) ) .collect ( ) ;
439
let ids = | ws : Vec< ( i64 , Vec< & Runner> ) > | {
440
ws.into_iter ( ) .map ( | ( w , rs ) | ( w, rs.iter ( ) .map ( | r | r.id ) .collect:: < Vec< _ > > ( ) ) ) .collect:: < Vec< _ > > ( )
441
} ;
442
assert_eq! ( l.wave_of ( & all[ 0 ] ) , Some ( 1 ) , " its first wave" ) ;
443
assert! ( l.waves_before ( & all[ 0 ] , & all) .is_empty ( ) , " wave 1 goes first" ) ;
444
assert_eq! ( ids ( l.waves_before ( & all[ 1 ] , & all) ) , vec! [ ( 1 , vec! [ 1 ] ) ] ) ;
445
assert_eq! ( l.wave_of ( & all[ 2 ] ) , None , " a group with no wave" ) ;
446
assert_eq! ( ids ( l.waves_before ( & all[ 2 ] , & all) ) , vec! [ ( 1 , vec! [ 1 ] ) , ( 2 , vec! [ 2 ] ) ] , " in no wave: after every wave" ) ;
447
assert_eq! ( ids ( l.waves_before ( & all[ 3 ] , & all) ) , vec! [ ( 1 , vec! [ 1 ] ) , ( 2 , vec! [ 2 ] ) ] ) ;
448
449
l.groups.push ( Group { id: 4 , owner: " acme" .into ( ) , name: " acme first" .into ( ) , percent: None , created_ms: 0 , wave: Some ( 1 ) } ) ;
450
l.chosen.insert ( ( 4 , 5 ) ) ;
451
let mut all = all;
452
all.push ( runner ( 5 , " acme" ) ) ;
453
all.push ( runner ( 6 , " acme" ) ) ;
454
assert_eq! ( ids ( l.waves_before ( & all[ 5 ] , & all) ) , vec! [ ( 1 , vec! [ 1 , 5 ] ) , ( 2 , vec! [ 2 ] ) ] ) ;
455
assert_eq! ( ids ( l.waves_before ( & all[ 3 ] , & all) ) , vec! [ ( 1 , vec! [ 1 ] ) , ( 2 , vec! [ 2 ] ) ] , " not another owner's" ) ;
8501fd58 Andy McCall 11 days ago ↶
456
}
457
458
# [ test ]
459
fn windows_and_channels_read_as_written ( ) {
460
assert_eq! ( parse_window ( " 02-05" ) , Ok ( Some ( ( 2 , 5 ) ) ) ) ;
461
assert_eq! ( parse_window ( " 22:00-04:00 " ) , Ok ( Some ( ( 22 , 4 ) ) ) ) ;
462
assert_eq! ( parse_window ( " " ) , Ok ( None ) ) ;
463
assert! ( parse_window ( " 5" ) .is_err ( ) & & parse_window ( " 3-3" ) .is_err ( ) & & parse_window ( " 1-24" ) .is_err ( ) ) ;
464
assert! ( in_window ( ( 2 , 5 ) , 2 ) & & in_window ( ( 2 , 5 ) , 4 ) & & ! in_window ( ( 2 , 5 ) , 5 ) ) ;
465
assert! ( in_window ( ( 22 , 4 ) , 23 ) & & in_window ( ( 22 , 4 ) , 1 ) & & ! in_window ( ( 22 , 4 ) , 12 ) ) ;
466
assert! ( valid_channel ( " stable" ) & & valid_channel ( " canary" ) & & valid_channel ( " 0.3.4" ) ) ;
467
assert! ( ! valid_channel ( " latest" ) & & ! valid_channel ( " 0.3." ) & & ! valid_channel ( " " ) ) ;
468
assert_eq! ( version_of ( " v0.3.2" ) , " 0.3.2" ) ;
469
assert_eq! ( version_of ( " 0.3.1 (local build)" ) , " 0.3.1" ) ;
b6dfcb61 Andy McCall 11 days ago ↶
470
assert_eq! ( asset_for ( " linux" , " riscv64" ) , " flow-linux-riscv64" ) ;
471
assert_eq! ( asset_for ( " windows" , " x86_64" ) , " flow-windows-x86_64.exe" ) ;
faf5246c Andy McCall 11 days ago ↶
472
assert_eq! (
473
rolled_back_version ( " rolled back from flow 0.4.0 to 0.3.1: 3 starts without reaching the hub" ) .as_deref ( ) ,
474
Some ( " 0.4.0" )
475
) ;
476
assert_eq! ( rolled_back_version ( " runs flow 0.4.0 (was 0.3.1); proven" ) , None ) ;
8501fd58 Andy McCall 11 days ago ↶
477
}
478
}