42fa9554 Andy McCall 1 month ago ↶
1
2
3
4
5
use axum:: body:: Body;
6
use axum:: http:: { header, Request, StatusCode} ;
7
use http_body_util:: BodyExt;
8
use oxy_core:: protocol:: { open_remote, RemoteOptions} ;
9
use oxy_core:: repo:: Repo;
10
use oxygenhub:: config:: Config;
11
use oxygenhub:: repos:: RepoName;
12
use oxygenhub:: { db, Shared} ;
13
use std:: path:: Path;
14
use tower:: ServiceExt;
15
16
const ROOTFS : & str = " 36fc4e3238044e424219a2b9002bf61bbe1805fec14d52cf37e17fae77c9ff16" ;
17
18
fn hermetic ( ) {
19
static ONCE : std:: sync:: Once = std:: sync:: Once:: new( ) ;
20
static DIR : std:: sync:: OnceLock< tempfile:: TempDir> = std:: sync:: OnceLock:: new( ) ;
21
ONCE .call_once ( | | {
22
let d = tempfile:: tempdir( ) .unwrap ( ) ;
23
std:: env:: set_var( " OXY_HOME" , d.path ( ) ) ;
24
DIR .set ( d) .ok ( ) ;
25
} ) ;
26
}
27
28
fn write ( root : & Path, rel : & str , content : & str ) {
29
let p = root.join ( rel) ;
30
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
31
std:: fs:: write( p, content) .unwrap ( ) ;
32
}
33
34
struct Hub {
35
app : Shared,
36
base : String,
37
_dir : tempfile:: TempDir,
38
}
39
40
async fn hub ( ) -> Hub {
41
hermetic ( ) ;
42
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
43
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
44
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
4d2e44d7 Andy McCall 1 month ago ↶
45
config.flow.runner_tokens = vec! [ " runner-secret" .into ( ) , " runner-two" .into ( ) , " runner-three" .into ( ) ] ;
42fa9554 Andy McCall 1 month ago ↶
46
config.flow.repos = vec! [ " andy/*" .into ( ) ] ;
10708ab8 Andy McCall 1 month ago ↶
47
config.registration.mode = " open" .into ( ) ;
42fa9554 Andy McCall 1 month ago ↶
48
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
49
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
50
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
51
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
52
let router = oxygenhub:: router( app.clone ( ) ) ;
53
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
54
Hub { app, base: format! ( " http://127.0.0.1:{port} " ) , _dir: dir }
55
}
56
57
async fn call ( app : & Shared, method : & str , path : & str , bearer : Option< & str > , json : Option< String> ) -> ( StatusCode, String) {
58
let mut req = Request:: builder( ) .method ( method) .uri ( path) ;
59
if let Some ( b) = bearer {
60
req = req.header ( header:: AUTHORIZATION , format! ( " Bearer {b} " ) ) ;
61
}
62
let body = match json {
63
Some ( j) => {
64
req = req.header ( header:: CONTENT_TYPE , " application/json" ) ;
65
Body:: from( j)
66
}
67
None => Body:: empty( ) ,
68
} ;
69
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req.body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
70
let status = res.status ( ) ;
71
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
72
( status, String :: from_utf8_lossy( & bytes) .into_owned ( ) )
73
}
74
736f6813 Andy McCall 1 month ago ↶
75
76
async fn head ( app : & Shared, path : & str ) -> ( StatusCode, String, String) {
77
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
78
let h = | k : header:: HeaderName| res.headers ( ) .get ( k) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or ( " " ) .to_string ( ) ;
79
( res.status ( ) , h ( header:: LOCATION ) , h ( header:: CONTENT_TYPE ) )
80
}
81
42fa9554 Andy McCall 1 month ago ↶
82
async fn claim ( app : & Shared) -> Option< serde_json:: Value> {
83
let ( st, body) =
84
call ( app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( r # "{"runner":"test-1","runtimes":["microvm"]}"# .into ( ) ) ) .await;
85
match st {
86
StatusCode:: NO_CONTENT => None ,
87
StatusCode:: OK => Some ( serde_json:: from_str( & body) .unwrap ( ) ) ,
88
other => panic! ( " claim: {other} {body}" ) ,
89
}
90
}
91
92
fn receipt ( job : & str , pipeline_hash : & str , exit : u32 ) -> serde_json:: Value {
93
serde_json:: json! ( {
94
" job_name" : job, " pipeline_hash" : pipeline_hash, " scope_tree_hash" : " 11" .repeat ( 32 ) ,
95
" inputs" : [ ] , " environment" : " 22" .repeat ( 32 ) , " outputs" : [ ] , " exit_code" : exit, " duration_ms" : 640
96
} )
97
}
98
99
async fn finish ( app : & Shared, id : i64 , body : serde_json:: Value) -> StatusCode {
100
call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /finish" ) , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await.0
101
}
102
103
async fn create ( app : & Shared, owner : & str , name : & str ) {
104
app.repos.create ( & RepoName:: parse( & format! ( " {owner} /{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
105
db:: repos:: create( & app.db, owner, name, " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
106
}
107
108
fn client ( url : & str , work : & Path) -> Repo {
109
let mut c = Repo:: init( work) .unwrap ( ) ;
110
c.config.set ( " remote.origin.url" , url) .unwrap ( ) ;
111
c.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
112
c.save_config ( ) .unwrap ( ) ;
113
c
114
}
115
116
fn pipeline ( extra : & str ) -> String {
117
format! (
118
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n depends-on \" build\" \n exec \" sh\" \" -eu\" \" test.sh\" \n }} \n job \" lint\" {{ \n sandbox \" alpine\" \n exec \" true\" \n }} \n {extra} "
119
)
120
}
121
122
async fn runs_of ( app : & Shared, owner : & str , name : & str ) -> Vec< db:: flow:: Run> {
123
let row = db:: repos:: get( & app.db, owner, name) .await.unwrap ( ) .unwrap ( ) ;
124
for _ in 0 .. 100 {
125
let r = db:: flow:: runs( & app.db, row.id, 10 , None ) .await.unwrap ( ) ;
126
if ! r.is_empty ( ) {
127
return r;
128
}
129
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
130
}
131
vec! [ ]
132
}
133
90326c96 Andy McCall 1 month ago ↶
134
135
async fn checks_when ( app : & Shared, repo_id : i64 , state : & str , n : usize ) -> Vec< db:: hooks:: Check> {
136
for _ in 0 .. 100 {
137
let c = db:: hooks:: checks_for_state( & app.db, repo_id, state) .await.unwrap ( ) ;
138
if c.len ( ) > = n {
139
return c;
140
}
141
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
142
}
143
db:: hooks:: checks_for_state( & app.db, repo_id, state) .await.unwrap ( )
144
}
145
42fa9554 Andy McCall 1 month ago ↶
146
# [ tokio ::test ( flavor = " multi_thread" ) ]
147
async fn a_push_runs_the_pipeline_through_a_runner ( ) {
148
let h = hub ( ) .await;
149
create ( & h.app, " andy" , " demo" ) .await;
150
create ( & h.app, " andy" , " secret" ) .await;
151
create ( & h.app, " other" , " plain" ) .await;
152
let url = format! ( " {} /andy/demo" , h.base) ;
153
let work = tempfile:: tempdir( ) .unwrap ( ) ;
154
let mut c = client ( & url, work.path ( ) ) ;
155
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
156
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
157
c.snapshot ( " test" ) .unwrap ( ) ;
158
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
159
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
160
161
162
let runs = runs_of ( & h.app, " andy" , " demo" ) .await;
163
assert_eq! ( runs.len ( ) , 1 ) ;
164
let run = & runs[ 0 ] ;
165
assert_eq! ( ( run.bookmark.as_str ( ) , run.status.as_str ( ) , run.actor.as_str ( ) ) , ( " main" , " queued" , " site token" ) ) ;
166
assert_eq! ( run.pipeline_hash.len ( ) , 64 ) ;
167
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
168
assert_eq! ( jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.depth) ) .collect:: < Vec< _ > > ( ) , [ ( " build" , 0 ) , ( " lint" , 0 ) , ( " test" , 1 ) ] ) ;
90326c96 Andy McCall 1 month ago ↶
169
170
let checks = checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
42fa9554 Andy McCall 1 month ago ↶
171
assert_eq! (
172
checks.iter ( ) .map ( | c | ( c.name.as_str ( ) , c.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
173
[ ( " flow/build" , " queued" ) , ( " flow/lint" , " queued" ) , ( " flow/test" , " queued" ) ]
174
) ;
175
176
177
assert_eq! (
178
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " site" ) , Some ( r # "{"runner":"x","runtimes":["microvm"]}"# .into ( ) ) ) .await.0 ,
179
StatusCode:: UNAUTHORIZED
180
) ;
181
assert_eq! (
182
call ( & h.app, " POST" , " /api/v1/flow/claim" , None , Some ( r # "{"runner":"x","runtimes":["microvm"]}"# .into ( ) ) ) .await.0 ,
183
StatusCode:: UNAUTHORIZED
184
) ;
185
186
let ( st, _ ) =
187
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( r # "{"runner":"x","runtimes":["wasm"]}"# .into ( ) ) ) .await;
188
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
189
190
191
let a = claim ( & h.app) .await.unwrap ( ) ;
192
let b = claim ( & h.app) .await.unwrap ( ) ;
193
assert_eq! ( ( a[ " job" ] [ " name" ] .as_str ( ) , b[ " job" ] [ " name" ] .as_str ( ) ) , ( Some ( " build" ) , Some ( " lint" ) ) ) ;
194
assert! ( claim ( & h.app) .await.is_none ( ) , " test waits for build" ) ;
195
assert_eq! ( a[ " repo" ] , " andy/demo" ) ;
196
assert_eq! ( a[ " clone_url" ] , url.as_str ( ) ) ;
197
assert_eq! ( a[ " sandbox" ] [ " rootfs" ] , ROOTFS ) ;
198
assert_eq! ( a[ " job" ] [ " exec" ] , serde_json:: json! ( [ " make" ] ) ) ;
199
assert_eq! ( a[ " timeout_s" ] , 3600 ) ;
9a065bc0 Andy McCall 1 month ago ↶
200
201
assert_eq! (
202
( a[ " state" ] .as_str ( ) , a[ " bookmark" ] .as_str ( ) , a[ " run_number" ] .as_i64 ( ) ) ,
203
( Some ( run.state.as_str ( ) ) , Some ( " main" ) , Some ( run.number) )
204
) ;
42fa9554 Andy McCall 1 month ago ↶
205
let heads: Vec< String> = a[ " heads" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | v | v.as_str ( ) .unwrap ( ) .to_string ( ) ) .collect ( ) ;
206
assert_eq! ( heads.join ( " +" ) , run.heads) ;
207
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
208
assert_eq! ( checks.iter ( ) .find ( | c | c.name = = " flow/build" ) .unwrap ( ) .status, " running" ) ;
209
210
211
let token = a[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
212
assert! ( token.starts_with ( " flowjob_" ) ) ;
e8fa5369 Andy McCall 24 days ago ↶
213
let opts = RemoteOptions { token: Some ( token.clone ( ) ) , .. Default :: default( ) } ;
42fa9554 Andy McCall 1 month ago ↶
214
open_remote ( & url, & opts) .unwrap ( ) .refs ( ) .unwrap ( ) ;
215
assert! ( open_remote ( & format! ( " {} /andy/secret" , h.base) , & opts) .unwrap ( ) .refs ( ) .is_err ( ) , " another repository" ) ;
e8fa5369 Andy McCall 24 days ago ↶
216
let bad = RemoteOptions { token: Some ( " flowjob_00" .into ( ) ) , .. Default :: default( ) } ;
42fa9554 Andy McCall 1 month ago ↶
217
assert! ( open_remote ( & url, & bad) .unwrap ( ) .refs ( ) .is_err ( ) , " an unknown job token" ) ;
218
219
c.config.set ( " remote.origin.token" , & token) .unwrap ( ) ;
220
c.save_config ( ) .unwrap ( ) ;
221
write ( work.path ( ) , " more.txt" , " x\n " ) ;
222
c.snapshot ( " test" ) .unwrap ( ) ;
223
c.commit ( " More" , & [ ] ) .unwrap ( ) ;
224
let pushed = c.push ( " origin" , false ) ;
225
assert! ( pushed.is_err ( ) | | pushed.unwrap ( ) .iter ( ) .all ( | r | ! r.ok) , " a job token cannot push" ) ;
226
c.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
227
c.save_config ( ) .unwrap ( ) ;
228
229
230
let a_id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
231
let b_id = b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
232
let log = serde_json:: json! ( { " records" : [
233
{ " t" : 5 , " stream" : " flow" , " text" : " booting" } ,
234
{ " t" : 120 , " stream" : " out" , " text" : " cc -o demo demo.c" } ,
235
{ " t" : 130 , " stream" : " weird" , " text" : " x" .repeat ( 20_000 ) } ,
236
] } ) ;
237
assert_eq! (
238
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{a_id} /log" ) , Some ( " runner-secret" ) , Some ( log.to_string ( ) ) ) .await.0 ,
239
StatusCode:: NO_CONTENT
240
) ;
241
let lines = db:: flow:: log( & h.app.db, a_id, 0 , 100 ) .await.unwrap ( ) ;
242
assert_eq! ( lines.len ( ) , 3 ) ;
243
assert_eq! ( ( lines[ 2 ] .stream.as_str ( ) , lines[ 2 ] .text.len ( ) ) , ( " console" , 16 * 1024 ) , " streams are known ones, text is capped" ) ;
244
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) ;
245
assert_eq! (
246
finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " lint" , ph, 0 ) } ) ) .await,
247
StatusCode:: BAD_REQUEST ,
248
" a receipt for another job"
249
) ;
250
assert_eq! (
251
finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " build" , ph, 0 ) } ) ) .await,
252
StatusCode:: NO_CONTENT
253
) ;
254
assert_eq! ( finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 } ) ) .await, StatusCode:: CONFLICT , " only once" ) ;
255
assert! ( open_remote ( & url, & opts) .unwrap ( ) .refs ( ) .is_err ( ) , " the token ends with its job" ) ;
256
assert_eq! (
257
finish ( & h.app, b_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : receipt ( " lint" , ph, 0 ) } ) ) .await,
258
StatusCode:: NO_CONTENT
259
) ;
260
let t = claim ( & h.app) .await.unwrap ( ) ;
261
assert_eq! ( t[ " job" ] [ " name" ] , " test" ) ;
262
let t_id = t[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
263
assert_eq! (
264
finish ( & h.app, t_id, serde_json:: json! ( { " exit_code" : 3 , " duration_ms" : 500 , " receipt" : receipt ( " test" , ph, 3 ) } ) ) .await,
265
StatusCode:: NO_CONTENT
266
) ;
267
268
let run = db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) ;
269
assert_eq! ( run.status, " failed" ) ;
270
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
271
let build = jobs.iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) ;
272
assert_eq! ( ( build.status.as_str ( ) , build.exit_code, build.receipt_hash.len ( ) ) , ( " passed" , Some ( 0 ) , 64 ) ) ;
273
assert_eq! ( jobs.iter ( ) .find ( | j | j.name = = " test" ) .unwrap ( ) .status, " failed" ) ;
274
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
275
assert_eq! (
276
checks.iter ( ) .map ( | c | ( c.name.as_str ( ) , c.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
277
[ ( " flow/build" , " passed" ) , ( " flow/lint" , " passed" ) , ( " flow/test" , " failed" ) ]
278
) ;
a0d4bbfb Andy McCall 1 month ago ↶
279
assert! ( checks[ 2 ] .summary.contains ( " exit 3" ) & & checks[ 2 ] .url.ends_with ( & format! ( " /andy/demo/flow/{} " , run.number) ) ) ;
42fa9554 Andy McCall 1 month ago ↶
280
281
282
let ( st, list) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
283
assert_eq! ( st, StatusCode:: OK ) ;
a0d4bbfb Andy McCall 1 month ago ↶
284
assert! ( list.contains ( & format! ( " /andy/demo/flow/{} " , run.number) ) & & list.contains ( " st-failed" ) , " {list}" ) ;
285
let ( st, page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
286
assert_eq! ( st, StatusCode:: OK ) ;
287
assert! ( page.contains ( " <svg class=\" flow-graph\" " ) , " graph" ) ;
288
assert_eq! ( page.matches ( " data-job-node=" ) .count ( ) , 3 ) ;
289
assert_eq! ( page.matches ( " <path class=\" edge" ) .count ( ) , 1 , " one dependency edge" ) ;
290
assert! ( page.contains ( " cc -o demo demo.c" ) , " log shown" ) ;
291
assert! ( ! page.contains ( " data-flow-live" ) , " a finished run does not poll" ) ;
a0d4bbfb Andy McCall 1 month ago ↶
292
let ( st, live) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} /live?after={a_id} :1" , run.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
293
assert_eq! ( st, StatusCode:: OK ) ;
294
let live: serde_json:: Value = serde_json:: from_str( & live) .unwrap ( ) ;
295
assert_eq! ( live[ " status" ] , " failed" ) ;
296
let build_live = live[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | j | j[ " name" ] = = " build" ) .unwrap ( ) .clone ( ) ;
297
assert_eq! ( build_live[ " lines" ] .as_array ( ) .unwrap ( ) .len ( ) , 2 , " lines after seq 1" ) ;
298
assert_eq! ( call ( & h.app, " GET" , " /andy/demo/flow/999" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
299
300
301
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
302
write ( work.path ( ) , " test.sh" , " echo changed\n " ) ;
303
c.snapshot ( " test" ) .unwrap ( ) ;
304
c.commit ( " Change the test" , & [ ] ) .unwrap ( ) ;
305
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
306
let mut second = None ;
307
for _ in 0 .. 100 {
308
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
309
if r.len ( ) = = 2 {
310
second = Some ( r[ 0 ] .clone ( ) ) ;
311
break ;
312
}
313
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
314
}
315
let second = second.expect ( " a second run" ) ;
316
let x = claim ( & h.app) .await.unwrap ( ) ;
317
let y = claim ( & h.app) .await.unwrap ( ) ;
318
let ( build_id, lint_id) = if x[ " job" ] [ " name" ] = = " build" {
319
( x[ " job_id" ] .as_i64 ( ) .unwrap ( ) , y[ " job_id" ] .as_i64 ( ) .unwrap ( ) )
320
} else {
321
( y[ " job_id" ] .as_i64 ( ) .unwrap ( ) , x[ " job_id" ] .as_i64 ( ) .unwrap ( ) )
322
} ;
323
assert_eq! (
324
finish ( & h.app, build_id, serde_json:: json! ( { " exit_code" : null, " duration_ms" : 10 , " error" : " rootfs is not on this runner" } ) )
325
.await,
326
StatusCode:: NO_CONTENT
327
) ;
328
assert_eq! ( finish ( & h.app, lint_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 } ) ) .await, StatusCode:: NO_CONTENT ) ;
329
assert! ( claim ( & h.app) .await.is_none ( ) , " test was skipped" ) ;
330
let jobs = db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) ;
331
assert_eq! (
332
jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
333
[ ( " build" , " error" ) , ( " lint" , " passed" ) , ( " test" , " skipped" ) ]
334
) ;
335
assert_eq! ( db:: flow:: run_by_id( & h.app.db, second.id) .await.unwrap ( ) .unwrap ( ) .status, " failed" ) ;
336
337
338
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " job \" broken\" {\n sandbox \" missing\" \n exec \" x\" \n }\n " ) ) ;
339
c.snapshot ( " test" ) .unwrap ( ) ;
340
c.commit ( " Break the pipeline" , & [ ] ) .unwrap ( ) ;
341
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
342
let mut third = None ;
343
for _ in 0 .. 100 {
344
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
345
if r.len ( ) = = 3 {
346
third = Some ( r[ 0 ] .clone ( ) ) ;
347
break ;
348
}
349
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
350
}
351
let third = third.expect ( " a third run" ) ;
352
assert_eq! ( third.status, " error" ) ;
353
assert! ( third.error.contains ( " line 18" ) & & third.error.contains ( " \" missing\" " ) , " {}" , third.error) ;
a0d4bbfb Andy McCall 1 month ago ↶
354
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , third.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
355
assert! ( page.contains ( " The pipeline is not valid" ) ) ;
356
736f6813 Andy McCall 1 month ago ↶
357
358
write ( work.path ( ) , " README.md" , " # Demo\n \n [](.flow/run)\n \n \n " ) ;
359
std:: fs:: create_dir_all( work.path ( ) .join ( " img" ) ) .unwrap ( ) ;
360
std:: fs:: write( work.path ( ) .join ( " img/logo.png" ) , b " \x89 PNG\r \n \x1a \n \0 \0 \0 \r IHDR\0 \0 \0 \x01 \0 \0 \0 \x01 \x08 \x06 \0 \0 \0 \x1f \x15 \xc4 \x89 " )
361
.unwrap ( ) ;
362
c.snapshot ( " test" ) .unwrap ( ) ;
363
c.commit ( " A README with a stamp" , & [ ] ) .unwrap ( ) ;
364
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
365
let mut fourth = None ;
366
for _ in 0 .. 100 {
367
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
368
if r.len ( ) = = 4 {
369
fourth = Some ( r[ 0 ] .clone ( ) ) ;
370
break ;
371
}
372
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
373
}
374
let fourth = fourth.expect ( " a fourth run" ) ;
375
let ( _ , overview) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
376
assert! ( overview.contains ( " src=\" /andy/demo/raw/main/.flow/stamp\" " ) , " {overview}" ) ;
377
assert! ( overview.contains ( " href=\" /andy/demo/file/main/.flow/run\" " ) ) ;
378
assert! ( overview.contains ( " src=\" /andy/demo/raw/main/img/logo.png\" " ) , " images use the raw route" ) ;
379
let ( st, _ , ct) = head ( & h.app, " /andy/demo/raw/main/img/logo.png" ) .await;
380
assert_eq! ( ( st, ct.as_str ( ) ) , ( StatusCode:: OK , " image/png" ) ) ;
381
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/raw/main/.flow/stamp" ) .await;
382
assert! ( st.is_redirection ( ) & & loc = = " /andy/demo/flow/stamp/main" , " {st} {loc}" ) ;
383
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/flow/stamp/main" ) .await;
384
assert! ( st.is_redirection ( ) & & loc = = format! ( " /andy/demo/flow/stamp/{} " , fourth.heads) , " a bookmark's stamp names its state: {loc}" ) ;
385
let ( st, _ , ct) = head ( & h.app, & loc) .await;
386
assert_eq! ( ( st, ct.as_str ( ) ) , ( StatusCode:: OK , " image/svg+xml; charset=utf-8" ) ) ;
387
let ( _ , svg) = call ( & h.app, " GET" , & loc, None , None ) .await;
388
assert! ( svg.starts_with ( " <svg" ) & & svg.contains ( " error ·" ) & & svg.contains ( & fourth.heads[ .. 12 ] ) , " {svg}" ) ;
389
let ( _ , svg1) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/stamp/{} " , run.heads) , None , None ) .await;
390
assert! ( svg1.contains ( " failed ·" ) & & svg1.contains ( & format! ( " run #{} failed" , run.id) ) , " an older state keeps its own result: {svg1}" ) ;
391
assert! ( svg1.contains ( & build.receipt_hash) , " receipt hashes in the metadata" ) ;
392
assert_eq! ( svg1.matches ( " text-anchor=\" middle\" " ) .count ( ) , 3 , " one glyph per job" ) ;
393
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/file/main/.flow/run" ) .await;
394
assert! ( st.is_redirection ( ) & & loc = = " /andy/demo/flow/at/main" , " {loc}" ) ;
395
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/flow/at/main" ) .await;
396
assert! ( st.is_redirection ( ) & & loc = = format! ( " /andy/demo/flow/{} " , fourth.id) , " {loc}" ) ;
397
42fa9554 Andy McCall 1 month ago ↶
398
399
let work2 = tempfile:: tempdir( ) .unwrap ( ) ;
400
let c2 = client ( & format! ( " {} /other/plain" , h.base) , work2.path ( ) ) ;
401
write ( work2.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
402
c2.snapshot ( " test" ) .unwrap ( ) ;
403
c2.commit ( " Pipeline" , & [ ] ) .unwrap ( ) ;
404
assert! ( c2.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
405
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 500 ) ) .await;
406
let row = db:: repos:: get( & h.app.db, " other" , " plain" ) .await.unwrap ( ) .unwrap ( ) ;
407
assert! ( db:: flow:: runs( & h.app.db, row.id, 10 , None ) .await.unwrap ( ) .is_empty ( ) ) ;
408
let ( _ , page) = call ( & h.app, " GET" , " /other/plain/flow" , None , None ) .await;
409
assert! ( page.contains ( " not enabled for this repository" ) ) ;
736f6813 Andy McCall 1 month ago ↶
410
let ( _ , loc, _ ) = head ( & h.app, " /other/plain/flow/stamp/main" ) .await;
411
let ( _ , svg) = call ( & h.app, " GET" , & loc, None , None ) .await;
412
assert! ( svg.contains ( " no run ·" ) , " {svg}" ) ;
413
let ( _ , loc, _ ) = head ( & h.app, " /other/plain/flow/at/main" ) .await;
414
assert_eq! ( loc, " /other/plain/flow" ) ;
42fa9554 Andy McCall 1 month ago ↶
415
}
fd3db644 Andy McCall 1 month ago ↶
416
417
# [ tokio ::test ( flavor = " multi_thread" ) ]
418
async fn claims_carry_pinned_subrepos_and_the_job_token_reads_only_those ( ) {
419
let h = hub ( ) .await;
25091e5c Andy McCall 1 month ago ↶
420
for n in [ " base" , " lib" , " vault" , " other" , " app" ] {
fd3db644 Andy McCall 1 month ago ↶
421
create ( & h.app, " andy" , n) .await;
422
}
423
let push = | name : & str , file : & str | {
424
let work = tempfile:: tempdir( ) .unwrap ( ) ;
425
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
426
write ( work.path ( ) , file, " x\n " ) ;
427
c.snapshot ( " t" ) .unwrap ( ) ;
428
c.commit ( " first" , & [ ] ) .unwrap ( ) ;
429
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
430
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
431
( work, heads)
432
} ;
25091e5c Andy McCall 1 month ago ↶
433
let ( _b, base_heads) = push ( " base" , " base.rs" ) ;
434
435
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
436
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
437
lib.submodule_add ( & format! ( " {} /andy/base" , h.base) , " deps/base" , Some ( " main" ) ) .unwrap ( ) ;
438
write ( lib_work.path ( ) , " lib.rs" , " x\n " ) ;
439
lib.snapshot ( " t" ) .unwrap ( ) ;
440
lib.commit ( " lib on base" , & [ ] ) .unwrap ( ) ;
441
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
442
let lib_heads = lib.bookmark_heads ( ) .unwrap ( ) ;
fd3db644 Andy McCall 1 month ago ↶
443
let ( _v, _ ) = push ( " vault" , " secret.rs" ) ;
444
let ( _o, _ ) = push ( " other" , " o.rs" ) ;
445
446
447
let work = tempfile:: tempdir( ) .unwrap ( ) ;
448
let app = client ( & format! ( " {} /andy/app" , h.base) , work.path ( ) ) ;
449
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
450
2bbe88a8 Andy McCall 1 month ago ↶
451
452
fd3db644 Andy McCall 1 month ago ↶
453
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'vault'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
454
let bare_work = tempfile:: tempdir( ) .unwrap ( ) ;
455
let mut bare = Repo:: init( bare_work.path ( ) ) .unwrap ( ) ;
456
bare.config.set ( " remote.origin.url" , & format! ( " {} /andy/bare" , h.base) ) .unwrap ( ) ;
457
bare.save_config ( ) .unwrap ( ) ;
458
assert! ( bare.submodule_add ( & format! ( " {} /andy/vault" , h.base) , " deps/vault" , Some ( " main" ) ) .is_err ( ) , " not without a token" ) ;
370035a4 Andy McCall 1 month ago ↶
459
app.submodule_add ( & format! ( " {} /andy/vault" , h.base) , " deps/vault" , Some ( " main" ) ) .unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
460
let nested = Repo:: open( & work.path ( ) .join ( " deps/vault" ) ) .unwrap ( ) ;
2bbe88a8 Andy McCall 1 month ago ↶
461
assert! ( nested.config.remote_tokens.is_empty ( ) , " no token is copied into a subrepo" ) ;
462
nested.fetch ( " origin" ) .unwrap ( ) ;
370035a4 Andy McCall 1 month ago ↶
463
fd3db644 Andy McCall 1 month ago ↶
464
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
465
write ( work.path ( ) , " Makefile" , " all:\n " ) ;
466
app.snapshot ( " t" ) .unwrap ( ) ;
467
app.commit ( " app with deps" , & [ ] ) .unwrap ( ) ;
468
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
469
assert! ( ! runs_of ( & h.app, " andy" , " app" ) .await.is_empty ( ) ) ;
470
471
let job = claim ( & h.app) .await.unwrap ( ) ;
472
let subs = job[ " subrepos" ] .as_array ( ) .cloned ( ) .unwrap_or_default ( ) ;
25091e5c Andy McCall 1 month ago ↶
473
assert_eq! ( subs.len ( ) , 2 , " lib and what lib pins; vault is private and the pusher has no account: {subs:?}" ) ;
474
assert_eq! ( subs[ 1 ] [ " path" ] , " deps/lib/deps/base" , " nested under its parent, after it" ) ;
475
assert_eq! ( subs[ 1 ] [ " heads" ] , serde_json:: json! ( base_heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) ) ) ;
370035a4 Andy McCall 1 month ago ↶
476
let _ = std:: fs:: remove_dir_all( work.path ( ) .join ( " deps/vault2" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
477
assert_eq! ( subs[ 0 ] [ " path" ] , " deps/lib" ) ;
478
assert_eq! ( subs[ 0 ] [ " repo" ] , " andy/lib" ) ;
479
assert_eq! ( subs[ 0 ] [ " heads" ] , serde_json:: json! ( lib_heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) ) ) ;
480
assert! ( subs[ 0 ] [ " clone_url" ] .as_str ( ) .unwrap ( ) .ends_with ( " /andy/lib" ) ) ;
afd0be38 Andy McCall 1 month ago ↶
481
482
let job_id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
483
let lines = db:: flow:: log( & h.app.db, job_id, 0 , 100 ) .await.unwrap ( ) ;
484
assert! (
485
lines.iter ( ) .any ( | l | l.stream = = " flow" & & l.text.contains ( " subrepo deps/vault (andy/vault) is left out" ) ) ,
486
" {:?}" ,
487
lines.iter ( ) .map ( | l | & l.text) .collect:: < Vec< _ > > ( )
488
) ;
fd3db644 Andy McCall 1 month ago ↶
489
490
491
let token = job[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
492
let read = | name : & str | {
493
let opts = RemoteOptions { token: Some ( token.clone ( ) ) , .. Default :: default( ) } ;
494
open_remote ( & format! ( " {} /andy/{name} " , h.base) , & opts) .and_then ( | r | r.refs ( ) ) .is_ok ( )
495
} ;
25091e5c Andy McCall 1 month ago ↶
496
assert! ( read ( " app" ) & & read ( " lib" ) & & read ( " base" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
497
assert! ( ! read ( " vault" ) , " not pinned for this pusher" ) ;
498
assert! ( ! read ( " other" ) , " public, but not the job's to read" ) ;
499
}
941abdd6 Andy McCall 1 month ago ↶
500
501
# [ tokio ::test ( flavor = " multi_thread" , worker_threads = 8 ) ]
502
async fn concurrent_log_appends_never_fail_on_a_locked_database ( ) {
503
let h = hub ( ) .await;
504
create ( & h.app, " andy" , " demo" ) .await;
505
let work = tempfile:: tempdir( ) .unwrap ( ) ;
506
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
507
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
508
write ( work.path ( ) , " Makefile" , " all:\n " ) ;
509
c.snapshot ( " t" ) .unwrap ( ) ;
510
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
511
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
512
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
513
let job = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .remove ( 0 ) ;
514
515
516
let mut tasks = Vec :: new( ) ;
517
for w in 0 .. 16 {
518
let pool = h.app.db.clone ( ) ;
519
tasks.push ( tokio:: spawn( async move {
520
let records: Vec< flow_core:: api:: Record> =
521
( 0 .. 20 ) .map ( | i | flow_core:: api:: Record { t: i, stream: " out" .into ( ) , text: format! ( " writer {w} line {i} " ) } ) .collect ( ) ;
522
db:: flow:: append_log( & pool, job.id, & records, 100_000 ) .await
523
} ) ) ;
524
}
525
for t in tasks {
526
assert_eq! ( t.await.unwrap ( ) .unwrap ( ) , 20 , " every append succeeds" ) ;
527
}
528
let lines = db:: flow:: log( & h.app.db, job.id, 0 , 1_000 ) .await.unwrap ( ) ;
529
assert_eq! ( lines.len ( ) , 320 ) ;
530
let seqs: std:: collections:: BTreeSet< i64 > = lines.iter ( ) .map ( | l | l.seq ) .collect ( ) ;
531
assert_eq! ( seqs.len ( ) , 320 , " sequence numbers are unique" ) ;
532
}
9008bf6c Andy McCall 1 month ago ↶
533
534
535
# [ tokio ::test ( flavor = " multi_thread" ) ]
536
async fn a_repository_setting_turns_flow_on_or_off ( ) {
537
let h = hub ( ) .await;
538
create ( & h.app, " other" , " plain" ) .await;
539
create ( & h.app, " andy" , " quiet" ) .await;
a0d4bbfb Andy McCall 1 month ago ↶
540
541
create ( & h.app, " andy" , " first" ) .await;
542
{
543
let work = tempfile:: tempdir( ) .unwrap ( ) ;
544
let c = client ( & format! ( " {} /andy/first" , h.base) , work.path ( ) ) ;
545
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
546
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
547
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
548
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
549
assert_eq! ( runs_of ( & h.app, " andy" , " first" ) .await.len ( ) , 1 ) ;
550
}
9008bf6c Andy McCall 1 month ago ↶
551
let row = db:: repos:: get( & h.app.db, " other" , " plain" ) .await.unwrap ( ) .unwrap ( ) ;
552
db:: repos:: set_flow( & h.app.db, row.id, " on" ) .await.unwrap ( ) ;
553
let quiet = db:: repos:: get( & h.app.db, " andy" , " quiet" ) .await.unwrap ( ) .unwrap ( ) ;
554
db:: repos:: set_flow( & h.app.db, quiet.id, " off" ) .await.unwrap ( ) ;
555
for ( owner, name) in [ ( " other" , " plain" ) , ( " andy" , " quiet" ) ] {
556
let work = tempfile:: tempdir( ) .unwrap ( ) ;
557
let c = client ( & format! ( " {} /{owner} /{name} " , h.base) , work.path ( ) ) ;
558
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
559
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
560
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
561
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
562
}
a0d4bbfb Andy McCall 1 month ago ↶
563
let plain = runs_of ( & h.app, " other" , " plain" ) .await;
564
assert_eq! ( plain.len ( ) , 1 , " turned on" ) ;
55673c65 Andy McCall 22 days ago ↶
565
566
bf3d0754 Andy McCall 1 month ago ↶
567
let a = claim ( & h.app) .await.unwrap ( ) ;
568
assert_eq! ( a[ " repo" ] , " andy/first" ) ;
569
let ( _ , page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
55673c65 Andy McCall 22 days ago ↶
570
assert! ( page.contains ( " Waiting for a runner: 1st in line" ) & & page.contains ( " Running now: andy/first #1" ) , " {page}" ) ;
bf3d0754 Andy McCall 1 month ago ↶
571
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/other/plain/flow/runs/1" , None , None ) .await;
55673c65 Andy McCall 22 days ago ↶
572
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " queue_position" ] , 1 ) ;
bf3d0754 Andy McCall 1 month ago ↶
573
let ( _ , list) = call ( & h.app, " GET" , " /other/plain/flow" , None , None ) .await;
55673c65 Andy McCall 22 days ago ↶
574
assert! ( list.contains ( " queued, 1st in line" ) , " {list}" ) ;
a0d4bbfb Andy McCall 1 month ago ↶
575
576
577
assert_eq! ( plain[ 0 ] .number, 1 ) ;
90326c96 Andy McCall 1 month ago ↶
578
let checks = checks_when ( & h.app, row.id, & plain[ 0 ] .state, 3 ) .await;
a0d4bbfb Andy McCall 1 month ago ↶
579
assert! ( checks.iter ( ) .all ( | c | c.url.ends_with ( " /other/plain/flow/1" ) ) , " {checks:?}" ) ;
580
let ( st, page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
581
assert! ( st = = StatusCode:: OK & & page.contains ( " Run #1" ) , " {st}" ) ;
9008bf6c Andy McCall 1 month ago ↶
582
assert! ( runs_of ( & h.app, " andy" , " quiet" ) .await.is_empty ( ) , " turned off despite andy/*" ) ;
583
}
1edac4b6 Andy McCall 1 month ago ↶
584
585
586
587
# [ tokio ::test ( flavor = " multi_thread" ) ]
588
async fn runs_start_without_a_push_and_finished_runs_re_run ( ) {
589
let h = hub ( ) .await;
590
591
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
592
create ( & h.app, " andy" , " demo" ) .await;
593
let url = format! ( " {} /andy/demo" , h.base) ;
594
let work = tempfile:: tempdir( ) .unwrap ( ) ;
595
let c = client ( & url, work.path ( ) ) ;
596
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
597
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
598
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
599
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
600
let first = runs_of ( & h.app, " andy" , " demo" ) .await;
601
assert_eq! ( ( first.len ( ) , first[ 0 ] .trigger.as_str ( ) ) , ( 1 , " push" ) ) ;
602
603
604
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
605
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
606
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
607
assert_eq! ( v[ " number" ] , 2 ) ;
608
assert! ( v[ " url" ] .as_str ( ) .unwrap ( ) .ends_with ( " /andy/demo/flow/2" ) ) ;
609
let second = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
610
assert_eq! ( ( second.trigger.as_str ( ) , second.actor.as_str ( ) , second.state.as_str ( ) ) , ( " manual" , " andy" , first[ 0 ] .state.as_str ( ) ) ) ;
611
assert_eq! ( db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) .len ( ) , 3 ) ;
612
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/2" , None , None ) .await;
613
assert! ( page.contains ( " started by andy" ) , " the run page says what started it" ) ;
614
4f692533 Andy McCall 1 month ago ↶
615
616
let head = first[ 0 ] .head_list ( ) [ 0 ] .to_string ( ) ;
617
let ( st, body) =
618
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{head} "}} "# ) ) ) .await;
619
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
620
let on_change = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 3 ) .await.unwrap ( ) .unwrap ( ) ;
621
assert_eq! (
622
( on_change.bookmark.as_str ( ) , on_change.heads.as_str ( ) , on_change.state.as_str ( ) ) ,
623
( " " , head.as_str ( ) , first[ 0 ] .state.as_str ( ) )
624
) ;
625
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/3" , None , None ) .await;
626
assert! ( page.contains ( " title=\" Run on one change, not a bookmark\" >change</span>" ) , " run page" ) ;
627
let ( st, body) =
628
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{} "}} "# , " ab" .repeat ( 32 ) ) ) )
629
.await;
630
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " no change" ) , " {st} {body}" ) ;
631
632
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
633
assert! ( page.contains ( " Sign in with write access to run the pipeline." ) & & ! page.contains ( " primary-btn" ) ) ;
634
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/bookmarks" , None , None ) .await;
635
assert! ( ! page.contains ( " /flow/start" ) , " no run buttons for anonymous viewers" ) ;
636
1edac4b6 Andy McCall 1 month ago ↶
637
638
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
639
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " still queued" ) , " {st} {body}" ) ;
640
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"bookmark":"nope"}"# .into ( ) ) ) .await;
641
assert_eq! ( st, StatusCode:: CONFLICT ) ;
642
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/99/rerun" , Some ( " site" ) , None ) .await;
643
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
644
645
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , None , Some ( " {}" .into ( ) ) ) .await;
646
assert_ne! ( st, StatusCode:: CREATED ) ;
647
648
649
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
650
for r in [ & first[ 0 ] , & second] {
651
for j in db:: flow:: jobs( & h.app.db, r.id) .await.unwrap ( ) {
3b1bc27f Andy McCall 22 days ago
652
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE id = $1" ) .bind ( j.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
653
}
3b1bc27f Andy McCall 22 days ago
654
sqlx:: query( " UPDATE flow_runs SET status = 'passed', finished_ms = 1 WHERE id = $1" ) .bind ( r.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
655
}
656
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
657
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
658
let third = db:: flow:: run( & h.app.db, row.id, 4 ) .await.unwrap ( ) .unwrap ( ) ;
348f8c4b Andy McCall 1 month ago ↶
659
assert_eq! (
660
( third.trigger_label ( ) .as_str ( ) , third.status.as_str ( ) , third.state.as_str ( ) ) ,
661
( " rerun:1" , " queued" , first[ 0 ] .state.as_str ( ) )
662
) ;
c889911b Andy McCall 1 month ago ↶
663
assert_eq! ( third.trigger, " rerun:1:push" , " a rerun keeps the event it repeats" ) ;
1edac4b6 Andy McCall 1 month ago ↶
664
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, & third.state) .await.unwrap ( ) ;
665
assert! (
4f692533 Andy McCall 1 month ago ↶
666
checks.iter ( ) .all ( | c | c.status = = " queued" & & c.url.ends_with ( " /flow/4" ) ) ,
1edac4b6 Andy McCall 1 month ago ↶
667
" the re-run's checks replace the old ones: {checks:?}"
668
) ;
4f692533 Andy McCall 1 month ago ↶
669
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/4" , None , None ) .await;
1edac4b6 Andy McCall 1 month ago ↶
670
assert! ( page.contains ( " re-run of #1 by andy" ) ) ;
671
assert! ( ! page.contains ( " RE-RUN" ) , " anonymous viewers get no button" ) ;
672
0650d94e Andy McCall 27 days ago ↶
673
674
675
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"jobs":["test"]}"# .into ( ) ) ) .await;
676
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
677
let n = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " number" ] .as_i64 ( ) .unwrap ( ) ;
678
let only = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, n) .await.unwrap ( ) .unwrap ( ) ;
679
let mut names: Vec< String> = db:: flow:: jobs( & h.app.db, only.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
680
names.sort ( ) ;
681
assert_eq! ( names, [ " build" , " test" ] , " test and the job it depends on, not lint" ) ;
682
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"jobs":["deploy"]}"# .into ( ) ) ) .await;
683
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not in the pipeline" ) & & body.contains ( " build, lint, test" ) , " {st} {body}" ) ;
684
1edac4b6 Andy McCall 1 month ago ↶
685
686
db:: repos:: set_flow( & h.app.db, row.id, " off" ) .await.unwrap ( ) ;
687
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
688
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not enabled" ) , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
689
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
690
assert! ( page.contains ( " Flow is off for this repository" ) , " and the Flow tab says so" ) ;
90326c96 Andy McCall 1 month ago ↶
691
}
692
693
694
695
# [ tokio ::test ( flavor = " multi_thread" ) ]
696
async fn runs_jobs_and_logs_are_in_the_api ( ) {
697
let h = hub ( ) .await;
698
create ( & h.app, " andy" , " demo" ) .await;
699
let url = format! ( " {} /andy/demo" , h.base) ;
700
let work = tempfile:: tempdir( ) .unwrap ( ) ;
701
let c = client ( & url, work.path ( ) ) ;
702
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
703
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
704
let head = c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
705
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
706
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
707
708
709
let a = claim ( & h.app) .await.unwrap ( ) ;
710
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
711
let recs =
712
serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " stdout" , " text" : " one" } , { " t" : 3 , " stream" : " stderr" , " text" : " three" } ] } ) ;
713
let ( st, _ ) = call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( recs.to_string ( ) ) ) .await;
714
assert! ( st.is_success ( ) , " {st}" ) ;
715
716
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs" , None , None ) .await;
717
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
718
let runs: Vec< hub_api:: FlowRun> = serde_json:: from_str( & body) .unwrap ( ) ;
719
assert_eq! ( runs.len ( ) , 1 ) ;
720
assert_eq! ( ( runs[ 0 ] .number, runs[ 0 ] .state.as_str ( ) , runs[ 0 ] .trigger.as_str ( ) ) , ( 1 , run.state.as_str ( ) , " push" ) ) ;
721
assert! ( runs[ 0 ] .url.ends_with ( " /andy/demo/flow/1" ) & & runs[ 0 ] .jobs.is_empty ( ) ) ;
722
723
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , None , None ) .await;
724
let one: hub_api:: FlowRun = serde_json:: from_str( & body) .unwrap ( ) ;
725
let names: Vec< ( & str , & str ) > = one.jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.status.as_str ( ) ) ) .collect ( ) ;
726
assert_eq! ( names.len ( ) , 3 ) ;
727
let running = one.jobs.iter ( ) .find ( | j | j.status = = " running" ) .unwrap ( ) ;
728
assert_eq! ( running.name, a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) ;
729
assert! ( one.jobs.iter ( ) .any ( | j | j.name = = " test" & & j.depends_on = = [ " build" ] ) ) ;
730
731
let job = running.name.clone ( ) ;
732
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log" ) , None , None ) .await;
733
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
734
let lines: Vec< hub_api:: FlowLogLine> = serde_json:: from_str( & body) .unwrap ( ) ;
735
assert! ( lines.iter ( ) .any ( | l | l.text = = " three" ) , " {body}" ) ;
736
let last = lines.last ( ) .unwrap ( ) .seq;
737
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log?after={last} " ) , None , None ) .await;
738
assert_eq! ( body, " []" , " nothing after the last line" ) ;
739
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
740
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1/jobs/nope/log" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
741
742
743
let by = | key : String| {
744
let app = h.app.clone ( ) ;
745
async move {
746
let ( st, body) = call ( & app, " GET" , & format! ( " /api/v1/repos/andy/demo/checks/{key} " ) , None , None ) .await;
747
assert_eq! ( st, StatusCode:: OK , " {key}: {body}" ) ;
748
serde_json:: from_str:: < Vec< hub_api:: Check> > ( & body) .unwrap ( ) .len ( )
749
}
750
} ;
751
checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
752
assert_eq! ( by ( run.state.clone ( ) ) .await, 3 ) ;
753
assert_eq! ( by ( head.to_hex ( ) ) .await, 3 , " a single head names its state" ) ;
754
assert_eq! ( by ( " main" .into ( ) ) .await, 3 ) ;
755
let ( st, _ ) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/checks/no-such-bookmark" , None , None ) .await;
756
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
757
758
759
let ( st, body) = call ( & h.app, " GET" , " /api/v1/users/andy/repos" , None , None ) .await;
760
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
761
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
762
assert_eq! ( v[ " status" ] , 404 , " {body}" ) ;
1edac4b6 Andy McCall 1 month ago ↶
763
}
cc19697f Andy McCall 1 month ago ↶
764
765
async fn get_bytes ( app : & Shared, path : & str ) -> ( StatusCode, Vec< u8 > ) {
766
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
767
let status = res.status ( ) ;
768
( status, res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) )
769
}
770
771
# [ tokio ::test ( flavor = " multi_thread" ) ]
772
async fn signed_receipts_are_checked_kept_and_shown ( ) {
773
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectRef, ObjectType} ;
774
use oxy_core:: identity:: SecretKey;
775
use oxy_core:: objects:: StateObject;
776
use oxy_core:: receipt:: SignedReceipt;
777
let h = hub ( ) .await;
778
create ( & h.app, " andy" , " demo" ) .await;
779
let url = format! ( " {} /andy/demo" , h.base) ;
780
let work = tempfile:: tempdir( ) .unwrap ( ) ;
781
let c = client ( & url, work.path ( ) ) ;
782
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
783
c.snapshot ( " test" ) .unwrap ( ) ;
784
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
785
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
786
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
787
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
788
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads.clone ( ) ) .encode ( ) ) ;
789
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
790
let sk = SecretKey:: generate( ) ;
791
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
792
let sign = | r : & serde_json:: Value, state : ObjectRef, sk : & SecretKey, id : & oxy_core:: identity:: Identity| {
793
let r: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
794
let run_url = format! ( " {url} /flow/{} " , run.number) ;
795
hex:: encode( SignedReceipt:: sign( r.to_oxy ( ) , repo_id, state, & run_url, id, sk) .encode ( ) )
796
} ;
797
798
let a = claim ( & h.app) .await.unwrap ( ) ;
799
let b = claim ( & h.app) .await.unwrap ( ) ;
800
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
801
let ( a_id, b_id) = ( a[ " job_id" ] .as_i64 ( ) .unwrap ( ) , b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
802
803
let rb = receipt ( " build" , & ph, 0 ) ;
804
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : rb, " signed_receipt" : sign ( & rb, state, & sk, & runner) } ) ;
805
assert_eq! ( finish ( & h.app, a_id, body) .await, StatusCode:: NO_CONTENT ) ;
806
807
let other = SecretKey:: generate( ) ;
808
let rl = receipt ( " lint" , & ph, 0 ) ;
809
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : rl,
810
" signed_receipt" : sign ( & rl, state, & other, & other.identity ( " mallory" , " " , 1 ) ) } ) ;
811
assert_eq! ( finish ( & h.app, b_id, body) .await, StatusCode:: NO_CONTENT ) ;
812
813
let t = claim ( & h.app) .await.unwrap ( ) ;
814
let rt = receipt ( " test" , & ph, 0 ) ;
815
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 500 , " receipt" : rt,
816
" signed_receipt" : sign ( & rt, ObjectRef( [ 9 ; 32 ] ) , & sk, & runner) } ) ;
817
assert_eq! ( finish ( & h.app, t[ " job_id" ] .as_i64 ( ) .unwrap ( ) , body) .await, StatusCode:: NO_CONTENT ) ;
818
819
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
820
let job = | n : & str | jobs.iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) .clone ( ) ;
821
assert_eq! ( job ( " build" ) .status, " passed" ) ;
822
assert_eq! ( ( job ( " build" ) .receipt_attest.len ( ) , job ( " build" ) .receipt_signer.as_str ( ) ) , ( 64 , " test-1 (OxygenFlow runner)" ) ) ;
823
assert_eq! ( job ( " lint" ) .status, " passed" , " the job's result stands" ) ;
824
assert! ( job ( " lint" ) .receipt_attest.is_empty ( ) ) ;
825
assert! ( job ( " lint" ) .receipt_note.contains ( " not the key this runner signs with" ) , " {}" , job ( " lint" ) .receipt_note) ;
826
assert! ( job ( " test" ) .receipt_note.contains ( " another state" ) , " {}" , job ( " test" ) .receipt_note) ;
827
828
829
830
let kept = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .receipts ( & heads) .unwrap ( ) ;
831
assert_eq! ( kept.len ( ) , 1 ) ;
832
let attest = job ( " build" ) .receipt_attest;
833
let ( st, bytes) = get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{attest} " ) ) .await;
834
assert_eq! ( st, StatusCode:: OK ) ;
835
let sr = SignedReceipt:: decode( & bytes) .unwrap ( ) ;
836
sr.verify ( ) .unwrap ( ) ;
837
assert_eq! ( ( sr.state ( ) , sr.receipt.job_name.as_str ( ) ) , ( state, " build" ) ) ;
838
assert_eq! ( get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{} " , " 0" .repeat ( 64 ) ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
839
840
841
842
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
843
assert! ( page.contains ( " signed receipt</a> by test-1" ) , " run page" ) ;
844
assert! ( page.contains ( " receipt not kept" ) , " run page" ) ;
845
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/change/{} " , heads[ 0 ] .to_hex ( ) ) , None , None ) .await;
846
assert! ( page.contains ( " <dt>Receipts</dt>" ) & & page.contains ( & format! ( " /andy/demo/receipts/{attest} " ) ) , " change page" ) ;
847
}
a3c8288b Andy McCall 1 month ago ↶
848
849
async fn upload ( app : & Shared, job : i64 , name : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
850
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /artifacts/{name} " ) )
851
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
852
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
853
.body ( Body:: from( bytes.to_vec ( ) ) )
854
.unwrap ( ) ;
855
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
856
let status = res.status ( ) ;
857
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
858
}
859
860
# [ tokio ::test ( flavor = " multi_thread" ) ]
861
async fn artifacts_are_kept_named_by_the_receipt_and_served ( ) {
862
let h = hub ( ) .await;
863
create ( & h.app, " andy" , " demo" ) .await;
864
let url = format! ( " {} /andy/demo" , h.base) ;
865
let work = tempfile:: tempdir( ) .unwrap ( ) ;
866
let c = client ( & url, work.path ( ) ) ;
867
write (
868
work.path ( ) ,
869
" .flow/pipeline.kdl" ,
870
& format! (
871
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n }} \n }} \n "
872
) ,
873
) ;
874
c.snapshot ( " test" ) .unwrap ( ) ;
875
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
876
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
877
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
878
let a = claim ( & h.app) .await.unwrap ( ) ;
879
assert_eq! ( a[ " job" ] [ " outputs" ] , serde_json:: json! ( [ { " name" : " tool" , " path" : " /src/out/tool" } ] ) , " the runner is told" ) ;
880
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
881
882
let body: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 253 ) as u8 ) .collect ( ) ;
883
let ( st, text) = upload ( & h.app, id, " other" , & body) .await;
884
assert_eq! ( st, StatusCode:: BAD_REQUEST , " an undeclared artifact: {text}" ) ;
885
let ( st, text) = upload ( & h.app, id, " tool" , & body) .await;
886
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
887
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
888
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( & body) .to_hex ( ) , " the same blob the runner computes" ) ;
889
890
891
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
892
assert_eq! (
893
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
894
StatusCode:: BAD_REQUEST
895
) ;
896
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
897
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r } ) ) .await, StatusCode:: NO_CONTENT ) ;
898
899
900
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
901
let link = format! ( " /andy/demo/flow/{} /artifacts/build/tool" , run.number) ;
902
assert! ( page.contains ( & link) , " run page lists the artifact" ) ;
903
let ( st, got) = get_bytes ( & h.app, & link) .await;
904
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) ) ;
905
assert_eq! ( get_bytes ( & h.app, & format! ( " /andy/demo/flow/{} /artifacts/build/none" , run.number) ) .await.0 , StatusCode:: NOT_FOUND ) ;
10708ab8 Andy McCall 1 month ago ↶
906
45977501 Andy McCall 27 days ago ↶
907
908
909
{
910
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
911
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
912
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
913
hosted.save_config ( ) .unwrap ( ) ;
914
}
915
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
916
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
917
let ( st, got) = get_bytes ( & h.app, & link) .await;
918
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) , " the artifact survives gc" ) ;
919
10708ab8 Andy McCall 1 month ago ↶
920
921
922
let form = | f : String| {
923
Request:: post( " /register" ) .header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) .body ( Body:: from( f) ) .unwrap ( )
924
} ;
925
let res = oxygenhub:: router( h.app.clone ( ) )
926
.oneshot ( form ( " username=andy&email=a%40example.com&password=correct-horse-battery" .into ( ) ) )
927
.await
928
.unwrap ( ) ;
929
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
930
let with_cookie = | method : & str , path : & str , body : Option< String> | {
931
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
932
if body.is_some ( ) {
933
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
934
}
935
req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( )
936
} ;
937
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
938
db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
939
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( with_cookie ( " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None ) ) .await.unwrap ( ) ;
940
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
941
let attach = format! ( " /andy/demo/flow/{} /artifacts/build/tool/attach" , run.number) ;
942
assert! ( page.contains ( & attach) , " a writer is offered the release" ) ;
943
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
944
let csrf = page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
945
let release_id = db:: releases:: list( & h.app.db, row.id) .await.unwrap ( ) [ 0 ] .id;
946
let res = oxygenhub:: router( h.app.clone ( ) )
947
.oneshot ( with_cookie ( " POST" , & attach, Some ( format! ( " _csrf={csrf} &release={release_id} " ) ) ) )
948
.await
949
.unwrap ( ) ;
950
assert_eq! ( res.status ( ) , StatusCode:: SEE_OTHER ) ;
951
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/releases/tag/v1" , None , None ) .await;
952
assert! (
953
page.contains ( " >tool</a>" ) & & page.contains ( & format! ( " built by <a href=\" /andy/demo/flow/{0} \" >run #{0} </a> build" , run.number) ) ,
954
" {page}"
955
) ;
956
let ( _ , api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases" , None , None ) .await;
957
let api: serde_json:: Value = serde_json:: from_str( & api) .unwrap ( ) ;
958
let asset = & api[ 0 ] [ " assets" ] [ 0 ] ;
959
assert_eq! (
960
( asset[ " name" ] .as_str ( ) , asset[ " built_by" ] [ " run" ] .as_i64 ( ) , asset[ " built_by" ] [ " job" ] .as_str ( ) ) ,
961
( Some ( " tool" ) , Some ( run.number) , Some ( " build" ) )
962
) ;
963
assert! ( asset[ " built_by" ] [ " receipt" ] .is_null ( ) , " that receipt was not signed" ) ;
a3c8288b Andy McCall 1 month ago ↶
964
}
df940d75 Andy McCall 1 month ago ↶
965
d29070fa Andy McCall 1 month ago ↶
966
967
a36bab6b Andy McCall 27 days ago ↶
968
async fn upload_report_file ( app : & Shared, job : i64 , name : & str , path : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
969
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /reports/{name} /files" ) )
970
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
971
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
972
.header ( " X-Flow-Report-Path" , path)
973
.body ( Body:: from( bytes.to_vec ( ) ) )
974
.unwrap ( ) ;
975
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
976
let status = res.status ( ) ;
977
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
978
}
979
980
981
982
983
# [ tokio ::test ( flavor = " multi_thread" ) ]
984
async fn reports_are_kept_file_by_file_and_indexed ( ) {
985
let h = hub ( ) .await;
986
create ( & h.app, " andy" , " demo" ) .await;
987
let url = format! ( " {} /andy/demo" , h.base) ;
988
let work = tempfile:: tempdir( ) .unwrap ( ) ;
989
let c = client ( & url, work.path ( ) ) ;
990
write (
991
work.path ( ) ,
992
" .flow/pipeline.kdl" ,
993
& format! (
994
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" perf\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n report \" frame-timing\" {{ \n path \" /src/out/report\" \n when \" always\" \n }} \n }} \n }} \n "
995
) ,
996
) ;
997
c.snapshot ( " test" ) .unwrap ( ) ;
998
c.commit ( " Measure frames" , & [ ] ) .unwrap ( ) ;
999
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1000
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1001
let a = claim ( & h.app) .await.unwrap ( ) ;
1002
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1003
1004
let toml = b " kind = \" performance\" \n title = \" Frame timing\" \n summary = \" Median 16.4 ms\" \n \n [metrics]\n median_ms = { value = 16.4, unit = \" ms\" , better = \" lower\" , fail_above = 20 }\n \n [context]\n target_fps = 30\n " ;
1005
let page = b " <p>frames</p>" .to_vec ( ) ;
1006
let data: Vec< u8 > = ( 0 .. 100_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
c8df33a6 Andy McCall 27 days ago ↶
1007
let ( page_bytes, data_bytes) = ( page.clone ( ) , data.clone ( ) ) ;
a36bab6b Andy McCall 27 days ago ↶
1008
let files: [ ( & str , & [ u8 ] ) ; 3 ] = [ ( " data/frames.csv" , & data) , ( " index.html" , & page) , ( " report.toml" , toml) ] ;
1009
1010
assert_eq! ( upload_report_file ( & h.app, id, " tool" , " report.toml" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a report" ) ;
1011
assert_eq! ( upload_report_file ( & h.app, id, " frame-timing" , " ../x" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a clean path" ) ;
1012
let mut listed = vec! [ ] ;
1013
for ( path, bytes) in files {
1014
let ( st, text) = upload_report_file ( & h.app, id, " frame-timing" , path, bytes) .await;
1015
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1016
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1017
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( bytes) .to_hex ( ) ) ;
1018
listed.push ( serde_json:: json! ( { " path" : path, " blob" : blob, " size" : bytes.len ( ) } ) ) ;
1019
}
1020
let mut missing = listed.clone ( ) ;
1021
missing.push ( serde_json:: json! ( { " path" : " assets/x.js" , " blob" : " ab" .repeat ( 32 ) , " size" : 1 } ) ) ;
1022
let ( st, text) =
1023
upload ( & h.app, id, " frame-timing" , & serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : missing } ) ) .unwrap ( ) ) .await;
1024
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a file never uploaded: {text}" ) ;
1025
assert! ( text.contains ( " assets/x.js" ) , " {text}" ) ;
1026
let manifest = serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : listed } ) ) .unwrap ( ) ;
1027
let ( st, text) = upload ( & h.app, id, " frame-timing" , & manifest) .await;
1028
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1029
1030
let ( kind, title, summary, files_n, bytes) : ( String , String , String , i64 , i64 ) =
3b1bc27f Andy McCall 22 days ago
1031
sqlx:: query_as( " SELECT kind, title, summary, files, bytes FROM reports WHERE job_id = $1 AND name = 'frame-timing'" )
a36bab6b Andy McCall 27 days ago ↶
1032
.bind ( id)
1033
.fetch_one ( & h.app.db)
1034
.await
1035
.unwrap ( ) ;
1036
assert_eq! ( ( kind.as_str ( ) , title.as_str ( ) , summary.as_str ( ) ) , ( " performance" , " Frame timing" , " Median 16.4 ms" ) ) ;
1037
assert_eq! ( ( files_n, bytes) , ( 3 , ( data.len ( ) + page.len ( ) + toml.len ( ) ) as i64 ) ) ;
3b1bc27f Andy McCall 22 days ago
1038
let rid: i64 = sqlx:: query_scalar( " SELECT id FROM reports WHERE job_id = $1" ) .bind ( id) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
a36bab6b Andy McCall 27 days ago ↶
1039
let m = oxygenhub:: reports:: metrics( & h.app.db, rid) .await.unwrap ( ) ;
1040
assert_eq! ( ( m[ 0 ] .key.as_str ( ) , m[ 0 ] .value, m[ 0 ] .fail_above) , ( " median_ms" , 16. 4 , Some ( 20. 0 ) ) ) ;
1041
let log = db:: flow:: log( & h.app.db, id, 0 , 1000 ) .await.unwrap ( ) ;
1042
assert! ( log.iter ( ) .any ( | r | r.text.contains ( " report frame-timing: performance \" Frame timing\" , 1 metric" ) ) , " {log:?}" ) ;
c8df33a6 Andy McCall 27 days ago ↶
1043
1044
1045
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
1046
let c = checks.iter ( ) .find ( | c | c.name = = " report/frame-timing/median_ms" ) .expect ( " a check per threshold" ) ;
1047
assert_eq! ( c.status, " passed" , " {c:?}" ) ;
1048
assert! ( c.summary.contains ( " 16.4 ms" ) & & c.url.contains ( " /reports/frame-timing?id=" ) , " {c:?}" ) ;
1049
1050
1051
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
1052
assert! ( page.contains ( " >Reports</a>" ) , " the tab" ) ;
1053
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/reports" , None , None ) .await;
1054
assert! ( st = = StatusCode:: OK & & page.contains ( " Frame timing" ) & & page.contains ( " median_ms" ) , " {st}" ) ;
1055
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/reports/frame-timing" , None , None ) .await;
1056
assert! ( st = = StatusCode:: OK & & page.contains ( " fails above 20" ) & & page.contains ( " sandbox=\" allow-scripts\" " ) , " {st}" ) ;
1057
let src = page.split ( " class=\" rep-frame\" src=\" " ) .nth ( 1 ) .unwrap ( ) .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1058
assert! ( src.ends_with ( " /index.html" ) , " {src}" ) ;
1059
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( Request:: get( & src) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
1060
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
1061
let csp = res.headers ( ) [ header:: CONTENT_SECURITY_POLICY ] .to_str ( ) .unwrap ( ) .to_string ( ) ;
1062
assert! ( csp.starts_with ( " sandbox allow-scripts;" ) & & csp.contains ( " default-src 'none'" ) , " {csp}" ) ;
d74ce22f Andy McCall 22 days ago ↶
1063
1064
assert! (
1065
res.headers ( ) .get ( header:: X_FRAME_OPTIONS ) .is_none ( ) & & res.headers ( ) .get ( header:: CONTENT_SECURITY_POLICY_REPORT_ONLY ) .is_none ( )
1066
) ;
1067
assert_eq! ( res.headers ( ) [ header:: X_CONTENT_TYPE_OPTIONS ] , " nosniff" ) ;
c8df33a6 Andy McCall 27 days ago ↶
1068
assert_eq! ( res.headers ( ) [ header:: ACCESS_CONTROL_ALLOW_ORIGIN ] , " *" ) ;
f3e5e2bc Andy McCall 27 days ago ↶
1069
1070
let served = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ;
1071
assert! ( served.starts_with ( & page_bytes) , " the report's page" ) ;
1072
assert! ( String :: from_utf8_lossy( & served) .contains ( " /static/report-frame.js" ) , " with the frame script" ) ;
1073
let ( st, _ ) = get_bytes ( & h.app, " /static/report.css" ) .await;
1074
assert_eq! ( st, StatusCode:: OK , " the stylesheet a report may link" ) ;
c8df33a6 Andy McCall 27 days ago ↶
1075
let data = src.replace ( " /index.html" , " /data/frames.csv" ) ;
1076
assert_eq! ( get_bytes ( & h.app, & data) .await, ( StatusCode:: OK , data_bytes.clone ( ) ) ) ;
1077
95b525b0 Andy McCall 27 days ago ↶
1078
1079
1080
let first: i64 = sqlx:: query_scalar( " SELECT id FROM reports WHERE name = 'frame-timing'" ) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
1081
let second: i64 = sqlx:: query_scalar(
1082
" INSERT INTO reports (repo_id, name, kind, title, summary, context, run_id, job_id, run_number, job, bookmark, state, manifest, files, bytes, created_ms) \
3b1bc27f Andy McCall 22 days ago
1083
SELECT repo_id, name, kind, title, 'Median 18.0 ms', context, run_id, NULL, 2, job, bookmark, state, manifest, files, bytes, created_ms + 1000 FROM reports WHERE id = $1 RETURNING id" ,
95b525b0 Andy McCall 27 days ago ↶
1084
)
1085
.bind ( first)
1086
.fetch_one ( & h.app.db)
1087
.await
1088
.unwrap ( ) ;
1089
sqlx:: query(
3b1bc27f Andy McCall 22 days ago
1090
" INSERT INTO report_metrics (report_id, key, value, unit, better, fail_above) VALUES ($1, 'median_ms', 18.0, 'ms', 'lower', 20)" ,
95b525b0 Andy McCall 27 days ago ↶
1091
)
1092
.bind ( second)
1093
.execute ( & h.app.db)
1094
.await
1095
.unwrap ( ) ;
1096
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/reports/frame-timing" , None , None ) .await;
1097
assert_eq! ( page.matches ( " data-tl-stop" ) .count ( ) , 2 , " a stop per run" ) ;
1098
assert! ( page.contains ( " Median 18.0 ms" ) , " the newest run is shown" ) ;
1099
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/reports/frame-timing?id={first} &compare={second} &play=3" ) , None , None ) .await;
1100
assert! ( page.contains ( " This run against run #2" ) & & page.contains ( " -1.6" ) , " every metric against the other run" ) ;
1101
let stop = page.split ( " data-tl-stop" ) .next ( ) .unwrap_or ( " " ) .rsplit ( " href=" ) .next ( ) .unwrap_or ( " " ) .to_string ( ) ;
1102
assert! ( page.contains ( " data-play=\" 3\" " ) , " playing" ) ;
1103
assert! ( stop.contains ( " play" ) & & stop.contains ( ' 3' ) , " playing carries on from stop to stop: {stop}" ) ;
1104
assert! ( page.contains ( " data-report-runs=" ) , " the frame interface knows the runs" ) ;
c8df33a6 Andy McCall 27 days ago ↶
1105
let forged = src.replacen ( " /files/" , " /files/9" , 1 ) ;
1106
assert_eq! ( get_bytes ( & h.app, & forged) .await.0 , StatusCode:: NOT_FOUND ) ;
1107
assert_eq! ( get_bytes ( & h.app, & src.replace ( " /index.html" , " /secret.txt" ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
a36bab6b Andy McCall 27 days ago ↶
1108
let _ = run;
1109
1110
1111
{
1112
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
1113
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
1114
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
1115
hosted.save_config ( ) .unwrap ( ) ;
1116
}
1117
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
1118
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
1119
let hosted = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
1120
for ( path, bytes) in files {
1121
assert_eq! ( hosted.read_blob ( & oxy_core:: repo:: Repo:: blob_ref( bytes) ) .unwrap ( ) , bytes, " {path} survives gc" ) ;
1122
}
1123
}
1124
d29070fa Andy McCall 1 month ago ↶
1125
# [ tokio ::test ( flavor = " multi_thread" ) ]
1126
async fn a_failing_job_keeps_an_artifact_outside_its_receipt ( ) {
1127
let h = hub ( ) .await;
1128
create ( & h.app, " andy" , " demo" ) .await;
1129
let url = format! ( " {} /andy/demo" , h.base) ;
1130
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1131
let c = client ( & url, work.path ( ) ) ;
1132
write (
1133
work.path ( ) ,
1134
" .flow/pipeline.kdl" ,
1135
& format! (
336c953a Andy McCall 1 month ago ↶
1136
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n exec \" make\" \" test\" \n output {{ \n artifact \" junit\" {{ \n path \" /src/out/junit.xml\" \n when \" always\" \n }} \n }} \n }} \n "
d29070fa Andy McCall 1 month ago ↶
1137
) ,
1138
) ;
1139
c.snapshot ( " test" ) .unwrap ( ) ;
1140
c.commit ( " Test" , & [ ] ) .unwrap ( ) ;
1141
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1142
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1143
let a = claim ( & h.app) .await.unwrap ( ) ;
336c953a Andy McCall 1 month ago ↶
1144
assert_eq! ( a[ " job" ] [ " outputs" ] [ 0 ] [ " always" ] , true , " the runner is told to keep it from a failing job" ) ;
d29070fa Andy McCall 1 month ago ↶
1145
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
0b203bbf Andy McCall 1 month ago ↶
1146
1147
let xml = br # "<testsuites><testsuite name="pytest"><testcase classname="tests.test_io" name="test_read" time="0.5"/><testcase classname="tests.test_io" name="test_write" time="1.5"><failure message="disk full">E OSError: disk full</failure></testcase></testsuite></testsuites>"# ;
1148
let ( st, text) = upload ( & h.app, id, " junit" , xml) .await;
d29070fa Andy McCall 1 month ago ↶
1149
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1150
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1151
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 1 ) ;
1152
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1153
assert_eq! (
1154
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
1155
StatusCode:: BAD_REQUEST ,
1156
" a failing receipt names no outputs"
1157
) ;
1158
r[ " outputs" ] = serde_json:: json! ( [ ] ) ;
1159
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r } ) ) .await, StatusCode:: NO_CONTENT ) ;
1160
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
1161
assert! ( page.contains ( & format! ( " /andy/demo/flow/{} /artifacts/test/junit" , run.number) ) , " listed" ) ;
1162
assert! ( page.contains ( " not in its receipt" ) , " marked" ) ;
0b203bbf Andy McCall 1 month ago ↶
1163
assert! ( page.contains ( " 2 tests: 1 failed" ) , " counts beside the job" ) ;
1164
assert! ( page.contains ( " tests.test_io::test_write" ) & & page.contains ( " disk full" ) & & page.contains ( " OSError" ) , " the failure" ) ;
1165
assert! ( page.contains ( " tests: 2 tests: 1 failed in 2.0 s" ) , " said in the job's log" ) ;
d29070fa Andy McCall 1 month ago ↶
1166
}
1167
df940d75 Andy McCall 1 month ago ↶
1168
1169
1170
1171
# [ tokio ::test ( flavor = " multi_thread" ) ]
1172
async fn a_release_is_audited_from_authors_to_receipts ( ) {
1173
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
1174
use oxy_core:: identity:: SecretKey;
1175
use oxy_core:: objects:: StateObject;
1176
use oxy_core:: receipt:: SignedReceipt;
1177
let h = hub ( ) .await;
1178
create ( & h.app, " andy" , " demo" ) .await;
1179
let url = format! ( " {} /andy/demo" , h.base) ;
1180
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1181
let mut c = client ( & url, work.path ( ) ) ;
1182
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
1183
write (
1184
work.path ( ) ,
1185
" .flow/pipeline.kdl" ,
1186
& format! (
1187
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n }} \n }} \n "
1188
) ,
1189
) ;
1190
c.snapshot ( " test" ) .unwrap ( ) ;
1191
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
1192
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
1193
c.tag_create ( " v1" , & heads, " first" ) .unwrap ( ) ;
1194
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1195
1196
1197
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1198
let a = claim ( & h.app) .await.unwrap ( ) ;
1199
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1200
let body: Vec< u8 > = b " #!/bin/sh\n echo tool\n " .to_vec ( ) ;
1201
let ( _ , text) = upload ( & h.app, id, " tool" , & body) .await;
1202
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1203
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
1204
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1205
let heads_run: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1206
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads_run) .encode ( ) ) ;
1207
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
1208
let sk = SecretKey:: generate( ) ;
1209
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
1210
let fr: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
1211
let signed = hex:: encode( SignedReceipt:: sign( fr.to_oxy ( ) , repo_id, state, & format! ( " {url} /flow/1" ) , & runner, & sk) .encode ( ) ) ;
1212
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 900 , " receipt" : r, " signed_receipt" : signed } ) ;
1213
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
1214
1215
1216
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1217
let rel = db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
1218
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " tool" , " " , & body) .await.unwrap ( ) ;
1219
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " notes.txt" , " " , b " by hand\n " ) .await.unwrap ( ) ;
1220
1221
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1" , None , None ) .await;
1222
assert_eq! ( st, StatusCode:: OK ) ;
1223
for want in [
1224
" Tag v1 is signed" ,
1225
" The one change in the release is authenticated" ,
1226
" tool: signed receipt from run #1 build" ,
1227
" a runner key of this hub" ,
1228
" tool: built from the tagged state" ,
1229
" tool: pinned environment" ,
1230
" notes.txt: no build found" ,
1231
" <svg class=\" trust-graph\" " ,
bf5101d3 Andy McCall 1 month ago ↶
1232
1233
1234
" Tag signed by" ,
1235
" 1 of 1 changes signed or sealed" ,
1236
" 1 of 2 assets built and attested by OxygenFlow" ,
1237
" attested build" ,
1238
" uploaded by hand" ,
1239
" receipt verified" ,
1240
" class=\" shackle\" " ,
1241
" <title>notes.txt:" ,
df940d75 Andy McCall 1 month ago ↶
1242
] {
1243
assert! ( page.contains ( want) , " missing {want:?}" ) ;
1244
}
bf5101d3 Andy McCall 1 month ago ↶
1245
let svg = & page[ page.find ( " <svg class=\" trust-graph\" " ) .unwrap ( ) .. ] ;
1246
assert! ( svg[ .. 200 ] .contains ( " width=\" " ) & & svg[ .. 200 ] .contains ( " height=\" " ) , " the graph has its own size: {}" , & svg[ .. 200 ] ) ;
df940d75 Andy McCall 1 month ago ↶
1247
let ( _ , releases) = call ( & h.app, " GET" , " /andy/demo/releases" , None , None ) .await;
1248
assert! ( releases.contains ( " /andy/demo/releases/trust/v1" ) , " the release links its audit" ) ;
1249
1250
1251
let ( st, json) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1?format=json" , None , None ) .await;
1252
assert_eq! ( st, StatusCode:: OK ) ;
1253
let t: serde_json:: Value = serde_json:: from_str( & json) .unwrap ( ) ;
1254
assert_eq! ( ( t[ " tag" ] .as_str ( ) , t[ " tag_signature" ] .as_str ( ) , t[ " changes" ] .as_i64 ( ) ) , ( Some ( " v1" ) , Some ( " valid" ) , Some ( 1 ) ) ) ;
1255
assert_eq! ( t[ " authors" ] [ 0 ] [ " who" ] [ " name" ] , " Andy" ) ;
1256
let tool = t[ " assets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | a | a[ " name" ] = = " tool" ) .unwrap ( ) ;
1257
let hex_receipt = tool[ " build" ] [ " signed_receipt" ] .as_str ( ) .unwrap ( ) ;
1258
let sr = SignedReceipt:: decode( & hex:: decode( hex_receipt) .unwrap ( ) ) .unwrap ( ) ;
1259
sr.verify ( ) .unwrap ( ) ;
1260
assert_eq! ( sr.receipt.outputs[ 0 ] .to_hex ( ) , blob) ;
1261
let audit = t[ " audit" ] .as_array ( ) .unwrap ( ) ;
1262
assert! ( audit.iter ( ) .any ( | c | c[ " verdict" ] = = " warn" & & c[ " title" ] .as_str ( ) .unwrap ( ) .starts_with ( " notes.txt" ) ) ) ;
1263
assert! ( ! audit.iter ( ) .any ( | c | c[ " verdict" ] = = " fail" ) , " {audit:?}" ) ;
1264
1265
let ( st, api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v1" , None , None ) .await;
1266
assert_eq! ( st, StatusCode:: OK ) ;
1267
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & api) .unwrap ( ) [ " assets" ] .as_array ( ) .unwrap ( ) .len ( ) , 2 ) ;
1268
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1269
assert_eq! ( call ( & h.app, " GET" , " /andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1270
}
b6b7e4cf Andy McCall 1 month ago ↶
1271
1272
1273
1274
1275
# [ tokio ::test ( flavor = " multi_thread" ) ]
1276
async fn runners_register_show_their_history_and_are_drained_and_removed ( ) {
1277
use oxy_core:: identity:: SecretKey;
1278
let h = hub ( ) .await;
1279
create ( & h.app, " andy" , " demo" ) .await;
1280
1281
let res = oxygenhub:: router( h.app.clone ( ) )
1282
.oneshot (
1283
Request:: post( " /register" )
1284
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1285
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery" ) )
1286
.unwrap ( ) ,
1287
)
1288
.await
1289
.unwrap ( ) ;
1290
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1291
let admin = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
1292
db:: accounts:: set_admin( & h.app.db, admin.id, true ) .await.unwrap ( ) ;
1293
let web = | method : & 'static str , path : String, body : Option< String> | {
1294
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1295
async move {
1296
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1297
if body.is_some ( ) {
1298
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1299
}
1300
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1301
let st = res.status ( ) ;
1302
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1303
}
1304
} ;
1305
let csrf_of = | page : & str | {
1306
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1307
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1308
} ;
1309
let ( st, page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1310
assert_eq! ( st, StatusCode:: OK ) ;
1311
assert! ( page.contains ( " No runners yet" ) ) ;
1312
assert_eq! ( call ( & h.app, " GET" , " /admin/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
1313
1314
1315
let ( _ , page) = web ( " POST" , " /admin/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1316
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1317
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1318
assert! ( token.starts_with ( " flowreg_" ) , " {token}" ) ;
1319
1320
1321
let sk = SecretKey:: generate( ) ;
1322
let identity = sk.identity ( " plex (OxygenFlow runner)" , " " , 1 ) ;
1323
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ " big-disk" ] , " max_jobs" : 2 , " memory_mib" : 16384 , " cpus" : 4 } ) ;
1324
let reg = serde_json:: json! ( { " token" : token, " name" : " plex" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1325
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1326
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1327
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1328
assert! ( credential.starts_with ( " flowrunner_" ) ) ;
1329
assert_eq! (
1330
call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 ,
1331
StatusCode:: FORBIDDEN ,
1332
" used once"
1333
) ;
1334
1335
1336
let claim_as = | who : String| {
1337
let app = h.app.clone ( ) ;
1338
let info = info.clone ( ) ;
1339
async move {
1340
let body = serde_json:: json! ( { " runner" : " ignored" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1341
call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await
1342
}
1343
} ;
1344
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " nothing queued" ) ;
1345
assert_eq! ( claim ( & h.app) .await, None ) ;
1346
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1347
assert! (
1348
page.contains ( " >plex</strong>" ) & & page.contains ( " st-online" ) & & page.contains ( " x86_64 · linux" ) & & page.contains ( " big-disk" ) ,
1349
" {page}"
1350
) ;
1351
assert! ( page.contains ( " >test-1</strong>" ) & & page.contains ( " hub.toml" ) , " the configured runner, by its claimed name" ) ;
1352
let plex = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
1353
assert_eq! ( plex.key, identity.hash ( ) .to_hex ( ) , " bound at registration" ) ;
1354
1355
1356
let url = format! ( " {} /andy/demo" , h.base) ;
1357
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1358
let c = client ( & url, work.path ( ) ) ;
1359
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1360
c.snapshot ( " test" ) .unwrap ( ) ;
1361
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1362
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1363
runs_of ( & h.app, " andy" , " demo" ) .await;
1364
let ( st, body) = claim_as ( credential.clone ( ) ) .await;
1365
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1366
let job: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1367
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1368
assert! (
1369
page.contains ( " andy/demo" ) & & page.contains ( job[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) & & page.contains ( " Running now</dt><dd>1 job" ) ,
1370
" {page}"
1371
) ;
b66e70b7 Andy McCall 1 month ago ↶
1372
1373
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
e9b9619c Andy McCall 22 days ago ↶
1374
assert! ( page.contains ( " id=\" queue\" " ) & & page.contains ( " >plex</a></td>" ) , " {page}" ) ;
1375
1376
1377
assert! ( page.contains ( " running 1 of " ) & & page.contains ( " /admin/runners?runner=plex#queue" ) , " {page}" ) ;
1378
let ( _ , only) = web ( " GET" , " /admin/runners?runner=plex" .into ( ) , None ) .await;
1379
assert! ( only.contains ( " >plex</a></td>" ) & & only.contains ( " of " ) & & only.contains ( " >all</a>" ) , " {only}" ) ;
1380
let ( _ , none) = web ( " GET" , " /admin/runners?runner=nobody&status=running" .into ( ) , None ) .await;
1381
assert! ( none.contains ( " Nothing matches." ) , " {none}" ) ;
1382
1383
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = 0 WHERE id = $1" ) .bind ( plex.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1384
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1385
assert! ( page.contains ( " st-offline" ) , " quiet for long: offline" ) ;
1386
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/heartbeat" , Some ( & credential) , None ) .await.0 , StatusCode:: NO_CONTENT ) ;
1387
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/heartbeat" , Some ( " flowrunner_nope" ) , None ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1388
let plex_now = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
1389
assert_eq! ( plex_now.connectivity ( ) , " online" ) ;
b66e70b7 Andy McCall 1 month ago ↶
1390
let ( _ , tokens) = web ( " GET" , " /settings/tokens" .into ( ) , None ) .await;
1391
let ( _ , made) = web ( " POST" , " /settings/tokens" .into ( ) , Some ( format! ( " _csrf={} &name=admin&scopes=user%3Aread" , csrf_of ( & tokens) ) ) ) .await;
1392
let i = made.find ( " oxyhub_" ) .unwrap ( ) ;
1393
let admin_token: String = made[ i.. ] .chars ( ) .take_while ( | c | c.is_ascii_alphanumeric ( ) | | * c = = ' _' ) .collect ( ) ;
1394
let ( st, body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1395
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1396
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1397
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1398
assert_eq! ( ( plex_row[ " running" ] .as_i64 ( ) , plex_row[ " arch" ] .as_str ( ) ) , ( Some ( 1 ) , Some ( " x86_64" ) ) ) ;
7f5f6c05 Andy McCall 1 month ago ↶
1399
1400
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1401
let ( st, _ ) = web (
1402
" POST" ,
1403
format! ( " /admin/runners/{} /limits" , plex.id) ,
1404
Some ( format! ( " _csrf={} &labels=gpu%2C+plex&timeout_minutes=5" , csrf_of ( & page) ) ) ,
1405
)
1406
.await;
1407
assert_eq! ( st, StatusCode:: SEE_OTHER ) ;
1408
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1409
assert! ( page.contains ( " >gpu<" ) & & page.contains ( " 5 min for its jobs" ) , " {page}" ) ;
1410
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1411
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1412
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1413
assert! ( plex_row[ " labels" ] .as_array ( ) .unwrap ( ) .iter ( ) .any ( | l | l = = " gpu" ) , " {plex_row}" ) ;
1414
let ( st, _ ) = web (
1415
" POST" ,
1416
format! ( " /admin/runners/{} /limits" , plex.id) ,
1417
Some ( format! ( " _csrf={} &labels=bad%20label%21&timeout_minutes=" , csrf_of ( & page) ) ) ,
1418
)
1419
.await;
1420
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a label with a space or punctuation is refused" ) ;
b66e70b7 Andy McCall 1 month ago ↶
1421
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners/queue" , Some ( & admin_token) , None ) .await;
1422
let queue: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1423
assert! ( queue.as_array ( ) .unwrap ( ) .iter ( ) .any ( | q | q[ " status" ] = = " running" & & q[ " runner" ] = = " plex" ) , " {queue}" ) ;
1424
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/runners/{} /jobs" , plex.id) , Some ( & admin_token) , None ) .await;
1425
let jobs: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1426
assert_eq! ( jobs[ 0 ] [ " repo" ] , " andy/demo" , " {jobs}" ) ;
1427
assert_eq! ( call ( & h.app, " GET" , " /api/v1/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
b6b7e4cf Andy McCall 1 month ago ↶
1428
1429
1430
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1431
let csrf = csrf_of ( & page) ;
1432
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=draining" ) ) ) .await;
1433
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " a draining runner takes nothing" ) ;
1434
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1435
assert! ( page.contains ( " is running jobs: drain it first" ) , " {page}" ) ;
1436
1437
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1438
let done = call (
1439
& h.app,
1440
" POST" ,
1441
& format! ( " /api/v1/flow/jobs/{id} /finish" ) ,
1442
Some ( & credential) ,
1443
Some ( r # "{"exit_code":0,"duration_ms":5}"# .into ( ) ) ,
1444
)
1445
.await;
1446
assert_eq! ( done.0 , StatusCode:: NO_CONTENT ) ;
1447
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=disabled" ) ) ) .await;
1448
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: FORBIDDEN ) ;
1449
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1450
assert! ( page.contains ( " Removed plex." ) , " {page}" ) ;
1451
assert_eq! ( claim_as ( credential) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1452
}
74be9796 Andy McCall 1 month ago ↶
1453
1454
1455
1456
# [ tokio ::test ( flavor = " multi_thread" ) ]
1457
async fn scoped_runners_take_only_their_scope ( ) {
1458
use oxy_core:: identity:: SecretKey;
1459
let h = hub ( ) .await;
1460
create ( & h.app, " andy" , " demo" ) .await;
1461
create ( & h.app, " andy" , " other" ) .await;
1462
let register = | scope : db:: runners:: Scope, name : & 'static str | {
1463
let app = h.app.clone ( ) ;
1464
async move {
1465
let token = format! ( " flowreg_{name} " ) ;
1466
db:: runners:: add_registration( & app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & scope) .await.unwrap ( ) ;
1467
let identity = SecretKey:: generate( ) .identity ( name, " " , 1 ) ;
1468
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1469
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1470
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1471
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1472
serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( )
1473
}
1474
} ;
1475
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1476
let only_demo = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } , " demo-runner" ) .await;
1477
let carols = register ( db:: runners:: Scope { owner: " carol" .into ( ) , repo_id: None } , " carol-runner" ) .await;
1478
let andys = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: None } , " andy-runner" ) .await;
1479
let claim_as = | who : String| {
1480
let app = h.app.clone ( ) ;
1481
async move {
1482
let body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
1483
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await;
1484
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
1485
}
1486
} ;
1487
1488
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1489
let c = client ( & format! ( " {} /andy/other" , h.base) , work.path ( ) ) ;
1490
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1491
c.snapshot ( " t" ) .unwrap ( ) ;
1492
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1493
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1494
assert! ( ! runs_of ( & h.app, " andy" , " other" ) .await.is_empty ( ) ) ;
1495
assert_eq! ( claim_as ( only_demo.clone ( ) ) .await, None , " nothing of andy/demo is queued" ) ;
1496
assert_eq! ( claim_as ( carols) .await, None , " nothing of carol's" ) ;
1497
assert_eq! ( claim_as ( andys) .await.as_deref ( ) , Some ( " andy/other" ) , " the owner's runner takes its repositories' jobs" ) ;
1498
1499
assert_eq! ( claim ( & h.app) .await.unwrap ( ) [ " repo" ] , " andy/other" ) ;
1500
assert_eq! ( claim_as ( only_demo) .await, None , " still nothing of its own" ) ;
1501
}
7974adb0 Andy McCall 1 month ago ↶
1502
1503
1504
1505
1506
# [ tokio ::test ( flavor = " multi_thread" ) ]
1507
async fn repository_admins_add_and_remove_their_own_runners ( ) {
1508
use oxy_core:: identity:: SecretKey;
1509
let h = hub ( ) .await;
1510
let res = oxygenhub:: router( h.app.clone ( ) )
1511
.oneshot (
1512
Request:: post( " /register" )
1513
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1514
.body ( Body:: from( " username=andy&email=andy%40example.com&password=correct-horse-battery" ) )
1515
.unwrap ( ) ,
1516
)
1517
.await
1518
.unwrap ( ) ;
1519
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1520
create ( & h.app, " andy" , " demo" ) .await;
1521
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1522
let web = | method : & 'static str , path : String, body : Option< String> | {
1523
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1524
async move {
1525
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1526
if body.is_some ( ) {
1527
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1528
}
1529
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1530
let st = res.status ( ) ;
1531
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1532
}
1533
} ;
1534
let csrf_of = | page : & str | {
1535
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1536
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1537
} ;
1538
let ( st, page) = web ( " GET" , " /andy/demo/settings" .into ( ) , None ) .await;
1539
assert_eq! ( st, StatusCode:: OK ) ;
1540
assert! ( page.contains ( " <h2>Runners</h2>" ) , " the owner sees the section" ) ;
1541
let ( _ , page) = web ( " POST" , " /andy/demo/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1542
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1543
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1544
let identity = SecretKey:: generate( ) .identity ( " mine" , " " , 1 ) ;
1545
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1546
let reg = serde_json:: json! ( { " token" : token, " name" : " mine" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1547
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1548
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1549
let mine = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " mine" ) .unwrap ( ) ;
1550
assert_eq! ( mine.scope ( ) , db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } ) ;
1551
1552
claim ( & h.app) .await;
1553
let site = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " test-1" ) .unwrap ( ) ;
1554
let ( _ , page) = web ( " GET" , " /andy/demo/settings" .into ( ) , None ) .await;
1555
assert! ( page.contains ( " <strong>mine</strong>" ) & & page.contains ( " this repository" ) , " {page}" ) ;
1556
assert! ( page.contains ( " <strong>test-1</strong>" ) & & page.contains ( " the whole site" ) , " {page}" ) ;
1557
let csrf = csrf_of ( & page) ;
1558
let ( st, _ ) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , site.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1559
assert_eq! ( st, StatusCode:: NOT_FOUND , " not the repository's own" ) ;
1560
let ( _ , page) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , mine.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1561
assert! ( page.contains ( " mine removed" ) , " {page}" ) ;
1562
assert! ( db:: runners:: get( & h.app.db, mine.id) .await.unwrap ( ) .is_none ( ) ) ;
1563
1564
1565
let ( _ , page) = web ( " GET" , " /orgs/new" .into ( ) , None ) .await;
1566
web ( " POST" , " /orgs/new" .into ( ) , Some ( format! ( " _csrf={} &name=acme&display_name=Acme" , csrf_of ( & page) ) ) ) .await;
1567
let ( st, page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1568
assert_eq! ( st, StatusCode:: OK ) ;
1569
let ( _ , page) = web ( " POST" , " /orgs/acme/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1570
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1571
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1572
let identity = SecretKey:: generate( ) .identity ( " acme-runner" , " " , 1 ) ;
1573
let reg = serde_json:: json! ( { " token" : token, " name" : " acme-runner" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1574
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 , StatusCode:: OK ) ;
1575
let acme = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " acme-runner" ) .unwrap ( ) ;
1576
assert_eq! ( acme.scope ( ) , db:: runners:: Scope { owner: " acme" .into ( ) , repo_id: None } ) ;
1577
let ( _ , page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1578
assert! ( page.contains ( " <strong>acme-runner</strong>" ) & & page.contains ( " the organisation's repositories" ) , " {page}" ) ;
1579
let ( _ , page) = web ( " POST" , format! ( " /orgs/acme/settings/runners/{} /remove" , acme.id) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1580
assert! ( page.contains ( " acme-runner removed" ) , " {page}" ) ;
1581
}
c4c87f48 Andy McCall 1 month ago ↶
1582
1583
1584
1585
# [ tokio ::test ( flavor = " multi_thread" ) ]
1586
async fn jobs_that_ask_for_labels_wait_for_a_runner_with_them ( ) {
1587
use oxy_core:: identity:: SecretKey;
1588
let h = hub ( ) .await;
1589
create ( & h.app, " andy" , " demo" ) .await;
1590
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1591
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
1592
let cross = " job \" cross\" {\n sandbox \" alpine\" \n runs-on \" riscv64\" \n exec \" true\" \n }\n " ;
1593
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( cross) ) ;
1594
c.snapshot ( " t" ) .unwrap ( ) ;
1595
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1596
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1597
assert! ( ! runs_of ( & h.app, " andy" , " demo" ) .await.is_empty ( ) ) ;
1598
1599
let mut taken = Vec :: new( ) ;
1600
while let Some ( j) = claim ( & h.app) .await {
1601
taken.push ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
1602
}
1603
assert! ( ! taken.contains ( & " cross" .to_string ( ) ) & & taken.contains ( & " lint" .to_string ( ) ) , " {taken:?}" ) ;
1604
let queue = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
1605
let waiting = queue.iter ( ) .find ( | q | q.job = = " cross" ) .unwrap ( ) ;
1606
assert_eq! ( ( waiting.status.as_str ( ) , waiting.needs ( ) ) , ( " queued" , vec! [ " riscv64" .to_string ( ) ] ) ) ;
1607
1608
let token = " flowreg_riscv" ;
1609
db:: runners:: add_registration( & h.app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & Default :: default( ) ) .await.unwrap ( ) ;
1610
let identity = SecretKey:: generate( ) .identity ( " visionfive" , " " , 1 ) ;
1611
let info = serde_json:: json! ( { " arch" : " riscv64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ " riscv64" ] , " max_jobs" : 1 } ) ;
1612
let reg = serde_json:: json! ( { " token" : token, " name" : " visionfive" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1613
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1614
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1615
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1616
let claim_body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1617
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( claim_body) ) .await;
1618
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1619
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] , " cross" ) ;
1620
}
070a7b64 Andy McCall 1 month ago ↶
1621
1622
1623
1624
1625
1626
# [ tokio ::test ( flavor = " multi_thread" ) ]
1627
async fn a_job_no_runner_reports_is_ended_after_the_time_limit ( ) {
1628
let h = hub ( ) .await;
1629
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1630
create ( & h.app, " andy" , " lost" ) .await;
1631
let url = format! ( " {} /andy/lost" , h.base) ;
1632
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1633
let c = client ( & url, work.path ( ) ) ;
1634
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1635
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1636
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1637
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1638
assert_eq! ( runs_of ( & h.app, " andy" , " lost" ) .await.len ( ) , 1 ) ;
1639
let job = claim ( & h.app) .await.expect ( " a job to claim" ) ;
1640
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1641
1642
1643
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 0 ) ;
1644
1645
let old = db:: now_ms( ) - 24 * 3600 * 1000 ;
3b1bc27f Andy McCall 22 days ago
1646
sqlx:: query( " UPDATE flow_jobs SET started_ms = $1 WHERE id = $2" ) .bind ( old) .bind ( id) .execute ( & h.app.db) .await.unwrap ( ) ;
070a7b64 Andy McCall 1 month ago ↶
1647
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 1 ) ;
1648
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1649
assert_eq! ( j.status, " error" ) ;
1650
assert! ( j.error.starts_with ( " lost:" ) , " {}" , j.error) ;
1651
let run = & runs_of ( & h.app, " andy" , " lost" ) .await[ 0 ] ;
1652
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
1653
assert! ( jobs.iter ( ) .all ( | j | j.status ! = " running" ) , " nothing left running" ) ;
1654
1655
assert_ne! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 } ) ) .await, StatusCode:: NO_CONTENT ) ;
1656
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " error" ) ;
1657
}
b0612e7d Andy McCall 1 month ago ↶
1658
1659
1660
1661
# [ tokio ::test ( flavor = " multi_thread" ) ]
1662
async fn a_runner_is_offered_only_jobs_whose_image_it_holds ( ) {
1663
let h = hub ( ) .await;
1664
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1665
create ( & h.app, " andy" , " images" ) .await;
1666
let url = format! ( " {} /andy/images" , h.base) ;
1667
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1668
let c = client ( & url, work.path ( ) ) ;
1669
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1670
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1671
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1672
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1673
assert_eq! ( runs_of ( & h.app, " andy" , " images" ) .await.len ( ) , 1 ) ;
1674
let claim_with = | images : Vec< String> | {
1675
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " images" : images } ) .to_string ( ) ;
1676
let app = h.app.clone ( ) ;
1677
async move {
1678
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await;
1679
( st, body)
1680
}
1681
} ;
1682
let ( st, _ ) = claim_with ( vec! [ " ab" .repeat ( 32 ) ] ) .await;
1683
assert_eq! ( st, StatusCode:: NO_CONTENT , " no job for a runner without the image" ) ;
1684
let jobs = db:: flow:: jobs( & h.app.db, runs_of ( & h.app, " andy" , " images" ) .await[ 0 ] .id) .await.unwrap ( ) ;
1685
let rootfs: serde_json:: Value = serde_json:: from_str( & jobs[ 0 ] .sandbox) .unwrap ( ) ;
1686
let rootfs = rootfs[ " rootfs" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1687
let ( st, body) = claim_with ( vec! [ " ab" .repeat ( 32 ) , rootfs] ) .await;
1688
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1689
}
eef677c4 Andy McCall 1 month ago ↶
1690
1691
1692
1693
1694
# [ tokio ::test ( flavor = " multi_thread" ) ]
1695
async fn a_job_its_runner_no_longer_holds_is_ended_at_its_next_claim ( ) {
1696
let h = hub ( ) .await;
1697
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1698
create ( & h.app, " andy" , " held" ) .await;
1699
let url = format! ( " {} /andy/held" , h.base) ;
1700
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1701
let c = client ( & url, work.path ( ) ) ;
1702
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1703
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1704
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1705
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1706
assert_eq! ( runs_of ( & h.app, " andy" , " held" ) .await.len ( ) , 1 ) ;
1707
let id = claim ( & h.app) .await.expect ( " a job to claim" ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1708
let claim_holding = | holding : Vec< i64 > | {
1709
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " holding" : holding } ) .to_string ( ) ;
1710
let app = h.app.clone ( ) ;
1711
async move { call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await.0 }
1712
} ;
1713
claim_holding ( vec! [ id] ) .await;
1714
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " running" , " a held job stays" ) ;
1715
claim_holding ( vec! [ ] ) .await;
1716
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1717
assert_eq! ( j.status, " error" ) ;
1718
assert! ( j.error.starts_with ( " lost:" ) & & j.error.contains ( " no longer holds" ) , " {}" , j.error) ;
1719
let run = & runs_of ( & h.app, " andy" , " held" ) .await[ 0 ] ;
1720
assert! ( db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.status ! = " running" ) , " the run settles" ) ;
1721
}
d7ce41ac Andy McCall 1 month ago ↶
1722
1723
1724
1725
1726
# [ tokio ::test ( flavor = " multi_thread" ) ]
1727
async fn a_scoped_job_runs_only_when_a_push_touches_its_paths ( ) {
1728
let h = hub ( ) .await;
1729
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1730
create ( & h.app, " andy" , " scoped" ) .await;
1731
let url = format! ( " {} /andy/scoped" , h.base) ;
1732
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1733
let c = client ( & url, work.path ( ) ) ;
1734
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \" packaging/**\" \n exec \" true\" \n }\n " ;
1735
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1736
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1737
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1738
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1739
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1740
let names = | app : Shared, n : usize | async move {
1741
let mut runs = Vec :: new( ) ;
1742
for _ in 0 .. 100 {
1743
runs = runs_of ( & app, " andy" , " scoped" ) .await;
1744
if runs.len ( ) > = n {
1745
break ;
1746
}
1747
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1748
}
1749
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1750
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1751
j.sort ( ) ;
745d225d Andy McCall 1 month ago ↶
1752
3b1bc27f Andy McCall 22 days ago
1753
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( runs[ 0 ] .id) .execute ( & app.db) .await.unwrap ( ) ;
d7ce41ac Andy McCall 1 month ago ↶
1754
j
1755
} ;
1756
assert_eq! ( names ( h.app.clone ( ) , 1 ) .await, [ " build" , " dist" , " lint" , " test" ] , " the first run has every job" ) ;
1757
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1758
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1759
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1760
assert_eq! ( names ( h.app.clone ( ) , 2 ) .await, [ " build" , " lint" , " test" ] , " dist is out of scope" ) ;
1761
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1762
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
1763
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1764
assert_eq! ( names ( h.app.clone ( ) , 3 ) .await, [ " build" , " dist" , " lint" , " test" ] , " a version bump runs dist" ) ;
1765
}
5dead1f4 Andy McCall 1 month ago ↶
1766
745d225d Andy McCall 1 month ago ↶
1767
1768
1769
1770
# [ tokio ::test ( flavor = " multi_thread" ) ]
1771
async fn runs_are_cancelled_by_hand_or_by_a_newer_push ( ) {
1772
let h = hub ( ) .await;
1773
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1774
create ( & h.app, " andy" , " stop" ) .await;
1775
let url = format! ( " {} /andy/stop" , h.base) ;
1776
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1777
let c = client ( & url, work.path ( ) ) ;
1778
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \n exec \" true\" \n }\n " ;
1779
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1780
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1781
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1782
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1783
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1784
let wait_runs = | app : Shared, n : usize | async move {
1785
for _ in 0 .. 100 {
1786
let runs = runs_of ( & app, " andy" , " stop" ) .await;
1787
if runs.len ( ) > = n {
1788
return runs;
1789
}
1790
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1791
}
1792
panic! ( " no run {n}" ) ;
1793
} ;
1794
let first = wait_runs ( h.app.clone ( ) , 1 ) .await;
3b1bc27f Andy McCall 22 days ago
1795
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
1796
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE run_id = $1" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
745d225d Andy McCall 1 month ago ↶
1797
1798
1799
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1800
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
1801
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1802
wait_runs ( h.app.clone ( ) , 2 ) .await;
1803
let job = claim ( & h.app) .await.expect ( " a job of run 2" ) ;
1804
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1805
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1806
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1807
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1808
let runs = wait_runs ( h.app.clone ( ) , 3 ) .await;
1809
let third = & runs[ 0 ] ;
1810
let mut second = runs[ 1 ] .clone ( ) ;
1811
for _ in 0 .. 100 {
1812
second = db:: flow:: run_by_id( & h.app.db, runs[ 1 ] .id) .await.unwrap ( ) .unwrap ( ) ;
1813
if ! second.live ( ) {
1814
break ;
1815
}
1816
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1817
}
1818
assert_eq! ( second.status, " cancelled" , " a newer push supersedes a run still going" ) ;
1819
let jobs = db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) ;
1820
assert! ( jobs.iter ( ) .all ( | j | j.status = = " cancelled" & & j.error = = " superseded by run #3" ) , " {jobs:?}" ) ;
1821
let mut names: Vec< String> = db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1822
names.sort ( ) ;
1823
assert_eq! ( names, [ " build" , " dist" , " lint" , " test" ] , " run 3 also checks what run 2 would have: the version bump" ) ;
1824
let mut checks = vec! [ ] ;
1825
for _ in 0 .. 100 {
1826
checks = db:: hooks:: checks_for_state( & h.app.db, second.repo_id, & second.state) .await.unwrap ( ) ;
1827
if checks.iter ( ) .all ( | c | c.status = = " cancelled" ) {
1828
break ;
1829
}
1830
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1831
}
1832
assert! ( checks.iter ( ) .all ( | c | c.status = = " cancelled" ) , " {checks:?}" ) ;
1833
1834
let ( st, _ ) =
1835
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( r # "{"records":[]}"# .into ( ) ) ) .await;
1836
assert_eq! ( st, StatusCode:: CONFLICT ) ;
1837
1838
1839
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
1840
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1841
let third = db:: flow:: run_by_id( & h.app.db, third.id) .await.unwrap ( ) .unwrap ( ) ;
1842
assert_eq! ( third.status, " cancelled" ) ;
1843
assert! ( db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.error = = " cancelled by andy" ) ) ;
1844
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
1845
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " already cancelled" ) , " {st} {body}" ) ;
1846
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , None , None ) .await;
1847
assert_ne! ( st, StatusCode:: OK , " anonymous callers cannot cancel" ) ;
1848
let ( _ , page) = call ( & h.app, " GET" , " /andy/stop/flow/3" , None , None ) .await;
1849
assert! ( page.contains ( " ⊘" ) & & ! page.contains ( " CANCEL</button>" ) , " a cancelled run shows so, with no button" ) ;
1850
}
1851
5dead1f4 Andy McCall 1 month ago ↶
1852
1853
1854
1855
# [ tokio ::test ( flavor = " multi_thread" ) ]
1856
async fn a_job_runs_under_its_own_time_limit_within_the_sites ( ) {
1857
let h = hub ( ) .await;
1858
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1859
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_DEFAULT , " 30" ) .await.unwrap ( ) ;
1860
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_MAX , " 120" ) .await.unwrap ( ) ;
1861
create ( & h.app, " andy" , " limits" ) .await;
1862
let url = format! ( " {} /andy/limits" , h.base) ;
1863
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1864
let c = client ( & url, work.path ( ) ) ;
1865
let extra = " job \" slow\" {\n sandbox \" alpine\" \n timeout \" 90m\" \n exec \" true\" \n }\n job \" huge\" {\n sandbox \" alpine\" \n timeout \" 5h\" \n exec \" true\" \n }\n " ;
1866
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1867
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1868
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1869
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1870
assert_eq! ( runs_of ( & h.app, " andy" , " limits" ) .await.len ( ) , 1 ) ;
1871
let mut limits = std:: collections:: HashMap:: new( ) ;
1872
let mut ids = std:: collections:: HashMap:: new( ) ;
1873
while let Some ( j) = claim ( & h.app) .await {
1874
let name = j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1875
limits.insert ( name.clone ( ) , j[ " timeout_s" ] .as_u64 ( ) .unwrap ( ) ) ;
1876
ids.insert ( name, j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
1877
}
1878
assert_eq! ( limits[ " slow" ] , 5400 , " its own" ) ;
1879
assert_eq! ( limits[ " huge" ] , 7200 , " cut to the site's maximum" ) ;
1880
assert_eq! ( limits[ " build" ] , 1800 , " the site's default" ) ;
1881
1882
let started = db:: now_ms( ) - 40 * 60 * 1000 ;
3b1bc27f Andy McCall 22 days ago
1883
sqlx:: query( " UPDATE flow_jobs SET started_ms = $1" ) .bind ( started) .execute ( & h.app.db) .await.unwrap ( ) ;
5dead1f4 Andy McCall 1 month ago ↶
1884
oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) ;
1885
let status = | id : i64 | {
1886
let app = h.app.clone ( ) ;
1887
async move { db:: flow:: job( & app.db, id) .await.unwrap ( ) .unwrap ( ) .status }
1888
} ;
1889
assert_eq! ( status ( ids[ " build" ] ) .await, " error" , " past its 30 minutes" ) ;
1890
assert_eq! ( status ( ids[ " slow" ] ) .await, " running" , " within its 90" ) ;
1891
assert_eq! ( status ( ids[ " huge" ] ) .await, " running" , " within its 120" ) ;
1892
}
c889911b Andy McCall 1 month ago ↶
1893
1894
1895
1896
1897
# [ tokio ::test ( flavor = " multi_thread" ) ]
1898
async fn on_decides_which_events_start_a_job ( ) {
1899
let h = hub ( ) .await;
1900
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1901
create ( & h.app, " andy" , " events" ) .await;
1902
let url = format! ( " {} /andy/events" , h.base) ;
1903
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1904
let c = client ( & url, work.path ( ) ) ;
1905
let text = format! (
1906
" on {{ \n push \" main\" \n manual\n }} \n {} job \" dist\" {{ \n sandbox \" alpine\" \n on {{ \n tag \" v*\" \n }} \n exec \" true\" \n }} \n " ,
1907
pipeline ( " " )
1908
) ;
1909
1910
let text = text.replacen (
1911
" on {\n push \" main\" \n manual\n }\n schema-version 1\n " ,
1912
" schema-version 1\n on {\n push \" main\" \n manual\n }\n " ,
1913
1 ,
1914
) ;
1915
write ( work.path ( ) , " .flow/pipeline.kdl" , & text) ;
1916
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1917
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1918
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1919
let jobs_of_latest = | app : Shared, n : usize | async move {
1920
let mut runs = Vec :: new( ) ;
1921
for _ in 0 .. 100 {
1922
runs = runs_of ( & app, " andy" , " events" ) .await;
1923
if runs.len ( ) > = n {
1924
break ;
1925
}
1926
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1927
}
1928
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1929
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1930
j.sort ( ) ;
1931
( runs[ 0 ] .clone ( ) , j)
1932
} ;
1933
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 1 ) .await;
1934
assert_eq! ( j, [ " build" , " lint" , " test" ] , " a push to main: no dist" ) ;
1935
1936
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
1937
c.tag_create ( " v1.0.0" , & heads, " first" ) .unwrap ( ) ;
1938
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1939
let ( run, j) = jobs_of_latest ( h.app.clone ( ) , 2 ) .await;
1940
assert_eq! ( j, [ " dist" ] , " a tag runs only what names it" ) ;
1941
assert_eq! ( run.trigger, " tag:v1.0.0" ) ;
1942
1943
let row = db:: repos:: get( & h.app.db, " andy" , " events" ) .await.unwrap ( ) .unwrap ( ) ;
1944
db:: flow:: finish(
1945
& h.app.db,
1946
db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) [ 0 ] .id,
1947
db:: flow:: Outcome { status: " passed" , exit_code: Some ( 0 ) , error: " " , receipt: " " , receipt_hash: " " } ,
1948
)
1949
.await
1950
.unwrap ( ) ;
1951
let run = db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) ;
1952
oxygenhub:: flow:: rerun( & h.app, & row, & run, " andy" ) .await.unwrap ( ) ;
1953
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 3 ) .await;
1954
assert_eq! ( j, [ " dist" ] , " the rerun is the tag's run again" ) ;
1955
1956
c.bookmark_create ( " develop" , None ) .unwrap ( ) ;
1957
assert! ( c.push_bookmark ( " origin" , " develop" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1958
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
1959
assert_eq! ( runs_of ( & h.app, " andy" , " events" ) .await.len ( ) , 3 , " no run for develop" ) ;
348f8c4b Andy McCall 1 month ago ↶
1960
}
1961
1962
1963
1964
# [ tokio ::test ( flavor = " multi_thread" ) ]
1965
async fn scheduled_jobs_run_when_due ( ) {
1966
let h = hub ( ) .await;
1967
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1968
create ( & h.app, " andy" , " nightly" ) .await;
1969
let url = format! ( " {} /andy/nightly" , h.base) ;
1970
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1971
let c = client ( & url, work.path ( ) ) ;
1972
1973
let extra = " job \" nightly\" {\n sandbox \" alpine\" \n on {\n schedule \" 17 4 29 2 *\" \n }\n exec \" true\" \n }\n " ;
1974
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1975
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1976
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1977
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1978
assert_eq! ( runs_of ( & h.app, " andy" , " nightly" ) .await.len ( ) , 1 ) ;
1979
let first = runs_of ( & h.app, " andy" , " nightly" ) .await;
1980
let names: Vec< String> = db:: flow:: jobs( & h.app.db, first[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1981
assert! ( ! names.contains ( & " nightly" .to_string ( ) ) , " a push does not run a scheduled job" ) ;
1982
use flow_core:: pipeline:: When;
1983
let not_yet = When { minute: 16 , hour: 4 , day: 29 , month: 2 , weekday: 4 } ;
1984
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, not_yet) .await, 0 ) ;
1985
let due = When { minute: 17 , .. not_yet } ;
1986
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, due) .await, 1 ) ;
1987
let runs = runs_of ( & h.app, " andy" , " nightly" ) .await;
1988
assert_eq! ( runs.len ( ) , 2 ) ;
1989
assert_eq! ( runs[ 0 ] .trigger, " schedule" ) ;
1990
let names: Vec< String> = db:: flow:: jobs( & h.app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1991
assert_eq! ( names, [ " nightly" ] ) ;
1992
assert_eq! ( runs[ 0 ] .bookmark, " main" , " on the default bookmark" ) ;
c889911b Andy McCall 1 month ago ↶
1993
}
2a83f530 Andy McCall 1 month ago ↶
1994
1995
# [ tokio ::test ( flavor = " multi_thread" ) ]
1996
async fn variables_from_three_levels_reach_a_job_inner_overriding_outer ( ) {
1997
use oxygenhub:: levels:: Scope;
1998
use oxygenhub:: variables;
1999
let h = hub ( ) .await;
2000
let app = & h.app;
2001
2002
let res = oxygenhub:: router( app.clone ( ) )
2003
.oneshot (
2004
Request:: post( " /register" )
2005
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
2006
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery" ) )
2007
.unwrap ( ) ,
2008
)
2009
.await
2010
.unwrap ( ) ;
2011
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
2012
let andy = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
2013
db:: accounts:: create_org( & app.db, " acme" , " Acme" , andy.id) .await.unwrap ( ) ;
2014
create ( app, " acme" , " app" ) .await;
2015
let page = | path : & str , form : Option< String> | {
2016
let mut req =
2017
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
2018
if form.is_some ( ) {
2019
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
2020
}
2021
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
2022
let app = app.clone ( ) ;
2023
async move {
2024
let res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
2025
let status = res.status ( ) ;
2026
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
2027
( status, body)
2028
}
2029
} ;
2030
let csrf_of = | page : & str | {
2031
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
2032
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
2033
} ;
2034
2035
2036
2037
variables:: set( & app.db, Scope:: Site, " CC" , " gcc" ) .await.unwrap ( ) ;
2038
variables:: set( & app.db, Scope:: Site, " LANG" , " C" ) .await.unwrap ( ) ;
2039
let org = db:: accounts:: org_by_name( & app.db, " acme" ) .await.unwrap ( ) .unwrap ( ) ;
2040
variables:: set( & app.db, Scope:: Org( org.id) , " CC" , " clang" ) .await.unwrap ( ) ;
2041
let ( st, settings) = page ( " /acme/app/settings" , None ) .await;
2042
assert_eq! ( st, StatusCode:: OK , " {settings}" ) ;
2043
assert! ( settings.contains ( " Inherited" ) & & settings.contains ( " the acme organisation" ) & & settings.contains ( " overridden" ) , " {settings}" ) ;
2044
let csrf = csrf_of ( & settings) ;
2045
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=RUST_LOG&value=debug" ) ) ) .await;
2046
assert! ( body.contains ( " RUST_LOG set." ) & & body.contains ( " <code>debug</code>" ) , " {body}" ) ;
2047
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=FLOW_REPO&value=x" ) ) ) .await;
2048
assert! ( body.contains ( " not FLOW_" ) , " {body}" ) ;
2049
let repo = db:: repos:: get( & app.db, " acme" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
2050
db:: repos:: set_flow( & app.db, repo.id, " on" ) .await.unwrap ( ) ;
2051
let env = variables:: env_for_repo( & app.db, & repo) .await.unwrap ( ) ;
2052
assert_eq! (
2053
env.iter ( ) .map ( | ( k , v ) | ( k.as_str ( ) , v.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
2054
[ ( " CC" , " clang" ) , ( " LANG" , " C" ) , ( " RUST_LOG" , " debug" ) ] ,
2055
" the organisation's CC wins over the site's"
2056
) ;
2057
2058
2059
let url = format! ( " {} /acme/app" , h.base) ;
2060
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2061
let c = client ( & url, work.path ( ) ) ;
2062
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2063
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2064
c.snapshot ( " test" ) .unwrap ( ) ;
2065
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2066
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2067
let runs = runs_of ( app, " acme" , " app" ) .await;
2068
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
2069
let got = claim ( app) .await.unwrap_or_else ( | | panic! ( " a job: {:?}" , runs[ 0 ] ) ) ;
2070
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " LANG" : " C" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
2071
2072
2073
let ( _ , admin) = page ( " /admin" , None ) .await;
2074
assert! ( admin.contains ( " Variables" ) & & admin.contains ( " <code>gcc</code>" ) , " {admin}" ) ;
2075
let ( _ , body) = page ( " /admin/variables" , Some ( format! ( " _csrf={} &action=delete&name=LANG" , csrf_of ( & admin) ) ) ) .await;
2076
assert! ( body.contains ( " LANG removed." ) , " {body}" ) ;
2077
let got = claim ( app) .await.expect ( " another job" ) ;
2078
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
2079
}
4ac01a73 Andy McCall 1 month ago ↶
2080
2081
# [ tokio ::test ( flavor = " multi_thread" ) ]
2082
async fn secrets_a_job_declares_come_with_the_claim_and_never_reach_the_log ( ) {
2083
let h = hub ( ) .await;
2084
let app = & h.app;
2085
let res = oxygenhub:: router( app.clone ( ) )
2086
.oneshot (
2087
Request:: post( " /register" )
2088
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
2089
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery" ) )
2090
.unwrap ( ) ,
2091
)
2092
.await
2093
.unwrap ( ) ;
2094
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
2095
let page = | path : & str , form : Option< String> | {
2096
let mut req =
2097
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
2098
if form.is_some ( ) {
2099
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
2100
}
2101
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
2102
let app = app.clone ( ) ;
2103
async move {
2104
let res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
2105
let status = res.status ( ) ;
2106
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
2107
( status, body)
2108
}
2109
} ;
2110
let csrf_of = | page : & str | {
2111
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
2112
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
2113
} ;
2114
create ( app, " andy" , " demo" ) .await;
2115
2116
2117
let ( _ , admin) = page ( " /admin" , None ) .await;
2118
let csrf = csrf_of ( & admin) ;
2119
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=deploy-token&value=site-level-value" ) ) ) .await;
2120
assert! ( body.contains ( " deploy-token set." ) & & ! body.contains ( " site-level-value" ) , " {body}" ) ;
2121
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=bad+name&value=x" ) ) ) .await;
2122
assert! ( body.contains ( " name is 1 to 64 of" ) , " {body}" ) ;
2123
let ( _ , settings) = page ( " /andy/demo/settings" , None ) .await;
2124
assert! (
2125
settings.contains ( " Secrets" ) & & settings.contains ( " <code>deploy-token</code>" ) & & settings.contains ( " the site</td>" ) ,
2126
" {settings}"
2127
) ;
2128
let csrf_r = csrf_of ( & settings) ;
2129
let ( _ , body) =
2130
page ( " /andy/demo/settings/secrets" , Some ( format! ( " _csrf={csrf_r} &action=set&name=deploy-token&value=hunter22-repo-value" ) ) ) .await;
2131
assert! ( body.contains ( " deploy-token set." ) & & body.contains ( " overridden" ) & & ! body.contains ( " hunter22" ) , " {body}" ) ;
2132
let repo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2133
assert! (
2134
! String :: from_utf8_lossy( & oxygenhub:: secrets:: list( & app.db, oxygenhub:: levels:: Scope:: Repo( repo.id) ) .await.unwrap ( ) [ 0 ] .value)
2135
.contains ( " hunter22" ) ,
2136
" sealed at rest"
2137
) ;
2138
2139
2140
let url = format! ( " {} /andy/demo" , h.base) ;
2141
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2142
let c = client ( & url, work.path ( ) ) ;
2143
write (
2144
work.path ( ) ,
2145
" .flow/pipeline.kdl" ,
2146
& pipeline ( " job \" deploy\" {\n sandbox \" alpine\" \n secret \" deploy-token\" {\n mount-path \" /run/secrets/token\" \n }\n secret \" missing\" {\n mount-path \" /run/secrets/missing\" \n }\n exec \" deploy\" \n }\n " ) ,
2147
) ;
2148
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2149
c.snapshot ( " test" ) .unwrap ( ) ;
2150
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2151
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2152
let runs = runs_of ( app, " andy" , " demo" ) .await;
2153
let jobs = match runs.first ( ) {
2154
Some ( r) => db:: flow:: jobs( & app.db, r.id) .await.unwrap ( ) .iter ( ) .map ( | j | format! ( " {} :{} " , j.name, j.status) ) .collect:: < Vec< _ > > ( ) ,
2155
None => vec! [ ] ,
2156
} ;
2157
let mut deploy = None ;
2158
for _ in 0 .. 50 {
2159
let Some ( got) = claim ( app) .await else {
2160
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2161
continue ;
2162
} ;
2163
if got[ " job" ] [ " name" ] = = " deploy" {
2164
deploy = Some ( got) ;
2165
break ;
2166
}
2167
assert_eq! ( got[ " secrets" ] , serde_json:: Value:: Null, " {got}" ) ;
2168
}
2169
let got = deploy.unwrap_or_else ( | | panic! ( " the deploy job: runs {runs:?} jobs {jobs:?}" ) ) ;
2170
assert_eq! (
2171
got[ " secrets" ] ,
2172
serde_json:: json! ( [ { " name" : " deploy-token" , " path" : " /run/secrets/token" , " value" : " hunter22-repo-value" } ] ) ,
2173
" the repository's value, the missing one left out: {got}"
2174
) ;
2175
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2176
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2177
assert! ( log.iter ( ) .any ( | l | l.stream = = " flow" & & l.text.contains ( " secret \" missing\" is not set" ) ) , " {log:?}" ) ;
2178
2179
2180
let records = serde_json:: json! ( { " records" : [
2181
{ " t" : 1 , " stream" : " out" , " text" : " token=hunter22-repo-value" } ,
2182
{ " t" : 2 , " stream" : " err" , " text" : " b64 aHVudGVyMjItcmVwby12YWx1ZQ== end" } ,
2183
] } ) ;
2184
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2185
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2186
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2187
let texts: Vec< & str > = log.iter ( ) .map ( | l | l.text.as_str ( ) ) .collect ( ) ;
2188
assert! ( texts.contains ( & " token=***" ) & & texts.contains ( & " b64 *** end" ) , " {texts:?}" ) ;
2189
assert! ( ! texts.iter ( ) .any ( | t | t.contains ( " hunter22" ) ) , " {texts:?}" ) ;
2190
}
d1727c57 Andy McCall 1 month ago ↶
2191
2192
# [ tokio ::test ( flavor = " multi_thread" ) ]
2193
async fn a_job_s_identity_is_a_signed_token_the_discovery_document_lets_anyone_verify ( ) {
2194
let h = hub ( ) .await;
2195
let app = & h.app;
2196
let ( st, disc) = call ( app, " GET" , " /.well-known/openid-configuration" , None , None ) .await;
2197
assert_eq! ( st, StatusCode:: OK , " {disc}" ) ;
2198
let disc: serde_json:: Value = serde_json:: from_str( & disc) .unwrap ( ) ;
2199
assert_eq! ( disc[ " issuer" ] , h.base.as_str ( ) ) ;
2200
assert_eq! ( disc[ " jwks_uri" ] , format! ( " {} /.well-known/jwks.json" , h.base) ) ;
2201
let ( st, jwks) = call ( app, " GET" , " /.well-known/jwks.json" , None , None ) .await;
2202
assert_eq! ( st, StatusCode:: OK , " {jwks}" ) ;
2203
let jwks: serde_json:: Value = serde_json:: from_str( & jwks) .unwrap ( ) ;
2204
assert_eq! ( jwks[ " keys" ] [ 0 ] [ " alg" ] , " RS256" ) ;
2205
let kid = jwks[ " keys" ] [ 0 ] [ " kid" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2206
2207
create ( app, " andy" , " demo" ) .await;
2208
let url = format! ( " {} /andy/demo" , h.base) ;
2209
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2210
let c = client ( & url, work.path ( ) ) ;
2211
write (
2212
work.path ( ) ,
2213
" .flow/pipeline.kdl" ,
2214
& pipeline ( " job \" deploy\" {\n sandbox \" alpine\" \n identity \" sts.amazonaws.com\" {\n mount-path \" /run/secrets/aws-token\" \n }\n exec \" deploy\" \n }\n " ) ,
2215
) ;
2216
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2217
c.snapshot ( " test" ) .unwrap ( ) ;
2218
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2219
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2220
let mut deploy = None ;
2221
for _ in 0 .. 50 {
2222
let Some ( got) = claim ( app) .await else {
2223
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2224
continue ;
2225
} ;
2226
if got[ " job" ] [ " name" ] = = " deploy" {
2227
deploy = Some ( got) ;
2228
break ;
2229
}
2230
}
2231
let got = deploy.expect ( " the deploy job" ) ;
2232
let secrets = got[ " secrets" ] .as_array ( ) .expect ( " secrets" ) ;
2233
assert_eq! ( secrets.len ( ) , 1 ) ;
2234
assert_eq! (
2235
( secrets[ 0 ] [ " name" ] .as_str ( ) , secrets[ 0 ] [ " path" ] .as_str ( ) ) ,
2236
( Some ( " identity:sts.amazonaws.com" ) , Some ( " /run/secrets/aws-token" ) )
2237
) ;
2238
let token = secrets[ 0 ] [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2239
let issuer = app.oidc ( ) .await.unwrap ( ) ;
2240
assert_eq! ( issuer.kid, kid, " the key the JWKS publishes signed it" ) ;
2241
let claims = issuer.verify ( & token, " sts.amazonaws.com" , db:: now_ms( ) ) .expect ( " verifies" ) ;
2242
assert_eq! ( claims[ " sub" ] , " repo:andy/demo:bookmark:main" ) ;
2243
assert_eq! ( claims[ " repository" ] , " andy/demo" ) ;
2244
assert_eq! ( claims[ " job" ] , " deploy" ) ;
2245
assert_eq! ( claims[ " run_number" ] , 1 ) ;
2246
assert_eq! ( claims[ " runner" ] , " test-1" ) ;
2247
assert_eq! ( claims[ " iss" ] , h.base.as_str ( ) ) ;
2248
assert! ( claims[ " state" ] .as_str ( ) .unwrap ( ) .len ( ) = = 64 ) ;
2249
2250
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2251
let records = serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " out" , " text" : format! ( " TOKEN={token} end" ) } ] } ) ;
2252
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2253
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2254
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2255
assert! ( log.iter ( ) .any ( | l | l.text = = " TOKEN=*** end" ) , " {log:?}" ) ;
2256
}
efd29a00 Andy McCall 1 month ago ↶
2257
2258
# [ tokio ::test ( flavor = " multi_thread" ) ]
2259
async fn jobs_are_offered_only_to_runners_of_their_architecture ( ) {
2260
let h = hub ( ) .await;
2261
let app = & h.app;
2262
create ( app, " andy" , " demo" ) .await;
2263
let url = format! ( " {} /andy/demo" , h.base) ;
2264
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2265
let c = client ( & url, work.path ( ) ) ;
2266
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " sandbox \" board\" {\n rootfs \" cas://blob/{ROOTFS}\" \n arch \" riscv64\" \n }\n job \" flash\" {\n sandbox \" board\" \n exec \" flash\" \n }\n " ) .replace ( " {ROOTFS}" , & " ab" .repeat ( 32 ) ) ) ;
2267
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2268
c.snapshot ( " test" ) .unwrap ( ) ;
2269
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2270
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2271
let claim_as = | arch : & 'static str , archs : serde_json:: Value| {
2272
let app = app.clone ( ) ;
2273
async move {
2274
let body = serde_json:: json! ( {
2275
" runner" : format! ( " {arch} -1" ) , " runtimes" : [ " microvm" ] ,
2276
" info" : { " arch" : arch, " os" : " linux" , " version" : " t" , " labels" : [ ] , " max_jobs" : 1 } ,
2277
" image_archs" : archs,
2278
} ) ;
2279
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await;
2280
match st {
2281
StatusCode:: NO_CONTENT => None ,
2282
StatusCode:: OK => {
2283
Some ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
2284
}
2285
other => panic! ( " claim: {other} {body}" ) ,
2286
}
2287
}
2288
} ;
2289
2290
2291
let rootfs = pipeline ( " " ) .split ( " cas://blob/" ) .nth ( 1 ) .unwrap ( ) [ .. 64 ] .to_string ( ) ;
2292
for _ in 0 .. 50 {
2293
if ! runs_of ( app, " andy" , " demo" ) .await.is_empty ( ) {
2294
break ;
2295
}
2296
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2297
}
2298
let runs = runs_of ( app, " andy" , " demo" ) .await;
2299
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
2300
let mut taken = vec! [ ] ;
2301
while let Some ( j) = claim_as ( " x86_64" , serde_json:: json! ( { rootfs.clone ( ) : " x86_64" } ) ) .await {
2302
taken.push ( j) ;
2303
}
2304
assert_eq! ( taken, [ " build" , " lint" ] , " test waits for build; flash is for riscv64" ) ;
2305
2306
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await.as_deref ( ) , Some ( " flash" ) ) ;
2307
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await, None ) ;
2308
let ( _ , queue) = call ( app, " GET" , " /admin/runners" , None , None ) .await;
2309
let _ = queue;
2310
}
4d2e44d7 Andy McCall 1 month ago ↶
2311
2312
2313
2314
2315
2316
# [ tokio ::test ( flavor = " multi_thread" ) ]
2317
async fn a_declined_job_goes_to_another_runner_or_ends_unplaceable ( ) {
2318
let h = hub ( ) .await;
2319
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2320
create ( & h.app, " andy" , " declines" ) .await;
2321
let url = format! ( " {} /andy/declines" , h.base) ;
2322
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2323
let c = client ( & url, work.path ( ) ) ;
2324
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2325
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2326
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2327
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2328
let run = runs_of ( & h.app, " andy" , " declines" ) .await[ 0 ] .clone ( ) ;
2329
let claim_as = | token : & 'static str , name : & 'static str | {
2330
let app = h.app.clone ( ) ;
2331
async move {
2332
let body = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
2333
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( body) ) .await;
2334
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) )
2335
}
2336
} ;
2337
let decline = | token : & 'static str , id : i64 , reason : & 'static str | {
2338
let app = h.app.clone ( ) ;
2339
async move {
2340
let body = serde_json:: json! ( { " reason" : reason } ) .to_string ( ) ;
2341
call ( & app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /decline" ) , Some ( token) , Some ( body) ) .await.0
2342
}
2343
} ;
2344
let job = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " a job" ) ;
2345
2346
assert_eq! ( decline ( " runner-two" , job, " not mine" ) .await, StatusCode:: CONFLICT , " only the runner it runs on may decline it" ) ;
2347
assert_eq! ( decline ( " runner-secret" , job, " the MicroVM tier needs /dev/kvm" ) .await, StatusCode:: NO_CONTENT ) ;
2348
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2349
assert_eq! ( ( j.status.as_str ( ) , j.runner.as_str ( ) ) , ( " queued" , " " ) ) ;
2350
assert_eq! ( j.waiting, " declined by earth-1: the MicroVM tier needs /dev/kvm; waiting for another runner" ) ;
2351
2352
let next = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " the other ready job" ) ;
2353
assert_ne! ( next, job) ;
2354
let page = call ( & h.app, " GET" , & format! ( " /andy/declines/flow/{} " , run.number) , None , None ) .await.1 ;
2355
assert! ( page.contains ( " Waiting: declined by earth-1: the MicroVM tier needs /dev/kvm" ) , " {page}" ) ;
2356
assert_eq! ( claim_as ( " runner-two" , " plex-1" ) .await, Some ( job) , " another runner takes it" ) ;
2357
assert_eq! ( decline ( " runner-two" , job, " rootfs 36fc4e323804 is not on this runner" ) .await, StatusCode:: NO_CONTENT ) ;
2358
assert_eq! ( claim_as ( " runner-three" , " narvi" ) .await, Some ( job) ) ;
2359
assert_eq! ( decline ( " runner-three" , job, " rootfs 36fc4e323804 is for x86_64; this runner is riscv64" ) .await, StatusCode:: NO_CONTENT ) ;
2360
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2361
assert_eq! ( j.status, " error" ) ;
2362
assert_eq! (
2363
j.error,
2364
" unplaceable: declined by 3 runners: earth-1: the MicroVM tier needs /dev/kvm; plex-1: rootfs 36fc4e323804 is not on this runner; narvi: rootfs 36fc4e323804 is for x86_64; this runner is riscv64"
2365
) ;
2366
let r = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex-1" ) .unwrap ( ) ;
2367
let declined = db:: flow:: declines_of_runner( & h.app.db, r.id, 10 ) .await.unwrap ( ) ;
2368
assert_eq! ( declined.len ( ) , 1 ) ;
2369
assert_eq! ( ( declined[ 0 ] .job_id, declined[ 0 ] .reason.as_str ( ) ) , ( job, " rootfs 36fc4e323804 is not on this runner" ) ) ;
2370
assert_eq! ( r.tier_list ( ) , vec! [ " microvm" ] ) ;
2371
}
554ccacf Andy McCall 1 month ago ↶
2372
2373
2374
2375
2376
# [ tokio ::test ( flavor = " multi_thread" ) ]
2377
async fn a_coverage_artifact_becomes_a_coverage_report ( ) {
2378
let h = hub ( ) .await;
2379
create ( & h.app, " andy" , " cov" ) .await;
2380
let url = format! ( " {} /andy/cov" , h.base) ;
2381
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2382
let c = client ( & url, work.path ( ) ) ;
2383
write (
2384
work.path ( ) ,
2385
" .flow/pipeline.kdl" ,
2386
& format! (
2387
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" coverage\" {{ \n sandbox \" alpine\" \n exec \" make\" \" coverage\" \n output {{ \n artifact \" coverage\" {{ \n path \" /src/lcov.info\" \n }} \n }} \n }} \n "
2388
) ,
2389
) ;
2390
write ( work.path ( ) , " src/lib.rs" , " pub fn add(a: u32, b: u32) -> u32 {\n a + b\n }\n \n pub fn unused() {\n todo!()\n }\n " ) ;
2391
c.snapshot ( " test" ) .unwrap ( ) ;
2392
c.commit ( " Code and coverage" , & [ ] ) .unwrap ( ) ;
2393
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2394
let run = runs_of ( & h.app, " andy" , " cov" ) .await.remove ( 0 ) ;
c8df33a6 Andy McCall 27 days ago ↶
2395
assert! ( ! call ( & h.app, " GET" , " /andy/cov" , None , None ) .await.1. contains ( " >Reports</a>" ) , " no tab before a report" ) ;
554ccacf Andy McCall 1 month ago ↶
2396
let id = claim ( & h.app) .await.unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2397
2398
let ( st, text) = upload ( & h.app, id, " coverage" , b " this is not a report" ) .await;
2399
assert_eq! ( st, StatusCode:: OK , " a bad report never fails the upload: {text}" ) ;
2400
let lcov = " TN:\n SF:/src/src/lib.rs\n FN:1,add\n FN:5,unused\n FNDA:3,add\n FNDA:0,unused\n DA:1,3\n DA:2,3\n DA:5,0\n DA:6,0\n end_of_record\n \
2401
SF:/src/src/gone.rs\n DA:1,1\n end_of_record\n SF:/usr/lib/rustlib/x.rs\n DA:1,1\n end_of_record\n " ;
2402
let ( st, text) = upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await;
2403
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
2404
let log: Vec< String> = db:: flow:: log( & h.app.db, id, 0 , 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.text ) .collect ( ) ;
2405
assert! ( log.iter ( ) .any ( | l | l.contains ( " coverage: this is not an LCOV report" ) ) , " {log:?}" ) ;
2406
assert! (
2407
log.iter ( ) .any ( | l | l = = " coverage: 50.0% of lines (2 of 4) in 1 files (2 named files are not in this state and were left out)" ) ,
2408
" {log:?}"
2409
) ;
2410
2411
let ( st, page) = call ( & h.app, " GET" , " /andy/cov/coverage" , None , None ) .await;
2412
assert_eq! ( st, StatusCode:: OK ) ;
c987c865 Andy McCall 1 month ago ↶
2413
assert! (
c8df33a6 Andy McCall 27 days ago ↶
2414
page.contains ( " >Reports</a>" )
c987c865 Andy McCall 1 month ago ↶
2415
& & page.contains ( " 50.0%" )
2416
& & page.contains ( " <strong>2</strong> of <strong>4</strong> relevant lines covered" ) ,
2417
" {page}"
2418
) ;
554ccacf Andy McCall 1 month ago ↶
2419
assert! ( page.contains ( " 1 of 2" ) & & page.contains ( " /andy/cov/coverage/" ) & & page.contains ( " /file/src/lib.rs" ) , " {page}" ) ;
2420
let report = crate_report ( & h.app, run.id) .await;
2421
let ( st, file) = call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/lib.rs" ) , None , None ) .await;
2422
assert_eq! ( st, StatusCode:: OK , " {file}" ) ;
2423
assert_eq! ( file.matches ( " class=\" cov-hit\" " ) .count ( ) , 2 , " {file}" ) ;
2424
assert_eq! ( file.matches ( " class=\" cov-miss\" " ) .count ( ) , 2 , " {file}" ) ;
2425
assert! ( file.contains ( " 3×" ) , " {file}" ) ;
2426
assert_eq! ( call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/gone.rs" ) , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
2427
let ( _ , badge) = call ( & h.app, " GET" , " /andy/cov/coverage/badge.svg" , None , None ) .await;
2428
assert! ( badge.contains ( " coverage: 50.0%" ) , " {badge}" ) ;
2429
let ( _ , run_page) = call ( & h.app, " GET" , & format! ( " /andy/cov/flow/{} " , run.number) , None , None ) .await;
2430
assert! ( run_page.contains ( " coverage 50.0%" ) , " {run_page}" ) ;
617c2840 Andy McCall 1 month ago ↶
2431
2432
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/cov/coverage" , None , None ) .await;
2433
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
2434
assert_eq! ( ( list[ 0 ] [ " percent" ] .as_f64 ( ) , list[ 0 ] [ " format" ] .as_str ( ) ) , ( Some ( 50. 0 ) , Some ( " lcov" ) ) , " {list}" ) ;
2435
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/cov/coverage/{report} " ) , None , None ) .await;
2436
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
2437
assert_eq! ( ( one[ " files" ] [ 0 ] [ " path" ] .as_str ( ) , one[ " files" ] [ 0 ] [ " lines_hit" ] .as_i64 ( ) ) , ( Some ( " src/lib.rs" ) , Some ( 2 ) ) , " {one}" ) ;
7edaff01 Andy McCall 1 month ago ↶
2438
2439
2440
2441
let row = db:: repos:: get( & h.app.db, " andy" , " cov" ) .await.unwrap ( ) .unwrap ( ) ;
2442
let check = | checks : Vec< db:: hooks:: Check> | checks.into_iter ( ) .find ( | c | c.name = = " coverage" ) .expect ( " a coverage check" ) ;
2443
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
2444
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " passed" , " 50.0%" ) ) ;
2445
assert! ( c.url.ends_with ( & format! ( " /andy/cov/coverage?report={report} " ) ) , " {}" , c.url) ;
2446
oxygenhub:: coverage:: set_rules( & h.app.db, row.id, oxygenhub:: coverage:: Rules { min: Some ( 60. 0 ) , max_drop: None } ) .await.unwrap ( ) ;
2447
assert_eq! ( upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
2448
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
2449
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " failed" , " 50.0%: below the minimum of 60%" ) ) ;
554ccacf Andy McCall 1 month ago ↶
2450
}
2451
2452
async fn crate_report ( app : & Shared, run_id : i64 ) -> i64 {
2453
oxygenhub:: coverage:: for_run( & app.db, run_id) .await.unwrap ( ) [ 0 ] .id
2454
}
36e5a9c1 Andy McCall 1 month ago ↶
2455
2456
2457
2458
# [ tokio ::test ( flavor = " multi_thread" ) ]
2459
async fn junit_reports_make_a_tests_tab ( ) {
2460
let h = hub ( ) .await;
2461
create ( & h.app, " andy" , " t" ) .await;
2462
let url = format! ( " {} /andy/t" , h.base) ;
2463
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2464
let c = client ( & url, work.path ( ) ) ;
2465
write (
2466
work.path ( ) ,
2467
" .flow/pipeline.kdl" ,
2468
& format! (
2469
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n exec \" make\" \" test\" \n output {{ \n artifact \" junit\" {{ \n path \" /src/junit.xml\" \n }} \n }} \n }} \n "
2470
) ,
2471
) ;
c8df33a6 Andy McCall 27 days ago ↶
2472
assert! ( ! call ( & h.app, " GET" , " /andy/t" , None , None ) .await.1. contains ( " >Reports</a>" ) ) ;
36e5a9c1 Andy McCall 1 month ago ↶
2473
let case = | name : & str , ok : bool , secs : & str | {
2474
if ok {
2475
format! ( r # "<testcase classname="t.suite" name="{name} " time="{secs} "/>"# )
2476
} else {
2477
format! (
2478
r # "<testcase classname="t.suite" name="{name} " time="{secs} "><failure message="{name} broke">AssertionError</failure></testcase>"#
2479
)
2480
}
2481
} ;
2482
let runs = [
2483
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.5" ) , case ( " c" , true , " 0.2" ) ] ,
2484
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , false , " 2.6" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
2485
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.4" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
2486
] ;
2487
for ( i, cases) in runs.iter ( ) .enumerate ( ) {
2488
write ( work.path ( ) , " n.txt" , & i.to_string ( ) ) ;
2489
c.snapshot ( " test" ) .unwrap ( ) ;
2490
c.commit ( & format! ( " Run {i} " ) , & [ ] ) .unwrap ( ) ;
2491
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2492
2493
let mut a = None ;
2494
for _ in 0 .. 100 {
2495
a = claim ( & h.app) .await;
2496
if a.is_some ( ) {
2497
break ;
2498
}
2499
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2500
}
2501
let a = a.expect ( " a job to claim" ) ;
2502
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2503
let xml = format! ( " <testsuites><testsuite name=\" t\" >{} </testsuite></testsuites>" , cases.concat ( ) ) ;
2504
assert_eq! ( upload ( & h.app, id, " junit" , xml.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
2505
let failed = cases.iter ( ) .any ( | c | c.contains ( " <failure" ) ) ;
2506
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , failed as u32 ) ;
2507
if ! failed {
2508
r[ " outputs" ] = serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( xml.as_bytes ( ) ) .to_hex ( ) ] ) ;
2509
}
2510
let code = failed as i32 ;
2511
assert_eq! (
2512
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : code, " duration_ms" : 700 , " receipt" : r } ) ) .await,
2513
StatusCode:: NO_CONTENT
2514
) ;
2515
}
2516
let ( st, page) = call ( & h.app, " GET" , " /andy/t/tests" , None , None ) .await;
2517
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
2518
if let Some ( out) = std:: env:: var_os( " OXYHUB_DUMP_TESTS_PAGE" ) {
2519
std:: fs:: write( out, & page) .unwrap ( ) ;
2520
}
c8df33a6 Andy McCall 27 days ago ↶
2521
assert! ( page.contains ( " >Reports</a>" ) , " the tab" ) ;
36e5a9c1 Andy McCall 1 month ago ↶
2522
assert! ( page.contains ( " 100%" ) & & page.contains ( " of 4 tests passed" ) , " the latest run passes" ) ;
2523
assert! ( page.contains ( " 1 fixed" ) & & page.contains ( " t.suite::b" ) , " b fixed against the run before" ) ;
2524
assert! ( page.contains ( " 1 ✕" ) & & page.contains ( " 2 ✓" ) , " b is flaky: {page}" ) ;
2525
assert! ( page.contains ( " <svg class=\" cov-chart\" " ) , " charts" ) ;
2526
2527
let slow = page.find ( " Slowest tests" ) .unwrap ( ) ;
2528
assert! ( page[ slow.. ] .find ( " t.suite::b" ) .unwrap ( ) < page[ slow.. ] .find ( " t.suite::d" ) .unwrap ( ) ) ;
2529
2530
let row = db:: repos:: get( & h.app.db, " andy" , " t" ) .await.unwrap ( ) .unwrap ( ) ;
2531
let second = oxygenhub:: junit:: on_bookmark( & h.app.db, row.id, " main" , 10 ) .await.unwrap ( ) [ 1 ] .id;
2532
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/t/tests?report={second} " ) , None , None ) .await;
2533
assert! ( page.contains ( " 1 failing" ) & & page.contains ( " Newly failing" ) & & page.contains ( " b broke" ) , " {page}" ) ;
2534
assert! ( page.contains ( " 1 new tests" ) , " d is new" ) ;
617c2840 Andy McCall 1 month ago ↶
2535
2536
2537
2538
let ( st, list) = call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests" , None , None ) .await;
2539
assert_eq! ( st, StatusCode:: OK , " {list}" ) ;
2540
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
2541
assert_eq! ( list.as_array ( ) .unwrap ( ) .len ( ) , 3 ) ;
2542
assert_eq! ( ( list[ 0 ] [ " tests" ] .as_i64 ( ) , list[ 1 ] [ " failed" ] .as_i64 ( ) ) , ( Some ( 4 ) , Some ( 1 ) ) ) ;
2543
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} ?status=failed" ) , None , None ) .await;
2544
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
2545
assert_eq! ( one[ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 1 ) ;
2546
assert_eq! ( ( one[ " cases" ] [ 0 ] [ " name" ] .as_str ( ) , one[ " cases" ] [ 0 ] [ " message" ] .as_str ( ) ) , ( Some ( " b" ) , Some ( " b broke" ) ) ) ;
2547
let ( _ , all) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} " ) , None , None ) .await;
2548
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & all) .unwrap ( ) [ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 4 ) ;
2549
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests/99999" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
3d5c825a Andy McCall 1 month ago ↶
2550
2551
2552
let reports = oxygenhub:: junit:: on_bookmark( & h.app.db, row.id, " main" , 10 ) .await.unwrap ( ) ;
2553
let check = | state : String| {
2554
let app = h.app.clone ( ) ;
2555
async move { db:: hooks:: checks_for_state( & app.db, row.id, & state) .await.unwrap ( ) .into_iter ( ) .find ( | c | c.name = = " tests" ) .unwrap ( ) }
2556
} ;
2557
let c = check ( reports[ 1 ] .state.clone ( ) ) .await;
2558
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " failed" , " 1 failing; 1 new against main: t.suite::b" ) ) ;
2559
let c = check ( reports[ 0 ] .state.clone ( ) ) .await;
2560
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " passed" , " 4 passed" ) ) ;
2561
2562
2563
let ( _ , tab) = call ( & h.app, " GET" , " /andy/t/tests" , None , None ) .await;
2564
assert! (
2565
tab.contains ( " /andy/t/tests/case?classname=t.suite&name=b" ) | | tab.contains ( " /andy/t/tests/case?classname=t.suite&name=b" ) ,
2566
" linked"
2567
) ;
2568
let ( st, hist) = call ( & h.app, " GET" , " /andy/t/tests/case?classname=t.suite&name=b" , None , None ) .await;
2569
assert_eq! ( st, StatusCode:: OK , " {hist}" ) ;
2570
assert! ( hist.contains ( " 2 passed" ) & & hist.contains ( " 1 failed" ) & & hist.contains ( " b broke" ) , " {hist}" ) ;
2571
assert_eq! ( call ( & h.app, " GET" , " /andy/t/tests/case?classname=t.suite&name=nope" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
36e5a9c1 Andy McCall 1 month ago ↶
2572
}
67040b15 Andy McCall 27 days ago ↶
2573
2574
2575
2576
# [ tokio ::test ( flavor = " multi_thread" ) ]
2577
async fn a_job_no_runner_can_take_says_why ( ) {
2578
let h = hub ( ) .await;
2579
create ( & h.app, " andy" , " demo" ) .await;
2580
let url = format! ( " {} /andy/demo" , h.base) ;
2581
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2582
let c = client ( & url, work.path ( ) ) ;
2583
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2584
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2585
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2586
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2587
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
2588
2589
2590
let r = db:: runners:: configured( & h.app.db, & blake3:: hash( b " elsewhere" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
2591
db:: runners:: seen( & h.app.db, r.id, None , None ) .await.unwrap ( ) ;
2592
let other = " ab" .repeat ( 32 ) ;
92a797b2 Andy McCall 27 days ago ↶
2593
db:: runners:: set_offer( & h.app.db, r.id, & [ " microvm" .to_string ( ) ] , Some ( & [ other] ) , false , " p" ) .await.unwrap ( ) ;
67040b15 Andy McCall 27 days ago ↶
2594
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
2595
2596
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
2597
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
2598
let build = v[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | j | j[ " name" ] = = " build" ) .unwrap ( ) .clone ( ) ;
2599
let why = build[ " waiting" ] .as_str ( ) .unwrap_or_default ( ) .to_string ( ) ;
2600
assert! ( why.contains ( " earth-9 does not hold image" ) & & why.contains ( & ROOTFS [ .. 12 ] ) , " {body}" ) ;
2601
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/1" , None , None ) .await;
2602
assert! ( page.contains ( " data-waiting" ) & & page.contains ( " does not hold image" ) , " the run page says it" ) ;
2603
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
2604
let check = checks.iter ( ) .find ( | c | c.name = = " flow/build" ) .unwrap ( ) ;
2605
assert! ( check.summary.contains ( " does not hold image" ) , " {check:?}" ) ;
2606
}
92a797b2 Andy McCall 27 days ago ↶
2607
2608
async fn put_bytes ( app : & Shared, path : & str , bearer : & str , bytes : Vec< u8 > ) -> ( StatusCode, String) {
2609
let req = Request:: put( path)
2610
.header ( header:: AUTHORIZATION , format! ( " Bearer {bearer} " ) )
2611
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
2612
.body ( Body:: from( bytes) )
2613
.unwrap ( ) ;
2614
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
2615
let st = res.status ( ) ;
2616
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
2617
}
2618
2619
2620
2621
2622
2623
# [ tokio ::test ( flavor = " multi_thread" ) ]
2624
async fn images_are_sent_by_chunk_and_offered_to_runners_that_fetch ( ) {
2625
let h = hub ( ) .await;
2626
2627
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2628
2629
let mut image: Vec< u8 > = ( 0 .. 400_000 u32 ) .map ( | i | ( i % 251 ) as u8 ) .collect ( ) ;
2630
image.extend ( std:: iter:: repeat_n( 0 u8 , 2 < < 20 ) ) ;
2631
image.extend ( ( 0 .. 100_000 u32 ) .map ( | i | ( i % 239 ) as u8 ) ) ;
2632
let mut chunks = std:: collections:: HashMap:: new( ) ;
2633
let m = jet:: images:: scan( & image[ .. ] , Some ( " x86_64" .into ( ) ) , | c , b | {
2634
chunks.insert ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ;
2635
Ok ( ( ) )
2636
} )
2637
.unwrap ( ) ;
2638
let hash = m.hash.clone ( ) ;
2639
2640
2641
let ask = serde_json:: json! ( { " chunks" : m.needed ( ) } ) .to_string ( ) ;
2642
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( ask.clone ( ) ) ) .await;
2643
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2644
let missing: Vec< String> = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " missing" ]
2645
.as_array ( )
2646
.unwrap ( )
2647
.iter ( )
2648
.map ( | v | v.as_str ( ) .unwrap ( ) .to_string ( ) )
2649
.collect ( ) ;
2650
assert_eq! ( missing.len ( ) , m.needed ( ) .len ( ) ) ;
2651
let manifest = serde_json:: to_string( & m) .unwrap ( ) ;
2652
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest.clone ( ) ) ) .await;
2653
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " was not sent" ) , " {st} {body}" ) ;
2654
2655
let first = & missing[ 0 ] ;
2656
assert_eq! ( put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{first} " ) , " site" , vec! [ 1 , 2 , 3 ] ) .await.0 , StatusCode:: BAD_REQUEST ) ;
2657
assert_ne! (
2658
put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{first} " ) , " runner-secret" , chunks[ first] .clone ( ) ) .await.0 ,
2659
StatusCode:: NO_CONTENT
2660
) ;
2661
for c in & missing {
2662
let ( st, body) = put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{c} " ) , " site" , chunks[ c] .clone ( ) ) .await;
2663
assert_eq! ( st, StatusCode:: NO_CONTENT , " {body}" ) ;
2664
}
2665
let ( _ , body) = call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( ask) ) .await;
2666
assert! ( body.contains ( " \" missing\" :[]" ) , " {body}" ) ;
750579ec Andy McCall 26 days ago ↶
2667
2668
let many: Vec< String> = ( 0 .. 60_000 u32 ) .map ( | i | blake3:: hash( & i.to_le_bytes ( ) ) .to_hex ( ) .to_string ( ) ) .collect ( ) ;
2669
let ( st, body) =
2670
call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( serde_json:: json! ( { " chunks" : many } ) .to_string ( ) ) ) .await;
2671
assert_eq! ( st, StatusCode:: OK , " {}" , & body[ .. body.len ( ) .min ( 200 ) ] ) ;
2672
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " missing" ] .as_array ( ) .unwrap ( ) .len ( ) , 60_000 ) ;
92a797b2 Andy McCall 27 days ago ↶
2673
2674
let mut wrong = m.clone ( ) ;
2675
wrong.hash = " ab" .repeat ( 32 ) ;
2676
let ( st, _ ) =
2677
call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{} " , wrong.hash) , Some ( " site" ) , Some ( serde_json:: to_string( & wrong) .unwrap ( ) ) )
2678
.await;
2679
assert_eq! ( st, StatusCode:: CONFLICT , " chunks that make another image" ) ;
2680
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest) ) .await;
2681
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
2682
2683
2684
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " runner-secret" ) , None ) .await;
2685
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2686
let got: jet:: images:: Manifest = serde_json:: from_str( & body) .unwrap ( ) ;
2687
assert_eq! ( got, m) ;
2688
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
2689
let out = dir.path ( ) .join ( " img.ext4" ) ;
2690
let mut fetched = Vec :: new( ) ;
2691
for c in got.needed ( ) {
2692
let req = Request:: get( format! ( " /api/v1/flow/images/chunks/{c} " ) )
2693
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
2694
.body ( Body:: empty( ) )
2695
.unwrap ( ) ;
2696
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
2697
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
2698
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ;
2699
fetched.push ( ( c.to_string ( ) , bytes) ) ;
2700
}
2701
jet:: images:: assemble( & got, & out, | c | Ok ( fetched.iter ( ) .find ( | ( h , _) | h = = c.hash ( ) ) .unwrap ( ) .1. clone ( ) ) ) .unwrap ( ) ;
2702
assert_eq! ( std:: fs:: read( & out) .unwrap ( ) , image) ;
2703
assert_eq! (
2704
call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{} " , " cd" .repeat ( 32 ) ) , Some ( " runner-secret" ) , None ) .await.0 ,
2705
StatusCode:: NOT_FOUND
2706
) ;
2707
assert_ne! ( call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{hash} " ) , None , None ) .await.0 , StatusCode:: OK , " not anonymous" ) ;
2708
2709
2710
2711
create ( & h.app, " andy" , " demo" ) .await;
2712
let url = format! ( " {} /andy/demo" , h.base) ;
2713
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2714
let c = client ( & url, work.path ( ) ) ;
2715
write (
2716
work.path ( ) ,
2717
" .flow/pipeline.kdl" ,
2718
& format! ( " schema-version 1\n sandbox \" img\" {{ \n rootfs \" cas://blob/{hash} \" \n }} \n job \" build\" {{ \n sandbox \" img\" \n exec \" make\" \n }} \n " ) ,
2719
) ;
2720
c.commit ( " Build on the image" , & [ ] ) .unwrap ( ) ;
2721
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2722
runs_of ( & h.app, " andy" , " demo" ) .await;
1c8276b9 Andy McCall 26 days ago ↶
2723
2724
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2725
let run1 = db:: flow:: run( & h.app.db, row.id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
2726
let checks = checks_when ( & h.app, row.id, & run1.state, 1 ) .await;
2727
assert! ( checks.iter ( ) .all ( | c | c.name ! = " pipeline/images" ) , " {checks:?}" ) ;
92a797b2 Andy McCall 27 days ago ↶
2728
let ask = | fetches : bool | format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"images":[],"fetches_images":{fetches} }} "# ) ;
2729
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( false ) ) ) .await;
2730
assert_eq! ( st, StatusCode:: NO_CONTENT , " it lacks the image and does not fetch" ) ;
2731
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
2732
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
2733
assert! ( body.contains ( " does not hold image" ) , " {body}" ) ;
2734
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( true ) ) ) .await;
2735
assert_eq! ( st, StatusCode:: OK , " it fetches, and the hub holds the image: {body}" ) ;
7829b7c7 Andy McCall 26 days ago ↶
2736
2737
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
2738
assert! ( ! body.contains ( " waiting" ) , " a running job keeps no waiting reason: {body}" ) ;
92a797b2 Andy McCall 27 days ago ↶
2739
}
40dd3f86 Andy McCall 27 days ago ↶
2740
2741
2742
2743
2744
# [ tokio ::test ( flavor = " multi_thread" ) ]
2745
async fn coverage_uploaded_from_a_workstation_says_so ( ) {
2746
let h = hub ( ) .await;
2747
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2748
create ( & h.app, " andy" , " vk" ) .await;
2749
let url = format! ( " {} /andy/vk" , h.base) ;
2750
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2751
let c = client ( & url, work.path ( ) ) ;
2752
write ( work.path ( ) , " src/game.c" , " int a;\n int b;\n int c;\n int d;\n " ) ;
2753
c.commit ( " Game" , & [ ] ) .unwrap ( ) ;
2754
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2755
2756
let lcov = " SF:src/game.c\n DA:1,3\n DA:2,1\n DA:3,0\n DA:4,5\n end_of_record\n SF:reference/rom.asm\n DA:1,1\n end_of_record\n " ;
2757
let body = serde_json:: json! ( { " bookmark" : " main" , " content" : lcov } ) .to_string ( ) ;
2758
let ( st, out) = call ( & h.app, " POST" , " /api/v1/repos/andy/vk/coverage" , Some ( " site" ) , Some ( body) ) .await;
2759
assert_eq! ( st, StatusCode:: CREATED , " {out}" ) ;
2760
let v: serde_json:: Value = serde_json:: from_str( & out) .unwrap ( ) ;
2761
assert_eq! ( ( v[ " uploaded_by" ] .as_str ( ) , v[ " lines_hit" ] .as_i64 ( ) , v[ " lines_found" ] .as_i64 ( ) ) , ( Some ( " andy" ) , Some ( 3 ) , Some ( 4 ) ) ) ;
2762
assert_eq! ( v[ " dropped" ] , 1 , " a file outside the state is left out" ) ;
2763
assert_eq! ( v[ " bookmark" ] , " main" ) ;
2764
2765
let ( _ , page) = call ( & h.app, " GET" , " /andy/vk/coverage" , None , None ) .await;
2766
assert! ( page.contains ( " uploaded by andy" ) & & page.contains ( " 75.0%" ) , " the Coverage page says where it came from" ) ;
2767
let ( _ , page) = call ( & h.app, " GET" , " /andy/vk/reports" , None , None ) .await;
2768
assert! ( page.contains ( " uploaded by andy" ) , " and the Reports card" ) ;
2769
let row = db:: repos:: get( & h.app.db, " andy" , " vk" ) .await.unwrap ( ) .unwrap ( ) ;
2770
let state = v[ " state" ] .as_str ( ) .unwrap ( ) ;
2771
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, state) .await.unwrap ( ) ;
2772
assert! ( checks.iter ( ) .all ( | c | c.name ! = " coverage" ) , " an upload sets no check: {checks:?}" ) ;
2773
2774
2775
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/vk/coverage" , None , Some ( r # "{"content":"x"}"# .into ( ) ) ) .await;
2776
assert_ne! ( st, StatusCode:: CREATED ) ;
2777
let ( st, _ ) = call (
2778
& h.app,
2779
" POST" ,
2780
" /api/v1/repos/andy/vk/coverage" ,
2781
Some ( " site" ) ,
2782
Some ( r # "{"bookmark":"nope","content":"SF:a\nend_of_record\n"}"# .into ( ) ) ,
2783
)
2784
.await;
2785
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
2786
}
2fb70d01 Andy McCall 27 days ago ↶
2787
2788
2789
# [ tokio ::test ( flavor = " multi_thread" ) ]
2790
async fn a_runners_facts_are_kept_from_its_claim ( ) {
2791
let h = hub ( ) .await;
2792
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2793
let body = r # "{"runner":"test-1","runtimes":["microvm"],"facts":{"free_disk_mib":42000,"load_x100":112,"speed":100,"speed_class":3,"kernel":"7.0.0"}}"# ;
2794
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.into ( ) ) ) .await;
2795
assert_eq! ( st, StatusCode:: NO_CONTENT , " nothing to run, but the facts are kept" ) ;
2796
let ( st, out) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( " site" ) , None ) .await;
2797
assert_eq! ( st, StatusCode:: OK , " {out}" ) ;
2798
let v: serde_json:: Value = serde_json:: from_str( & out) .unwrap ( ) ;
2799
let r = & v[ 0 ] ;
2800
assert_eq! (
2801
( r[ " speed" ] .as_i64 ( ) , r[ " speed_class" ] .as_i64 ( ) , r[ " free_disk_mib" ] .as_i64 ( ) , r[ " load_x100" ] .as_i64 ( ) ) ,
2802
( Some ( 100 ) , Some ( 3 ) , Some ( 42000 ) , Some ( 112 ) )
2803
) ;
2804
let row = db:: runners:: list( & h.app.db) .await.unwrap ( ) .remove ( 0 ) ;
2805
assert_eq! ( row.facts_line ( ) , " speed 100 (class 3) · load 1.12 · 41 GiB free" ) ;
2806
}
44a3d2eb Andy McCall 27 days ago ↶
2807
2808
2809
2810
# [ tokio ::test ( flavor = " multi_thread" ) ]
2811
async fn a_jobs_next_run_needs_the_disk_its_last_took ( ) {
2812
let h = hub ( ) .await;
2813
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2814
create ( & h.app, " andy" , " demo" ) .await;
2815
let url = format! ( " {} /andy/demo" , h.base) ;
2816
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2817
let c = client ( & url, work.path ( ) ) ;
2818
write (
2819
work.path ( ) ,
2820
" .flow/pipeline.kdl" ,
2821
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
2822
) ;
2823
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
2824
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2825
runs_of ( & h.app, " andy" , " demo" ) .await;
2826
let claim_with = | free : u64 | format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"facts":{{ "free_disk_mib":{free} }} }} "# ) ;
2827
2828
2829
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 500 ) ) ) .await;
2830
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2831
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2832
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " peak_disk_mib" : 8000 , " peak_memory_mib" : 900 } ) ;
2833
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
2834
let job = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
2835
assert_eq! ( ( job.peak_disk_mib, job.peak_memory_mib) , ( Some ( 8000 ) , Some ( 900 ) ) ) ;
2836
2837
2838
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
2839
assert_eq! ( st, StatusCode:: CREATED ) ;
2840
let repo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2841
let run2 = db:: flow:: run( & h.app.db, repo.id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
2842
let next = db:: flow:: jobs( & h.app.db, run2.id) .await.unwrap ( ) .remove ( 0 ) ;
2843
assert_eq! ( next.need_disk_mib, Some ( 8000 + 2000 + 64 ) ) ;
2844
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 9000 ) ) ) .await;
2845
assert_eq! ( st, StatusCode:: NO_CONTENT , " 9000 MiB free is not enough" ) ;
2846
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
2847
let waiting = db:: flow:: job( & h.app.db, next.id) .await.unwrap ( ) .unwrap ( ) .waiting;
2848
assert! ( waiting.contains ( " 8.8 GiB free; the job needs ~9.8 GiB" ) , " {waiting}" ) ;
2849
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 20_000 ) ) ) .await;
2850
assert_eq! ( st, StatusCode:: OK , " a roomier runner takes it: {body}" ) ;
2851
}
501d8b26 Andy McCall 27 days ago ↶
2852
2853
2854
2855
2856
# [ tokio ::test ( flavor = " multi_thread" ) ]
2857
async fn a_job_waits_briefly_for_a_runner_that_holds_its_image ( ) {
2858
let h = hub ( ) .await;
2859
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2860
2861
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
2862
let ask = | images : & str | {
2863
format! (
2864
r # "{{ "runner":"r","runtimes":["microvm"],"images":[{images} ],"fetches_images":true,"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
2865
)
2866
} ;
2867
let holder = ask ( & format! ( " \" {ROOTFS} \" " ) ) ;
2868
2869
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder.clone ( ) ) ) .await;
2870
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2871
3b1bc27f Andy McCall 22 days ago
2872
sqlx:: query( " INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES ($1, 1, 'x86_64', 1, 'andy', 0)" )
501d8b26 Andy McCall 27 days ago ↶
2873
.bind ( ROOTFS )
2874
.execute ( & h.app.db)
2875
.await
2876
.unwrap ( ) ;
2877
2878
create ( & h.app, " andy" , " demo" ) .await;
2879
let url = format! ( " {} /andy/demo" , h.base) ;
2880
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2881
let c = client ( & url, work.path ( ) ) ;
2882
write (
2883
work.path ( ) ,
2884
" .flow/pipeline.kdl" ,
2885
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
2886
) ;
2887
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
2888
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2889
runs_of ( & h.app, " andy" , " demo" ) .await;
2890
2891
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( " " ) ) ) .await;
2892
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the runner that holds the image" ) ;
2893
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder) ) .await;
2894
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2895
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2896
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
2897
assert! ( placed.starts_with ( " holds the image, idle, speed 100 (class 3)" ) , " {placed}" ) ;
2898
assert! ( placed.contains ( " held for a better runner" ) , " {placed}" ) ;
2899
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
2900
assert! ( body.contains ( " \" placed\" :\" holds the image" ) , " {body}" ) ;
2901
}
20b8fe85 Andy McCall 26 days ago ↶
2902
4fa0da12 Andy McCall 25 days ago ↶
2903
2904
2905
# [ tokio ::test ( flavor = " multi_thread" ) ]
2906
async fn a_job_waits_briefly_for_the_runner_holding_its_build_cache ( ) {
2907
let h = hub ( ) .await;
2908
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2909
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
2910
let slot = flow_core:: api:: cache_slot( " andy/demo" , " build" ) ;
2911
let ask = | caches : & str | {
2912
format! (
2913
r # "{{ "runner":"r","runtimes":["microvm"],"images":["{ROOTFS} "],"fetches_images":true,"caches":[{caches} ],"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
2914
)
2915
} ;
2916
let holder = ask ( & format! ( " \" {slot} \" " ) ) ;
2917
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder.clone ( ) ) ) .await;
2918
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2919
2920
create ( & h.app, " andy" , " demo" ) .await;
2921
let url = format! ( " {} /andy/demo" , h.base) ;
2922
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2923
let c = client ( & url, work.path ( ) ) ;
2924
write (
2925
work.path ( ) ,
2926
" .flow/pipeline.kdl" ,
2927
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
2928
) ;
2929
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
2930
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2931
runs_of ( & h.app, " andy" , " demo" ) .await;
2932
2933
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( " " ) ) ) .await;
2934
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the runner holding the build cache" ) ;
2935
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder) ) .await;
2936
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2937
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2938
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
2939
assert! ( placed.starts_with ( " holds its build cache, holds the image" ) , " {placed}" ) ;
2940
}
2941
c2be148c Andy McCall 24 days ago ↶
2942
2943
2944
# [ tokio ::test ( flavor = " multi_thread" ) ]
2945
async fn a_job_waits_briefly_for_the_fresher_build_cache ( ) {
2946
let h = hub ( ) .await;
2947
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2948
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
2949
let slot = flow_core:: api:: cache_slot( " andy/demo" , " build" ) ;
2950
let now = db:: now_ms( ) ;
2951
let ask = | kept : i64 | {
2952
format! (
2953
r # "{{ "runner":"r","runtimes":["microvm"],"images":["{ROOTFS} "],"fetches_images":true,"caches":["{slot} "],"cache_times":{{ "{slot} ":{kept} }} ,"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
2954
)
2955
} ;
2956
2957
let fresh = ask ( now - 60_000 ) ;
2958
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( fresh.clone ( ) ) ) .await;
2959
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2960
2961
create ( & h.app, " andy" , " demo" ) .await;
2962
let url = format! ( " {} /andy/demo" , h.base) ;
2963
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2964
let c = client ( & url, work.path ( ) ) ;
2965
write (
2966
work.path ( ) ,
2967
" .flow/pipeline.kdl" ,
2968
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
2969
) ;
2970
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
2971
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2972
runs_of ( & h.app, " andy" , " demo" ) .await;
2973
2974
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( now - 86_400_000 ) ) ) .await;
2975
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the fresher copy" ) ;
2976
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( fresh) ) .await;
2977
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2978
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2979
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
2980
assert! ( placed.starts_with ( " holds its build cache (kept 1 min ago), holds the image" ) , " {placed}" ) ;
2981
}
2982
20b8fe85 Andy McCall 26 days ago ↶
2983
2984
2985
2986
# [ tokio ::test ( flavor = " multi_thread" ) ]
2987
async fn report_files_expire_under_retention ( ) {
2988
let h = hub ( ) .await;
2989
create ( & h.app, " andy" , " demo" ) .await;
2990
let repo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2991
let add = | bookmark : & 'static str , at : i64 | {
2992
let db = h.app.db.clone ( ) ;
2993
async move {
2994
let id: i64 = sqlx:: query_scalar(
2995
" INSERT INTO reports (repo_id, name, kind, title, summary, context, job, bookmark, state, files, bytes, created_ms) \
3b1bc27f Andy McCall 22 days ago
2996
VALUES ($1, 'perf', 'performance', 'Perf', '', '{}', 'perf', $2, 's', 1, 1, $3) RETURNING id" ,
20b8fe85 Andy McCall 26 days ago ↶
2997
)
2998
.bind ( repo.id)
2999
.bind ( bookmark)
3000
.bind ( at)
3001
.fetch_one ( & db)
3002
.await
3003
.unwrap ( ) ;
3b1bc27f Andy McCall 22 days ago
3004
sqlx:: query( " INSERT INTO report_files (report_id, path, blob, size) VALUES ($1, 'report.toml', $2, 1)" )
20b8fe85 Andy McCall 26 days ago ↶
3005
.bind ( id)
3006
.bind ( format! ( " {:064x}" , id) )
3007
.execute ( & db)
3008
.await
3009
.unwrap ( ) ;
3b1bc27f Andy McCall 22 days ago
3010
sqlx:: query( " INSERT INTO report_metrics (report_id, key, value, unit, better) VALUES ($1, 'ms', 1, '', '')" )
20b8fe85 Andy McCall 26 days ago ↶
3011
.bind ( id)
3012
.execute ( & db)
3013
.await
3014
.unwrap ( ) ;
3015
id
3016
}
3017
} ;
3018
let main_old = add ( " main" , 1 ) .await;
3019
add ( " main" , 2 ) .await;
3020
let mut feature = Vec :: new( ) ;
3021
for at in 0 .. 52 {
3022
feature.push ( add ( " feature" , 10 + at) .await) ;
3023
}
3024
assert_eq! ( oxygenhub:: reports:: expire( & h.app.db) .await.unwrap ( ) , 2 , " the two oldest feature runs" ) ;
3025
let files = | id : i64 | {
3026
let db = h.app.db.clone ( ) ;
3027
async move {
3b1bc27f Andy McCall 22 days ago
3028
sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM report_files WHERE report_id = $1" ) .bind ( id) .fetch_one ( & db) .await.unwrap ( )
20b8fe85 Andy McCall 26 days ago ↶
3029
}
3030
} ;
3031
assert_eq! ( ( files ( feature[ 0 ] ) .await, files ( feature[ 1 ] ) .await, files ( feature[ 2 ] ) .await) , ( 0 , 0 , 1 ) ) ;
3032
assert_eq! ( files ( main_old) .await, 1 , " the default bookmark keeps every run" ) ;
3033
assert_eq! ( oxygenhub:: reports:: metrics( & h.app.db, feature[ 0 ] ) .await.unwrap ( ) .len ( ) , 1 , " metrics are kept" ) ;
3034
assert_eq! ( oxygenhub:: reports:: expire( & h.app.db) .await.unwrap ( ) , 0 , " once" ) ;
3035
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/reports/perf?id={} " , feature[ 0 ] ) , None , None ) .await;
3036
assert! ( page.contains ( " files have expired" ) , " the page says so" ) ;
1c8276b9 Andy McCall 26 days ago ↶
3037
}
3038
3039
3040
3041
3042
# [ tokio ::test ( flavor = " multi_thread" ) ]
3043
async fn a_pipeline_pinning_an_image_nobody_holds_fails_a_check ( ) {
3044
let h = hub ( ) .await;
3045
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3046
3047
let none = r # "{"runner":"test-1","runtimes":["microvm"],"images":[]}"# ;
3048
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( none.into ( ) ) ) .await;
3049
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
3050
create ( & h.app, " andy" , " demo" ) .await;
3051
let url = format! ( " {} /andy/demo" , h.base) ;
3052
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3053
let c = client ( & url, work.path ( ) ) ;
3054
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3055
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3056
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3057
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3058
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
3059
let checks = checks_when ( & h.app, run.repo_id, & run.state, 4 ) .await;
3060
let check = checks.iter ( ) .find ( | c | c.name = = " pipeline/images" ) .expect ( " an images check" ) ;
3061
assert! ( check.status = = " failed" & & check.summary.contains ( & ROOTFS [ .. 12 ] ) & & check.summary.contains ( " flow rootfs push" ) , " {check:?}" ) ;
3062
3063
let holds = format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"images":["{ROOTFS} "]}} "# ) ;
3064
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( holds) ) .await;
3065
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
3066
assert_eq! ( st, StatusCode:: CREATED ) ;
3067
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
3068
let check = checks.iter ( ) .find ( | c | c.name = = " pipeline/images" ) .unwrap ( ) ;
3069
assert_eq! ( check.status, " passed" , " {check:?}" ) ;
20b8fe85 Andy McCall 26 days ago ↶
3070
}
a46d15e7 Andy McCall 26 days ago ↶
3071
3072
3073
3074
3075
# [ tokio ::test ( flavor = " multi_thread" ) ]
3076
async fn the_tracker_names_peers_in_the_same_site_and_signs_a_ticket ( ) {
3077
let h = hub ( ) .await;
3078
let app = & h.app;
3079
let image = " ab" .repeat ( 32 ) ;
3080
let claim_as = | token : & 'static str , name : & 'static str , body : serde_json:: Value| async move {
3081
let mut b = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) ;
3082
b.as_object_mut ( ) .unwrap ( ) .extend ( body.as_object ( ) .unwrap ( ) .clone ( ) ) ;
3083
let ( st, out) = call ( app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( b.to_string ( ) ) ) .await;
3084
assert_eq! ( st, StatusCode:: NO_CONTENT , " {out}" ) ;
3085
} ;
3086
let announce = | token : & 'static str , state : & 'static str | {
3087
let body = serde_json:: json! ( { " image" : image, " state" : state } ) .to_string ( ) ;
3088
async move { call ( app, " POST" , " /api/v1/flow/swarm/announce" , Some ( token) , Some ( body) ) .await }
3089
} ;
3090
6adcd68b Andy McCall 26 days ago ↶
3091
claim_as (
3092
" runner-secret" ,
3093
" a" ,
3094
serde_json:: json! ( { " images" : [ image] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " lab" , " cert" : " 11" .repeat ( 32 ) } } ) ,
3095
)
3096
.await;
3097
claim_as (
3098
" runner-two" ,
3099
" b" ,
3100
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.2:7401" , " site" : " lab" , " cert" : " 22" .repeat ( 32 ) } } ) ,
3101
)
3102
.await;
3103
claim_as (
3104
" runner-three" ,
3105
" c" ,
3106
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.3:7401" , " site" : " lab" , " cert" : " 33" .repeat ( 32 ) } } ) ,
3107
)
3108
.await;
a46d15e7 Andy McCall 26 days ago ↶
3109
3110
let ( st, body) = announce ( " runner-two" , " fetching" ) .await;
3111
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3112
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3113
let names: Vec< ( & str , bool ) > = a.peers.iter ( ) .map ( | p | ( p.addr.as_str ( ) , p.complete) ) .collect ( ) ;
3114
assert_eq! ( names, vec! [ ( " 10.0.0.1:7401" , true ) ] , " only A holds it so far" ) ;
6adcd68b Andy McCall 26 days ago ↶
3115
assert_eq! ( a.peers[ 0 ] .cert, " 11" .repeat ( 32 ) , " with the fingerprint peers check it by" ) ;
a46d15e7 Andy McCall 26 days ago ↶
3116
let t = jet:: swarm:: Ticket:: open( & a.ticket, & app.hub_key.identity, oxygenhub:: db:: now_ms( ) as u64 ) .unwrap ( ) ;
3117
assert_eq! ( ( t.name.as_str ( ) , t.site.as_str ( ) ) , ( " b" , " lab" ) ) ;
3118
3119
3120
let ( _ , body) = announce ( " runner-three" , " fetching" ) .await;
3121
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3122
let mut peers: Vec< ( & str , bool ) > = a.peers.iter ( ) .map ( | p | ( p.addr.as_str ( ) , p.complete) ) .collect ( ) ;
3123
peers.sort ( ) ;
3124
assert_eq! ( peers, vec! [ ( " 10.0.0.1:7401" , true ) , ( " 10.0.0.2:7401" , false ) ] ) ;
3125
3126
3127
announce ( " runner-two" , " done" ) .await;
6adcd68b Andy McCall 26 days ago ↶
3128
claim_as (
3129
" runner-three" ,
3130
" c" ,
3131
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.3:7401" , " site" : " elsewhere" , " cert" : " 33" .repeat ( 32 ) } } ) ,
3132
)
3133
.await;
a46d15e7 Andy McCall 26 days ago ↶
3134
let ( _ , body) = announce ( " runner-three" , " fetching" ) .await;
3135
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3136
assert! ( a.peers.is_empty ( ) , " {:?}" , a.peers) ;
6adcd68b Andy McCall 26 days ago ↶
3137
3138
3139
claim_as ( " runner-secret" , " a" , serde_json:: json! ( { " images" : [ image] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " elsewhere" } } ) )
3140
.await;
3141
let ( st, _ ) = announce ( " runner-secret" , " fetching" ) .await;
3142
assert_eq! ( st, StatusCode:: CONFLICT ) ;
a46d15e7 Andy McCall 26 days ago ↶
3143
3144
3145
claim_as ( " runner-two" , " b" , serde_json:: json! ( { " images" : [ ] } ) ) .await;
3146
let ( st, body) = announce ( " runner-two" , " fetching" ) .await;
3147
assert_eq! ( st, StatusCode:: CONFLICT ) ;
3148
assert! ( body.contains ( " [runner.swarm]" ) , " {body}" ) ;
3149
let ( st, _ ) =
3150
call ( app, " POST" , " /api/v1/flow/swarm/announce" , Some ( " runner-secret" ) , Some ( r # "{"image":"nope","state":"fetching"}"# .into ( ) ) )
3151
.await;
3152
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
3153
let ( st, _ ) = call (
3154
app,
3155
" POST" ,
3156
" /api/v1/flow/swarm/announce" ,
3157
None ,
3158
Some ( serde_json:: json! ( { " image" : image, " state" : " fetching" } ) .to_string ( ) ) ,
3159
)
3160
.await;
3161
assert_eq! ( st, StatusCode:: UNAUTHORIZED ) ;
3162
}
6ca0b971 Andy McCall 26 days ago ↶
3163
3164
3165
# [ tokio ::test ( flavor = " multi_thread" ) ]
3166
async fn the_images_page_says_where_each_image_is_and_what_uses_it ( ) {
3167
let h = hub ( ) .await;
3168
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3169
create ( & h.app, " andy" , " demo" ) .await;
3170
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3171
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
3172
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3173
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3174
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3175
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3176
3177
let held = " cd" .repeat ( 32 ) ;
3178
let kept = " ef" .repeat ( 32 ) ;
3179
let body = serde_json:: json! ( { " runner" : " earth-1" , " runtimes" : [ " wasm" ] , " images" : [ held] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " home" , " cert" : " 11" .repeat ( 32 ) } } ) ;
3180
let ( st, out) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await;
3181
assert! ( st = = StatusCode:: NO_CONTENT | | st = = StatusCode:: OK , " {st} {out}" ) ;
3b1bc27f Andy McCall 22 days ago
3182
sqlx:: query( " INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES ($1, 1048576, 'x86_64', 9, 'andy', 1)" )
6ca0b971 Andy McCall 26 days ago ↶
3183
.bind ( & kept)
3184
.execute ( & h.app.db)
3185
.await
3186
.unwrap ( ) ;
3187
3188
let res = oxygenhub:: router( h.app.clone ( ) )
3189
.oneshot (
3190
Request:: post( " /register" )
3191
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
3192
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery" ) )
3193
.unwrap ( ) ,
3194
)
3195
.await
3196
.unwrap ( ) ;
3197
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
3198
let carol = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
3199
db:: accounts:: set_admin( & h.app.db, carol.id, true ) .await.unwrap ( ) ;
3200
let res = oxygenhub:: router( h.app.clone ( ) )
3201
.oneshot ( Request:: get( " /admin/runners/images" ) .header ( header:: COOKIE , cookie) .body ( Body:: empty( ) ) .unwrap ( ) )
3202
.await
3203
.unwrap ( ) ;
3204
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
3205
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
3206
let row = | hash : & str | {
3207
let at = page.find ( & format! ( " title=\" {hash} \" " ) ) .unwrap_or_else ( | | panic! ( " {hash} is not on the page" ) ) ;
3208
let end = page[ at.. ] .find ( " </tr>" ) .unwrap ( ) ;
3209
page[ at.. at + end] .to_string ( )
3210
} ;
3211
let pinned = row ( ROOTFS ) ;
3212
assert! ( pinned.contains ( " nowhere" ) & & pinned.contains ( " andy/demo" ) & & pinned.contains ( " 3 jobs" ) , " {pinned}" ) ;
3213
let on_runner = row ( & held) ;
3214
assert! ( on_runner.contains ( " img-holds" ) & & on_runner.contains ( " earth-1" ) & & ! on_runner.contains ( " nowhere" ) , " {on_runner}" ) ;
3215
let on_hub = row ( & kept) ;
3216
assert! ( on_hub.contains ( " img-hub" ) & & on_hub.contains ( " 1.0 MiB" ) & & on_hub.contains ( " x86_64" ) , " {on_hub}" ) ;
3217
assert! ( page.contains ( " earth-1 (home)" ) , " the swarm is named" ) ;
6246d1e5 Andy McCall 25 days ago ↶
3218
3219
assert! ( page.contains ( r # "{"images":"# ) & & page.contains ( r # ""scene":"swarm""# ) & & page.contains ( r # ""earth-1""# ) , " the swarm's data" ) ;
6ca0b971 Andy McCall 26 days ago ↶
3220
assert_eq! ( call ( & h.app, " GET" , " /admin/runners/images" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
3221
}
f677d627 Andy McCall 26 days ago ↶
3222
3223
3224
3225
3226
# [ tokio ::test ( flavor = " multi_thread" ) ]
3227
async fn consumers_of_a_subrepo_are_indexed_with_how_far_behind_they_are ( ) {
3228
let h = hub ( ) .await;
e7158350 Andy McCall 26 days ago ↶
3229
3230
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
f677d627 Andy McCall 26 days ago ↶
3231
create ( & h.app, " andy" , " api" ) .await;
3232
create ( & h.app, " andy" , " game" ) .await;
3233
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
3234
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
3235
write ( api_work.path ( ) , " api.h" , " v1\n " ) ;
3236
api.snapshot ( " t" ) .unwrap ( ) ;
3237
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
3238
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3239
3240
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
3241
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
3242
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
3243
write ( game_work.path ( ) , " game.c" , " int main;\n " ) ;
3244
game.snapshot ( " t" ) .unwrap ( ) ;
3245
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
3246
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3247
3248
let consumers = | | async {
3249
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
3250
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3251
serde_json:: from_str:: < Vec< serde_json:: Value> > ( & body) .unwrap ( )
3252
} ;
3253
3254
3255
let settle = | want : & 'static ( dyn Fn( & [serde_json:: Value]) -> bool + Sync) | {
3256
let consumers = & consumers;
3257
async move {
3258
for _ in 0 .. 100 {
3259
let list = consumers ( ) .await;
3260
if want ( & list) {
3261
return list;
3262
}
3263
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3264
}
3265
consumers ( ) .await
3266
}
3267
} ;
3268
let list = settle ( & | l| ! l.is_empty ( ) ) .await;
3269
assert_eq! ( list.len ( ) , 1 , " {list:?}" ) ;
3270
assert_eq! (
3271
( list[ 0 ] [ " name" ] .as_str ( ) , list[ 0 ] [ " path" ] .as_str ( ) , list[ 0 ] [ " bookmark" ] .as_str ( ) ) ,
3272
( Some ( " game" ) , Some ( " deps/api" ) , Some ( " main" ) )
3273
) ;
3274
assert_eq! ( list[ 0 ] [ " behind" ] , 0 ) ;
3275
3276
3277
write ( api_work.path ( ) , " api.c" , " v2\n " ) ;
3278
api.snapshot ( " t" ) .unwrap ( ) ;
3279
api.commit ( " api v2" , & [ ] ) .unwrap ( ) ;
3280
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3281
assert_eq! ( settle ( & | l| l.first ( ) .is_some_and ( | c | c[ " behind" ] = = 1 ) ) .await[ 0 ] [ " behind" ] , 1 ) ;
3282
let ( _ , page) = call ( & h.app, " GET" , " /andy/api" , None , None ) .await;
c9aacc0e Andy McCall 25 days ago ↶
3283
assert! ( page.contains ( " Diffuses to" ) & & page.contains ( " andy/game" ) & & page.contains ( " 1 behind" ) , " the page shows it" ) ;
f677d627 Andy McCall 26 days ago ↶
3284
3285
3286
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'game'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
3287
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , None , None ) .await;
3288
assert_eq! ( body.trim ( ) , " []" ) ;
3289
let ( _ , page) = call ( & h.app, " GET" , " /andy/api" , None , None ) .await;
3290
assert! ( ! page.contains ( " andy/game" ) ) ;
3291
sqlx:: query( " UPDATE repositories SET visibility = 'public' WHERE name = 'game'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
e7158350 Andy McCall 26 days ago ↶
3292
1d634f69 Andy McCall 25 days ago ↶
3293
3294
3295
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
3296
let api_row = db:: repos:: get( & h.app.db, " andy" , " api" ) .await.unwrap ( ) .unwrap ( ) ;
3297
let api_heads = api.bookmark_heads ( ) .unwrap ( ) ;
3298
let bumped = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, api_heads.clone ( ) ) .await.unwrap ( ) ;
3299
let oxygenhub:: diffusion:: Bumped:: Moved { bookmark, heads, .. } = bumped else { panic! ( " {bumped:?}" ) } ;
3300
assert_eq! ( bookmark, " diffusion/deps/api" ) ;
3301
game.fetch ( " origin" ) .unwrap ( ) ;
3302
let bump_state = game.load_state ( & heads) .unwrap ( ) .materialize ( ) ;
3303
let pin = match & bump_state.files[ " deps/api" ] .content {
3304
oxy_core:: state:: FileContent:: Opaque { blobs } => oxy_core:: subrepo:: Pin:: decode( & game.read_blob ( & blobs[ 0 ] ) .unwrap ( ) ) .unwrap ( ) ,
3305
other => panic! ( " {other:?}" ) ,
3306
} ;
3307
assert! (
3308
matches! ( & pin, oxy_core:: subrepo:: Pin:: Oxy { heads, .. } if { let mut a = heads.clone ( ) ; a.sort ( ) ; let mut b = api_heads.clone ( ) ; b.sort ( ) ; a = = b } ) ,
3309
" {pin:?}"
3310
) ;
3311
assert! ( bump_state.files.contains_key ( " game.c" ) , " the rest of the game is as it was" ) ;
9aaa88b2 Andy McCall 25 days ago ↶
3312
assert! ( matches! (
1d634f69 Andy McCall 25 days ago ↶
3313
oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, api_heads.clone ( ) ) .await.unwrap ( ) ,
9aaa88b2 Andy McCall 25 days ago ↶
3314
oxygenhub:: diffusion:: Bumped:: Already { .. }
3315
) ) ;
7a14bdae Andy McCall 25 days ago ↶
3316
3317
3318
1d634f69 Andy McCall 25 days ago ↶
3319
write ( api_work.path ( ) , " other.c" , " v3\n " ) ;
3320
api.snapshot ( " t" ) .unwrap ( ) ;
3321
let v3 = api.commit ( " api v3, on its own" , & [ ] ) .unwrap ( ) ;
3322
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3323
let only_v3 = api.load_state ( & [ v3] ) .unwrap ( ) .heads ( ) .to_vec ( ) ;
7a14bdae Andy McCall 25 days ago ↶
3324
let combined = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, only_v3) .await.unwrap ( ) ;
3325
let oxygenhub:: diffusion:: Bumped:: Moved { heads: ch, .. } = combined else { panic! ( " {combined:?}" ) } ;
3326
game.fetch ( " origin" ) .unwrap ( ) ;
3327
let st = game.load_state ( & ch) .unwrap ( ) .materialize ( ) ;
3328
let pin = match & st.files[ " deps/api" ] .content {
3329
oxy_core:: state:: FileContent:: Opaque { blobs } => oxy_core:: subrepo:: Pin:: decode( & game.read_blob ( & blobs[ 0 ] ) .unwrap ( ) ) .unwrap ( ) ,
3330
other => panic! ( " {other:?}" ) ,
3331
} ;
3332
let oxy_core:: subrepo:: Pin:: Oxy { heads: pinned, .. } = pin else { panic! ( ) } ;
3333
let all = api.load_state ( & pinned) .unwrap ( ) ;
3334
3335
assert! ( all.contains ( & v3) & & all.len ( ) = = 2 , " v1 and v3, {} changes" , all.len ( ) ) ;
1d634f69 Andy McCall 25 days ago ↶
3336
3337
let newer = api.bookmark_heads ( ) .unwrap ( ) ;
3338
let again = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, newer) .await.unwrap ( ) ;
3339
assert! ( matches! ( again, oxygenhub:: diffusion:: Bumped:: Moved { ref bookmark, .. } if bookmark = = " diffusion/deps/api" ) , " {again:?}" ) ;
3340
e7158350 Andy McCall 26 days ago ↶
3341
3342
let put =
3343
| body : & 'static str , who : Option< & 'static str > | call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , who, Some ( body.into ( ) ) ) ;
3344
let ( st, body) = put ( r # "{"follow":"green"}"# , Some ( " site" ) ) .await;
3345
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3346
assert! ( body.contains ( " \" follow\" :\" green\" " ) & & body.contains ( " \" from\" :\" setting\" " ) , " {body}" ) ;
3347
assert_eq! ( put ( r # "{"follow":"sometimes"}"# , Some ( " site" ) ) .await.0 , StatusCode:: BAD_REQUEST ) ;
3348
assert_ne! ( put ( r # "{"follow":"any"}"# , None ) .await.0 , StatusCode:: OK , " not without a token" ) ;
3349
let ( _ , body) = put ( r # "{"follow":"default"}"# , Some ( " site" ) ) .await;
3350
assert! ( body.contains ( " \" follow\" :\" off\" " ) & & body.contains ( " \" from\" :\" site\" " ) , " {body}" ) ;
f677d627 Andy McCall 26 days ago ↶
3351
3352
3353
game.submodule_remove ( " deps/api" ) .unwrap ( ) ;
3354
std:: fs:: remove_dir_all( game_work.path ( ) .join ( " deps/api" ) ) .unwrap ( ) ;
3355
game.snapshot ( " t" ) .unwrap ( ) ;
3356
game.commit ( " game without api" , & [ ] ) .unwrap ( ) ;
3357
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3358
let left = settle ( & | l| l.is_empty ( ) ) .await;
3359
assert! ( left.is_empty ( ) , " {left:?}" ) ;
3360
}
a7e6e8e6 Andy McCall 25 days ago ↶
3361
3362
3363
3364
3365
# [ tokio ::test ( flavor = " multi_thread" ) ]
3366
async fn diffusion_proposes_a_bump_when_the_upstream_moves ( ) {
3367
let h = hub ( ) .await;
3368
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3369
create ( & h.app, " andy" , " api" ) .await;
3370
create ( & h.app, " andy" , " game" ) .await;
180dc43a Andy McCall 25 days ago ↶
3371
3372
let seen: std:: sync:: Mutex< Vec< serde_json:: Value> > = std:: sync:: Mutex:: new( Vec :: new( ) ) ;
3373
let seen = & seen;
3374
let hub_app = & h.app;
3375
let pass_all = move | | async move {
3376
let h_app = hub_app;
a7e6e8e6 Andy McCall 25 days ago ↶
3377
for _ in 0 .. 20 {
180dc43a Andy McCall 25 days ago ↶
3378
let Some ( a) = claim ( h_app) .await else {
a7e6e8e6 Andy McCall 25 days ago ↶
3379
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
180dc43a Andy McCall 25 days ago ↶
3380
if ! jobs_left ( h_app) .await {
a7e6e8e6 Andy McCall 25 days ago ↶
3381
break ;
3382
}
3383
continue ;
3384
} ;
180dc43a Andy McCall 25 days ago ↶
3385
seen.lock ( ) .unwrap ( ) .push ( a.clone ( ) ) ;
a7e6e8e6 Andy McCall 25 days ago ↶
3386
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3387
let ( name, ph) = ( a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
3388
assert_eq! (
180dc43a Andy McCall 25 days ago ↶
3389
finish ( h_app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, 0 ) } ) ) .await,
a7e6e8e6 Andy McCall 25 days ago ↶
3390
StatusCode:: NO_CONTENT
3391
) ;
3392
}
3393
} ;
3394
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
3395
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
3396
write ( api_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3397
write ( api_work.path ( ) , " test.sh" , " echo ok\n " ) ;
3398
write ( api_work.path ( ) , " api.h" , " v1\n " ) ;
3399
api.snapshot ( " t" ) .unwrap ( ) ;
3400
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
3401
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3402
pass_all ( ) .await;
3403
3404
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
3405
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
3406
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
3407
write ( game_work.path ( ) , " game.c" , " int main;\n " ) ;
f01cb865 Andy McCall 25 days ago ↶
3408
write ( game_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3409
write ( game_work.path ( ) , " test.sh" , " echo ok\n " ) ;
a7e6e8e6 Andy McCall 25 days ago ↶
3410
game.snapshot ( " t" ) .unwrap ( ) ;
3411
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
3412
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3413
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
3414
let bump = | | async {
3415
for _ in 0 .. 100 {
3416
let open = db:: proposals:: list( & h.app.db, game_row.id, & [ " open" ] ) .await.unwrap ( ) ;
3417
if let Some ( p) = open.into_iter ( ) .find ( | p | p.source_bookmark = = " diffusion/deps/api" ) {
3418
return Some ( p) ;
3419
}
3420
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3421
}
3422
None
3423
} ;
3424
3425
3426
write ( api_work.path ( ) , " a2.c" , " v2\n " ) ;
3427
api.snapshot ( " t" ) .unwrap ( ) ;
3428
api.commit ( " api v2: faster sprites" , & [ ] ) .unwrap ( ) ;
3429
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3430
pass_all ( ) .await;
3431
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
3432
assert! ( db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) .is_empty ( ) , " off proposes nothing" ) ;
3433
3776b70d Andy McCall 25 days ago ↶
3434
3435
a7e6e8e6 Andy McCall 25 days ago ↶
3436
let put = | body : & 'static str | call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( body.into ( ) ) ) ;
3437
assert_eq! ( put ( r # "{"follow":"any"}"# ) .await.0 , StatusCode:: OK ) ;
3776b70d Andy McCall 25 days ago ↶
3438
let first = bump ( ) .await.expect ( " caught up when turned on" ) ;
3439
assert! ( first.title.contains ( " 1 change" ) & & first.body.contains ( " api v2: faster sprites" ) , " {:?}" , first.title) ;
f01cb865 Andy McCall 25 days ago ↶
3440
3441
let mut ran = false ;
3442
for _ in 0 .. 100 {
3443
if runs_of ( & h.app, " andy" , " game" ) .await.iter ( ) .any ( | r | r.bookmark = = " diffusion/deps/api" ) {
3444
ran = true ;
3445
break ;
3446
}
3447
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3448
}
3449
assert! ( ran, " a run of the bump" ) ;
3450
pass_all ( ) .await;
a7e6e8e6 Andy McCall 25 days ago ↶
3451
write ( api_work.path ( ) , " a3.c" , " v3\n " ) ;
3452
api.snapshot ( " t" ) .unwrap ( ) ;
3453
api.commit ( " api v3: save slots past ten" , & [ ] ) .unwrap ( ) ;
3454
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3776b70d Andy McCall 25 days ago ↶
3455
let mut p = first.clone ( ) ;
3456
for _ in 0 .. 100 {
3457
p = bump ( ) .await.unwrap ( ) ;
3458
if p.body.contains ( " api v3" ) {
3459
break ;
3460
}
3461
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3462
}
3463
assert_eq! ( p.number, first.number, " the same proposal" ) ;
a7e6e8e6 Andy McCall 25 days ago ↶
3464
assert_eq! ( ( p.author.as_str ( ) , p.target.as_str ( ) ) , ( " diffusion" , " main" ) ) ;
3465
assert! ( p.title.contains ( " deps/api" ) & & p.title.contains ( " andy/api" ) & & p.title.contains ( " 2 changes" ) , " {}" , p.title) ;
3466
assert! ( p.body.contains ( " api v2: faster sprites" ) & & p.body.contains ( " api v3: save slots past ten" ) , " {}" , p.body) ;
3467
let labels = db:: issues:: proposal_labels( & h.app.db, p.id) .await.unwrap ( ) ;
3468
assert! ( labels.iter ( ) .any ( | l | l.name = = " dependencies" ) ) ;
3469
pass_all ( ) .await;
3470
3471
3472
3473
assert_eq! ( put ( r # "{"follow":"green"}"# ) .await.0 , StatusCode:: OK ) ;
3474
write ( api_work.path ( ) , " a4.c" , " v4\n " ) ;
3475
api.snapshot ( " t" ) .unwrap ( ) ;
3476
api.commit ( " api v4: rsm_set cheaper" , & [ ] ) .unwrap ( ) ;
3477
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3478
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
3479
assert! ( ! bump ( ) .await.unwrap ( ) .body.contains ( " api v4" ) , " not before its run passes" ) ;
3480
pass_all ( ) .await;
3481
let mut updated = None ;
3482
for _ in 0 .. 100 {
3483
let p2 = bump ( ) .await.unwrap ( ) ;
3484
if p2.body.contains ( " api v4" ) {
3485
updated = Some ( p2) ;
3486
break ;
3487
}
3488
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3489
}
3490
let p2 = updated.expect ( " brought up to date once the run passed" ) ;
3491
assert_eq! ( p2.number, p.number, " the same proposal" ) ;
3492
assert! ( p2.title.contains ( " 3 changes" ) , " {}" , p2.title) ;
3493
assert_eq! ( db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) .len ( ) , 1 , " never a pile of bumps" ) ;
3776b70d Andy McCall 25 days ago ↶
3494
757195ad Andy McCall 25 days ago ↶
3495
3496
3497
pass_all ( ) .await;
3498
let ( st, page) = call ( & h.app, " GET" , " /andy/api/diffusion" , None , None ) .await;
3499
assert_eq! ( st, StatusCode:: OK ) ;
c9aacc0e Andy McCall 25 days ago ↶
3500
assert! ( page.contains ( " Diffuses to" ) & & page.contains ( " andy/game" ) & & page.contains ( & format! ( " #{} " , p2.number) ) , " the bump is listed" ) ;
757195ad Andy McCall 25 days ago ↶
3501
assert! ( page.contains ( " df-green" ) , " a passing run of a checked upstream is green" ) ;
6246d1e5 Andy McCall 25 days ago ↶
3502
3503
assert! (
3504
page.contains ( r # ""scene":"ripples""# ) & & page.contains ( r # "["andy/game","# ) & & page.contains ( r # ""repo":"andy/api""# ) ,
3505
" the ripples' data"
3506
) ;
757195ad Andy McCall 25 days ago ↶
3507
let ( _ , page) = call ( & h.app, " GET" , " /andy/game/diffusion" , None , None ) .await;
c9aacc0e Andy McCall 25 days ago ↶
3508
assert! ( page.contains ( " Takes from" ) & & page.contains ( " andy/api" ) ) ;
757195ad Andy McCall 25 days ago ↶
3509
let ( _ , overview) = call ( & h.app, " GET" , " /andy/game" , None , None ) .await;
c9aacc0e Andy McCall 25 days ago ↶
3510
assert! ( overview.contains ( " Takes from" ) & & overview.contains ( " >Diffusion</a>" ) , " the tab and the panel" ) ;
eb2a3502 Andy McCall 25 days ago ↶
3511
3512
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , None ) .await;
3513
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3514
let d: hub_api:: Diffusion = serde_json:: from_str( & body) .unwrap ( ) ;
3515
assert! ( d.diffuses_to.is_empty ( ) , " nothing pins the game" ) ;
3516
let pin = d.takes_from.first ( ) .expect ( " the game takes from the api" ) ;
3517
assert_eq! ( ( pin.upstream.as_str ( ) , pin.consumer.as_str ( ) ) , ( " andy/api" , " andy/game" ) ) ;
3518
assert_eq! ( pin.bump, Some ( p2.number) ) ;
3519
assert_eq! ( pin.upstream_state.tone, " green" , " {body}" ) ;
3520
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/diffusion" , Some ( " site" ) , None ) .await;
3521
let d: hub_api:: Diffusion = serde_json:: from_str( & body) .unwrap ( ) ;
3522
assert! ( d.takes_from.is_empty ( ) & & d.diffuses_to.iter ( ) .any ( | p | p.consumer = = " andy/game" ) , " {body}" ) ;
757195ad Andy McCall 25 days ago ↶
3523
let ( st, _ ) = call ( & h.app, " GET" , " /admin/diffusion" , None , None ) .await;
3524
assert_eq! ( st, StatusCode:: NOT_FOUND , " administrators only" ) ;
3525
3526
5d54cf74 Andy McCall 22 days ago ↶
3527
3528
3529
let game = oxygenhub:: db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
3530
oxygenhub:: diffusion:: fail_next_records( & h.app, game.id, 1 ) ;
757195ad Andy McCall 25 days ago ↶
3531
let ( st, body) =
3532
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/game/proposals/{} /land" , p2.number) , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
3533
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3534
let mut behind = serde_json:: Value:: Null;
3535
for _ in 0 .. 100 {
3536
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
3537
let list: Vec< serde_json:: Value> = serde_json:: from_str( & list) .unwrap ( ) ;
3538
behind = list.iter ( ) .find ( | c | c[ " name" ] = = " game" ) .map ( | c | c[ " behind" ] .clone ( ) ) .unwrap_or_default ( ) ;
3539
if behind = = 0 {
3540
break ;
3541
}
3542
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3543
}
3544
assert_eq! ( behind, 0 , " the index followed the landing" ) ;
5d54cf74 Andy McCall 22 days ago ↶
3545
assert_eq! ( oxygenhub:: diffusion:: forced_left( & h.app, game.id) , 0 , " the first write did fail" ) ;
0e04dfc2 Andy McCall 22 days ago ↶
3546
3547
3548
3549
let pins_of = | | async {
3b1bc27f Andy McCall 22 days ago
3550
sqlx:: query_scalar:: < _ , String> ( " SELECT heads FROM diffusion_pins WHERE consumer_id = $1" )
0e04dfc2 Andy McCall 22 days ago ↶
3551
.bind ( game.id)
3552
.fetch_one ( & h.app.db)
3553
.await
3554
.unwrap ( )
3555
} ;
3556
let right = pins_of ( ) .await;
3b1bc27f Andy McCall 22 days ago
3557
sqlx:: query( " UPDATE diffusion_pins SET heads = 'stale' WHERE consumer_id = $1" ) .bind ( game.id) .execute ( & h.app.db) .await.unwrap ( ) ;
3558
sqlx:: query( " DELETE FROM diffusion_indexed WHERE consumer_id = $1" ) .bind ( game.id) .execute ( & h.app.db) .await.unwrap ( ) ;
0e04dfc2 Andy McCall 22 days ago ↶
3559
let r = oxygenhub:: diffusion:: reconcile( & h.app) .await;
3560
assert! ( r.reindexed > = 1 , " {r:?}" ) ;
3561
assert_eq! ( pins_of ( ) .await, right, " the pin read again from the game's bookmark" ) ;
3562
assert_eq! ( oxygenhub:: diffusion:: reconcile( & h.app) .await.reindexed, 0 , " nothing left to mend" ) ;
757195ad Andy McCall 25 days ago ↶
3563
180dc43a Andy McCall 25 days ago ↶
3564
3565
3566
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'api'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
3567
seen.lock ( ) .unwrap ( ) .clear ( ) ;
3568
write ( api_work.path ( ) , " a6.c" , " v6\n " ) ;
3569
api.snapshot ( " t" ) .unwrap ( ) ;
3570
api.commit ( " api v6" , & [ ] ) .unwrap ( ) ;
3571
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4fa0da12 Andy McCall 25 days ago ↶
3572
3573
3574
let mut bump_job = None ;
3575
for _ in 0 .. 100 {
3576
pass_all ( ) .await;
3577
bump_job = seen.lock ( ) .unwrap ( ) .iter ( ) .find ( | c | c[ " repo" ] = = " andy/game" & & c[ " bookmark" ] = = " diffusion/deps/api" ) .cloned ( ) ;
3578
if bump_job.is_some ( ) {
3579
break ;
3580
}
3581
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
3582
}
3583
let bump_job = bump_job.expect ( " the bump's job ran" ) ;
180dc43a Andy McCall 25 days ago ↶
3584
let subs: Vec< & str > = bump_job[ " subrepos" ] .as_array ( ) .unwrap ( ) .iter ( ) .filter_map ( | s | s[ " path" ] .as_str ( ) ) .collect ( ) ;
3585
assert_eq! ( subs, vec! [ " deps/api" ] , " {bump_job}" ) ;
3586
3776b70d Andy McCall 25 days ago ↶
3587
3588
3589
create ( & h.app, " andy" , " game2" ) .await;
3590
let g2_work = tempfile:: tempdir( ) .unwrap ( ) ;
3591
let g2 = client ( & format! ( " {} /andy/game2" , h.base) , g2_work.path ( ) ) ;
3592
g2.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
3593
g2.snapshot ( " t" ) .unwrap ( ) ;
3594
g2.commit ( " game2 on api" , & [ ] ) .unwrap ( ) ;
3595
assert! ( g2.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3596
3597
write ( api_work.path ( ) , " a5.c" , " v5\n " ) ;
3598
api.snapshot ( " t" ) .unwrap ( ) ;
3599
api.commit ( " api v5" , & [ ] ) .unwrap ( ) ;
7e01a766 Andy McCall 22 days ago ↶
3600
let api_row = db:: repos:: get( & h.app.db, " andy" , " api" ) .await.unwrap ( ) .unwrap ( ) ;
3601
let runs_before = db:: flow:: runs( & h.app.db, api_row.id, 100 , None ) .await.unwrap ( ) .len ( ) ;
3776b70d Andy McCall 25 days ago ↶
3602
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
7e01a766 Andy McCall 22 days ago ↶
3603
3604
3605
for _ in 0 .. 300 {
3606
if db:: flow:: runs( & h.app.db, api_row.id, 100 , None ) .await.unwrap ( ) .len ( ) > runs_before {
3607
break ;
3608
}
3609
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 20 ) ) .await;
3610
}
3776b70d Andy McCall 25 days ago ↶
3611
pass_all ( ) .await;
3612
let g2_row = db:: repos:: get( & h.app.db, " andy" , " game2" ) .await.unwrap ( ) .unwrap ( ) ;
3613
for _ in 0 .. 100 {
3b1bc27f Andy McCall 22 days ago
3614
if sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
3776b70d Andy McCall 25 days ago ↶
3615
.bind ( g2_row.id)
3616
.fetch_one ( & h.app.db)
3617
.await
3618
.unwrap ( )
3619
> 0
3620
{
3621
break ;
3622
}
3623
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
3624
}
3625
assert! ( db:: proposals:: list( & h.app.db, g2_row.id, & [ ] ) .await.unwrap ( ) .is_empty ( ) , " off until turned on" ) ;
3626
let ( st, _ ) = call ( & h.app, " PUT" , " /api/v1/repos/andy/game2/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green"}"# .into ( ) ) ) .await;
3627
assert_eq! ( st, StatusCode:: OK ) ;
3628
let mut caught = None ;
5d54cf74 Andy McCall 22 days ago ↶
3629
3630
for _ in 0 .. 300 {
3776b70d Andy McCall 25 days ago ↶
3631
if let Some ( p) = db:: proposals:: list( & h.app.db, g2_row.id, & [ " open" ] ) .await.unwrap ( ) .into_iter ( ) .next ( ) {
3632
caught = Some ( p) ;
3633
break ;
3634
}
3635
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3636
}
3637
let caught = caught.expect ( " bumped as soon as it was turned on" ) ;
3638
assert! ( caught.body.contains ( " api v5" ) & & caught.title.contains ( " 1 change" ) , " {:?} {:?}" , caught.title, caught.body) ;
0e04dfc2 Andy McCall 22 days ago ↶
3639
3b1bc27f Andy McCall 22 days ago
3640
sqlx:: query( " UPDATE proposals SET state = 'closed' WHERE id = $1" ) .bind ( caught.id) .execute ( & h.app.db) .await.unwrap ( ) ;
62cdf34d Andy McCall 22 days ago ↶
3641
3642
0e04dfc2 Andy McCall 22 days ago ↶
3643
let r = oxygenhub:: diffusion:: reconcile( & h.app) .await;
62cdf34d Andy McCall 22 days ago ↶
3644
assert_eq! ( r.caught_up, 0 , " the reconciler respects the closed bump: {r:?}" ) ;
a7e6e8e6 Andy McCall 25 days ago ↶
3645
}
3646
3647
3648
async fn jobs_left ( app : & Shared) -> bool {
3649
sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM flow_jobs WHERE status IN ('queued', 'running')" )
3650
.fetch_one ( & app.db)
3651
.await
3652
.unwrap_or ( 0 )
3653
> 0
9aaa88b2 Andy McCall 25 days ago ↶
3654
}
3655
3656
3657
3658
# [ tokio ::test ( flavor = " multi_thread" ) ]
3659
async fn bumps_from_an_upstream_without_checks_are_marked_unverified ( ) {
3660
let h = hub ( ) .await;
3661
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3662
create ( & h.app, " andy" , " lib" ) .await;
3663
create ( & h.app, " andy" , " app" ) .await;
3664
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
3665
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
3666
write ( lib_work.path ( ) , " lib.h" , " v1\n " ) ;
3667
lib.snapshot ( " t" ) .unwrap ( ) ;
3668
lib.commit ( " lib v1" , & [ ] ) .unwrap ( ) ;
3669
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3670
let app_work = tempfile:: tempdir( ) .unwrap ( ) ;
3671
let app = client ( & format! ( " {} /andy/app" , h.base) , app_work.path ( ) ) ;
3672
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
c4e432da Andy McCall 25 days ago ↶
3673
3674
let modules = std:: fs:: read_to_string( app_work.path ( ) .join ( " .oxymodules" ) ) .unwrap ( ) ;
3675
write ( app_work.path ( ) , " .oxymodules" , & format! ( " {modules} watch = lib.c\n " ) ) ;
9aaa88b2 Andy McCall 25 days ago ↶
3676
app.snapshot ( " t" ) .unwrap ( ) ;
3677
app.commit ( " app on lib" , & [ ] ) .unwrap ( ) ;
3678
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3679
let app_row = db:: repos:: get( & h.app.db, " andy" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
3680
for _ in 0 .. 100 {
3b1bc27f Andy McCall 22 days ago
3681
if sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
9aaa88b2 Andy McCall 25 days ago ↶
3682
.bind ( app_row.id)
3683
.fetch_one ( & h.app.db)
3684
.await
3685
.unwrap ( )
3686
> 0
3687
{
3688
break ;
3689
}
3690
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
3691
}
3692
let ( st, _ ) = call ( & h.app, " PUT" , " /api/v1/repos/andy/app/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green"}"# .into ( ) ) ) .await;
3693
assert_eq! ( st, StatusCode:: OK ) ;
3694
write ( lib_work.path ( ) , " lib.c" , " v2\n " ) ;
3695
lib.snapshot ( " t" ) .unwrap ( ) ;
3696
lib.commit ( " lib v2" , & [ ] ) .unwrap ( ) ;
3697
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3698
let mut found = None ;
3699
for _ in 0 .. 100 {
3700
if let Some ( p) = db:: proposals:: list( & h.app.db, app_row.id, & [ " open" ] ) .await.unwrap ( ) .into_iter ( ) .next ( ) {
3701
found = Some ( p) ;
3702
break ;
3703
}
3704
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3705
}
3706
let p = found.expect ( " a bump although the upstream never goes green" ) ;
3707
assert! ( p.body.contains ( " Upstream unverified" ) , " {}" , p.body) ;
c4e432da Andy McCall 25 days ago ↶
3708
assert! ( p.body.contains ( " Touches what you watch" ) & & p.body.contains ( " touches `lib.c`" ) & & p.body.contains ( " ⚑" ) , " {}" , p.body) ;
9aaa88b2 Andy McCall 25 days ago ↶
3709
let mut labels = Vec :: new( ) ;
3710
for _ in 0 .. 100 {
3711
labels = db:: issues:: proposal_labels( & h.app.db, p.id) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.name ) .collect:: < Vec< String> > ( ) ;
3712
if labels.len ( ) = = 2 {
3713
break ;
3714
}
3715
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
3716
}
3717
assert! ( labels.contains ( & " upstream unverified" .to_string ( ) ) & & labels.contains ( & " dependencies" .to_string ( ) ) , " {labels:?}" ) ;
a7e6e8e6 Andy McCall 25 days ago ↶
3718
}
d943160d Andy McCall 25 days ago ↶
3719
3720
3721
3722
# [ tokio ::test ( flavor = " multi_thread" ) ]
3723
async fn a_moved_target_runs_its_open_proposals_again ( ) {
3724
let h = hub ( ) .await;
3725
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3726
create ( & h.app, " andy" , " demo" ) .await;
3727
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3728
let mut c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
3729
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3730
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3731
c.snapshot ( " t" ) .unwrap ( ) ;
3732
let base = c.commit ( " base" , & [ ] ) .unwrap ( ) ;
3733
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3734
3735
for ( b, file) in [ ( " fix-a" , " a.txt" ) , ( " fix-b" , " b.txt" ) ] {
3736
c.bookmark_create ( b, Some ( & [ base] ) ) .unwrap ( ) ;
3737
c.bookmark_switch ( b, true ) .unwrap ( ) ;
3738
write ( work.path ( ) , file, " x\n " ) ;
3739
c.snapshot ( " t" ) .unwrap ( ) ;
3740
c.commit ( & format! ( " add {file} " ) , & [ ] ) .unwrap ( ) ;
3741
assert! ( c.push_bookmark ( " origin" , b, false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3742
let body = serde_json:: json! ( { " title" : b, " target" : " main" , " source" : b } ) .to_string ( ) ;
3743
let ( st, out) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/proposals" , Some ( " site" ) , Some ( body) ) .await;
3744
assert_eq! ( st, StatusCode:: CREATED , " {out}" ) ;
3745
}
3746
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
3747
let props = db:: proposals:: list( & h.app.db, row.id, & [ " open" ] ) .await.unwrap ( ) ;
3748
let a = props.iter ( ) .find ( | p | p.source_bookmark = = " fix-a" ) .unwrap ( ) .number;
3749
let b = props.iter ( ) .find ( | p | p.source_bookmark = = " fix-b" ) .unwrap ( ) .number;
3750
3751
let ( st, out) = call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/demo/proposals/{a} /land" ) , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
3752
assert_eq! ( st, StatusCode:: OK , " {out}" ) ;
3753
let label = format! ( " proposal/{b} " ) ;
3754
let mut found = None ;
3755
for _ in 0 .. 100 {
3756
if let Some ( r) = runs_of ( & h.app, " andy" , " demo" ) .await.into_iter ( ) .find ( | r | r.bookmark = = label ) {
3757
found = Some ( r) ;
3758
break ;
3759
}
3760
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3761
}
3762
let run = found.expect ( " a run of fix-b's new projected state" ) ;
3763
let projected = db:: proposals:: get( & h.app.db, row.id, b) .await.unwrap ( ) .unwrap ( ) ;
3764
assert_eq! ( ( run.trigger.as_str ( ) , run.state.as_str ( ) ) , ( " proposal" , projected.projected_state.as_str ( ) ) , " on its projected state" ) ;
3765
3766
let before = runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) ;
3767
oxygenhub:: web:: proposals:: settle_after_push( & h.app, & row, " main" , " andy" ) .await;
3768
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , before, " not again for the same state" ) ;
3769
}
93501da2 Andy McCall 24 days ago ↶
3770
3771
3772
3773
async fn send_image ( app : & Shared, image : & [u8 ]) -> ( String, serde_json:: Value) {
3774
let mut chunks = std:: collections:: HashMap:: new( ) ;
3775
let m = jet:: images:: scan( image, None , | c , b | {
3776
chunks.insert ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ;
3777
Ok ( ( ) )
3778
} )
3779
.unwrap ( ) ;
3780
for c in m.needed ( ) {
3781
let ( st, body) = put_bytes ( app, & format! ( " /api/v1/flow/images/chunks/{c} " ) , " site" , chunks[ c] .clone ( ) ) .await;
3782
assert_eq! ( st, StatusCode:: NO_CONTENT , " {body}" ) ;
3783
}
3784
let ( st, body) =
3785
call ( app, " PUT" , & format! ( " /api/v1/flow/images/{} " , m.hash) , Some ( " site" ) , Some ( serde_json:: to_string( & m) .unwrap ( ) ) ) .await;
3786
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
3787
( m.hash.clone ( ) , serde_json:: from_str( & body) .unwrap ( ) )
3788
}
3789
3790
3791
3792
3793
3794
# [ tokio ::test ( flavor = " multi_thread" ) ]
3795
async fn an_image_names_its_own_architecture_and_a_pass_teaches_the_rest ( ) {
3796
use oxy_core:: identity:: SecretKey;
3797
let h = hub ( ) .await;
3798
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3799
3800
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
3801
let tree = dir.path ( ) .join ( " tree" ) ;
3802
std:: fs:: create_dir_all( tree.join ( " bin" ) ) .unwrap ( ) ;
3803
let mut elf = vec! [ 0 u8 ; 64 ] ;
3804
elf[ .. 7 ] .copy_from_slice ( b " \x7f ELF\x02 \x01 \x01 " ) ;
3805
elf[ 18 .. 20 ] .copy_from_slice ( & 0x3E u16 .to_le_bytes ( ) ) ;
3806
std:: fs:: write( tree.join ( " bin/sh" ) , & elf) .unwrap ( ) ;
3807
let img = dir.path ( ) .join ( " rootfs.ext4" ) ;
3808
let made = std:: process:: Command:: new( " mkfs.ext4" ) .args ( [ " -q" , " -F" , " -d" ] ) .arg ( & tree) .arg ( & img) .arg ( " 8M" ) .status ( ) ;
3809
if ! made.is_ok_and ( | s | s.success ( ) ) {
3810
eprintln! ( " mkfs.ext4 not found: skipped" ) ;
3811
return ;
3812
}
3813
let ( x86, answer) = send_image ( & h.app, & std:: fs:: read( & img) .unwrap ( ) ) .await;
3814
assert_eq! ( answer[ " arch" ] , " x86_64" , " read from its shell: {answer}" ) ;
3815
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{x86} " ) , Some ( " runner-secret" ) , None ) .await;
3816
assert_eq! ( serde_json:: from_str:: < jet:: images:: Manifest> ( & body) .unwrap ( ) .arch.as_deref ( ) , Some ( " x86_64" ) , " runners fetch it named" ) ;
3817
3818
let blob: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
3819
let ( unread, answer) = send_image ( & h.app, & blob) .await;
3820
assert! ( answer[ " arch" ] .is_null ( ) , " {answer}" ) ;
3821
let archs = db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) ;
3822
assert_eq! ( ( archs.get ( & x86) .map ( String :: as_str) , archs.get ( & unread) ) , ( Some ( " x86_64" ) , None ) ) ;
3823
3824
sqlx:: query( " UPDATE jet_images SET arch = ''" ) .execute ( & h.app.db) .await.unwrap ( ) ;
3825
sqlx:: query( " DELETE FROM flow_images" ) .execute ( & h.app.db) .await.unwrap ( ) ;
3826
assert_eq! ( oxygenhub:: images:: learn_archs( & h.app) .await.unwrap ( ) , 1 ) ;
3827
assert_eq! ( db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) .get ( & x86) .map ( String :: as_str) , Some ( " x86_64" ) ) ;
3828
3829
3830
create ( & h.app, " andy" , " arch" ) .await;
3831
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3832
let c = client ( & format! ( " {} /andy/arch" , h.base) , work.path ( ) ) ;
3833
let kdl = format! (
3834
" schema-version 1\n sandbox \" unread\" {{ \n rootfs \" cas://blob/{unread} \" \n }} \n sandbox \" x86\" {{ \n rootfs \" cas://blob/{x86} \" \n }} \n \
3835
job \" first\" {{ \n sandbox \" unread\" \n exec \" make\" \n }} \n job \" second\" {{ \n sandbox \" x86\" \n exec \" make\" \n depends-on \" first\" \n }} \n \
3836
job \" third\" {{ \n sandbox \" unread\" \n exec \" make\" \n depends-on \" first\" \n }} \n "
3837
) ;
3838
write ( work.path ( ) , " .flow/pipeline.kdl" , & kdl) ;
3839
c.commit ( " Jobs on two images" , & [ ] ) .unwrap ( ) ;
3840
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3841
runs_of ( & h.app, " andy" , " arch" ) .await;
3842
3843
3844
let x86_info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
3845
let x86_claim =
3846
serde_json:: json! ( { " runner" : " earth" , " runtimes" : [ " microvm" ] , " images" : [ ] , " fetches_images" : true , " info" : x86_info } ) ;
3847
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( x86_claim.to_string ( ) ) ) .await;
3848
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3849
let first: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
3850
assert_eq! ( first[ " job" ] [ " name" ] , " first" ) ;
3851
let ph = first[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) ;
3852
let id = first[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3853
assert_eq! (
3854
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " first" , ph, 0 ) } ) ) .await,
3855
StatusCode:: NO_CONTENT
3856
) ;
3857
assert_eq! ( db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) .get ( & unread) .map ( String :: as_str) , Some ( " x86_64" ) , " learned" ) ;
3858
3859
3860
let token = " flowreg_narvi" ;
3861
db:: runners:: add_registration( & h.app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & Default :: default( ) ) .await.unwrap ( ) ;
3862
let identity = SecretKey:: generate( ) .identity ( " narvi" , " " , 1 ) ;
3863
let info = serde_json:: json! ( { " arch" : " riscv64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
3864
let reg = serde_json:: json! ( { " token" : token, " name" : " narvi" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
3865
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
3866
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3867
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
3868
let narvi =
3869
serde_json:: json! ( { " runner" : " narvi" , " runtimes" : [ " microvm" ] , " images" : [ ] , " fetches_images" : true , " info" : info } ) .to_string ( ) ;
3870
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( narvi) ) .await;
3871
assert_eq! ( st, StatusCode:: NO_CONTENT , " the second job's image is x86_64 and the third's learned so: {body}" ) ;
3872
for _ in 0 .. 2 {
3873
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( x86_claim.to_string ( ) ) ) .await;
3874
assert_eq! ( st, StatusCode:: OK , " both go to x86_64: {body}" ) ;
3875
}
3876
}
f4d6ef41 Andy McCall 24 days ago ↶
3877
3878
3879
3880
3881
3882
# [ tokio ::test ( flavor = " multi_thread" ) ]
3883
async fn a_consumer_that_chose_auto_lands_green_bumps_itself ( ) {
3884
let h = hub ( ) .await;
3885
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3886
create ( & h.app, " andy" , " api" ) .await;
3887
create ( & h.app, " andy" , " game" ) .await;
3888
let hub_app = & h.app;
3889
3890
let pass_all = move | | async move {
3891
for _ in 0 .. 20 {
3892
let Some ( a) = claim ( hub_app) .await else {
3893
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
3894
if ! jobs_left ( hub_app) .await {
3895
break ;
3896
}
3897
continue ;
3898
} ;
3899
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3900
let ( name, ph) = ( a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
3901
assert_eq! (
3902
finish ( hub_app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, 0 ) } ) ) .await,
3903
StatusCode:: NO_CONTENT
3904
) ;
3905
}
3906
} ;
3907
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
3908
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
3909
write ( api_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3910
write ( api_work.path ( ) , " test.sh" , " echo ok\n " ) ;
3911
api.snapshot ( " t" ) .unwrap ( ) ;
3912
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
3913
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3914
pass_all ( ) .await;
3915
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
3916
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
3917
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
3918
write ( game_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3919
write ( game_work.path ( ) , " test.sh" , " echo ok\n " ) ;
3920
game.snapshot ( " t" ) .unwrap ( ) ;
3921
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
3922
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3923
pass_all ( ) .await;
3924
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
3925
for _ in 0 .. 100 {
3b1bc27f Andy McCall 22 days ago
3926
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 24 days ago ↶
3927
.bind ( game_row.id)
3928
.fetch_one ( & h.app.db)
3929
.await
3930
.unwrap ( ) ;
3931
if n > 0 {
3932
break ;
3933
}
3934
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
3935
}
3936
let ( st, body) =
3937
call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green","land":"auto"}"# .into ( ) ) ) .await;
3938
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3939
assert! ( body.contains ( r # ""land":"auto""# ) , " {body}" ) ;
3940
let ( st, _ ) =
3941
call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green","land":"soon"}"# .into ( ) ) ) .await;
3942
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
3943
3944
3945
3946
write ( api_work.path ( ) , " a2.c" , " v2\n " ) ;
3947
api.snapshot ( " t" ) .unwrap ( ) ;
3948
api.commit ( " api v2: faster sprites" , & [ ] ) .unwrap ( ) ;
3949
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3950
let mut landed = None ;
3951
for _ in 0 .. 100 {
3952
pass_all ( ) .await;
3953
let all = db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) ;
3954
if let Some ( p) = all.into_iter ( ) .find ( | p | p.source_bookmark = = " diffusion/deps/api" & & p.state = = " landed" ) {
3955
landed = Some ( p) ;
3956
break ;
3957
}
3958
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3959
}
3960
let p = landed.expect ( " landed by itself once both halves were green" ) ;
3961
assert! ( p.body.contains ( " lands its bumps itself" ) , " {}" , p.body) ;
3962
let events = db:: proposals:: events( & h.app.db, p.id) .await.unwrap ( ) ;
3963
let landing = events.iter ( ) .find ( | e | e.event = = " landed" ) .expect ( " a landing on the timeline" ) ;
3964
assert_eq! ( landing.actor, " diffusion" ) ;
3965
assert! ( landing.detail.contains ( " Diffusion, both halves green" ) , " {}" , landing.detail) ;
3966
3967
let mut behind = serde_json:: Value:: Null;
3968
for _ in 0 .. 100 {
3969
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
3970
let list: Vec< serde_json:: Value> = serde_json:: from_str( & list) .unwrap ( ) ;
3971
behind = list.iter ( ) .find ( | c | c[ " name" ] = = " game" ) .map ( | c | c[ " behind" ] .clone ( ) ) .unwrap_or_default ( ) ;
3972
if behind = = 0 {
3973
break ;
3974
}
3975
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3976
}
3977
assert_eq! ( behind, 0 , " main moved to the bump" ) ;
3978
3979
3980
3981
create ( & h.app, " andy" , " lib" ) .await;
3982
create ( & h.app, " andy" , " app" ) .await;
3983
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
3984
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
3985
write ( lib_work.path ( ) , " lib.h" , " v1\n " ) ;
3986
lib.snapshot ( " t" ) .unwrap ( ) ;
3987
lib.commit ( " lib v1" , & [ ] ) .unwrap ( ) ;
3988
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3989
let app_work = tempfile:: tempdir( ) .unwrap ( ) ;
3990
let app = client ( & format! ( " {} /andy/app" , h.base) , app_work.path ( ) ) ;
3991
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
3992
let modules = std:: fs:: read_to_string( app_work.path ( ) .join ( " .oxymodules" ) ) .unwrap ( ) ;
3993
write ( app_work.path ( ) , " .oxymodules" , & format! ( " {modules} follow = green\n land = auto\n " ) ) ;
3994
write ( app_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3995
write ( app_work.path ( ) , " test.sh" , " echo ok\n " ) ;
3996
app.snapshot ( " t" ) .unwrap ( ) ;
3997
app.commit ( " app on lib" , & [ ] ) .unwrap ( ) ;
3998
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3999
pass_all ( ) .await;
4000
let app_row = db:: repos:: get( & h.app.db, " andy" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
4001
for _ in 0 .. 100 {
3b1bc27f Andy McCall 22 days ago
4002
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 24 days ago ↶
4003
.bind ( app_row.id)
4004
.fetch_one ( & h.app.db)
4005
.await
4006
.unwrap ( ) ;
4007
if n > 0 {
4008
break ;
4009
}
4010
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4011
}
4012
write ( lib_work.path ( ) , " lib.c" , " v2\n " ) ;
4013
lib.snapshot ( " t" ) .unwrap ( ) ;
4014
lib.commit ( " lib v2" , & [ ] ) .unwrap ( ) ;
4015
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4016
let mut passed = false ;
4017
for _ in 0 .. 100 {
4018
pass_all ( ) .await;
4019
let s: Option< String> = sqlx:: query_scalar(
3b1bc27f Andy McCall 22 days ago
4020
" SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = 'diffusion/deps/lib' ORDER BY id DESC LIMIT 1" ,
f4d6ef41 Andy McCall 24 days ago ↶
4021
)
4022
.bind ( app_row.id)
4023
.fetch_optional ( & h.app.db)
4024
.await
4025
.unwrap ( ) ;
4026
if s.as_deref ( ) = = Some ( " passed" ) {
4027
passed = true ;
4028
break ;
4029
}
4030
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4031
}
4032
assert! ( passed, " the app's run on the bump passed" ) ;
4033
assert_eq! ( oxygenhub:: diffusion:: auto_land( & h.app, & app_row, " deps/lib" ) .await, Err ( oxygenhub:: diffusion:: Held:: Unverified) ) ;
4034
let open = db:: proposals:: list( & h.app.db, app_row.id, & [ " open" ] ) .await.unwrap ( ) ;
4035
assert! ( open.iter ( ) .any ( | p | p.source_bookmark = = " diffusion/deps/lib" ) , " amber waits for a person" ) ;
4036
}
e8fa5369 Andy McCall 24 days ago ↶
4037
4038
4039
4040
4041
4042
# [ tokio ::test ( flavor = " multi_thread" ) ]
4043
async fn one_job_reruns_with_what_depends_on_it ( ) {
4044
let h = hub ( ) .await;
4045
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4046
create ( & h.app, " andy" , " flaky" ) .await;
4047
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4048
let c = client ( & format! ( " {} /andy/flaky" , h.base) , work.path ( ) ) ;
4049
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4050
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4051
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4052
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4053
let run = runs_of ( & h.app, " andy" , " flaky" ) .await[ 0 ] .clone ( ) ;
4054
4055
let ( st, _ ) =
4056
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/test/rerun" , run.number) , Some ( " site" ) , None ) .await;
4057
assert_eq! ( st, StatusCode:: CONFLICT , " not while the run is live" ) ;
4058
while let Some ( j) = claim ( & h.app) .await {
4059
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4060
let ( name, ph) = ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4061
let exit = if name = = " test" { 1 } else { 0 } ;
4062
let log = serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " out" , " text" : format! ( " {name} ran" ) } ] } ) ;
4063
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( log.to_string ( ) ) ) .await;
4064
assert_eq! (
4065
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : exit, " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, exit as u32 ) } ) )
4066
.await,
4067
StatusCode:: NO_CONTENT
4068
) ;
4069
}
4070
let status = | n : & 'static str | {
4071
let ( app, run_id) = ( h.app.clone ( ) , run.id) ;
4072
async move { db:: flow:: jobs( & app.db, run_id) .await.unwrap ( ) .into_iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) }
4073
} ;
4074
assert_eq! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .status, " failed" ) ;
4075
let failed_test = status ( " test" ) .await;
4076
4077
4078
let ( st, body) =
4079
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/test/rerun" , run.number) , Some ( " site" ) , None ) .await;
4080
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4081
assert! ( body.contains ( r # ""queued":1"# ) , " {body}" ) ;
4082
assert_eq! (
4083
( status ( " build" ) .await.status, status ( " lint" ) .await.status, status ( " test" ) .await.status) ,
4084
( " passed" .into ( ) , " passed" .into ( ) , " queued" .into ( ) )
4085
) ;
4086
assert! ( db:: flow:: log( & h.app.db, failed_test.id, 0 , 100 ) .await.unwrap ( ) .is_empty ( ) , " its old log is gone" ) ;
4087
assert! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .live ( ) ) ;
4088
let checks = checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
4089
assert_eq! ( checks.iter ( ) .find ( | c | c.name = = " flow/test" ) .map ( | c | c.status.as_str ( ) ) , Some ( " queued" ) ) ;
4090
let j = claim ( & h.app) .await.expect ( " the test, queued again" ) ;
4091
assert_eq! ( j[ " job" ] [ " name" ] , " test" ) ;
4092
let ph = j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4093
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4094
assert_eq! (
4095
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " test" , & ph, 0 ) } ) ) .await,
4096
StatusCode:: NO_CONTENT
4097
) ;
4098
assert_eq! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .status, " passed" , " the run is whole again" ) ;
bff9106e Andy McCall 24 days ago ↶
4099
4100
let andy_id = db:: accounts:: account_by_name( & h.app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
4101
let kinds: Vec< String> = db:: notify:: inbox( & h.app.db, andy_id, 20 ) .await.unwrap ( ) .into_iter ( ) .map ( | n | n.kind ) .collect ( ) ;
4102
assert! ( kinds.contains ( & " flow.failed" .to_string ( ) ) & & kinds.contains ( & " flow.fixed" .to_string ( ) ) , " {kinds:?}" ) ;
e8fa5369 Andy McCall 24 days ago ↶
4103
4104
4105
let ( _ , body) =
4106
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/build/rerun" , run.number) , Some ( " site" ) , None ) .await;
4107
assert! ( body.contains ( r # ""queued":2"# ) , " {body}" ) ;
4108
assert_eq! (
4109
( status ( " build" ) .await.status, status ( " lint" ) .await.status, status ( " test" ) .await.status) ,
4110
( " queued" .into ( ) , " passed" .into ( ) , " queued" .into ( ) )
4111
) ;
4112
let ( st, _ ) =
4113
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/deploy/rerun" , run.number) , Some ( " site" ) , None ) .await;
4114
assert_eq! ( st, StatusCode:: CONFLICT , " live again, and no such job either way" ) ;
4115
}
4a4400c5 Andy McCall 24 days ago ↶
4116
4117
4118
4119
4120
4121
# [ tokio ::test ( flavor = " multi_thread" ) ]
4122
async fn failed_runs_are_told_and_an_erroring_runner_is_drained ( ) {
4123
let h = hub ( ) .await;
4124
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4125
let bob = db:: accounts:: create_account( & h.app.db, " bob" , " [email protected] " , " x" , " salt" , false ) .await.unwrap ( ) ;
4126
create ( & h.app, " andy" , " alerts" ) .await;
4127
let row = db:: repos:: get( & h.app.db, " andy" , " alerts" ) .await.unwrap ( ) .unwrap ( ) ;
4128
db:: notify:: set_watch( & h.app.db, bob.id, row.id, true ) .await.unwrap ( ) ;
4129
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4130
let c = client ( & format! ( " {} /andy/alerts" , h.base) , work.path ( ) ) ;
4131
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4132
write ( work.path ( ) , " test.sh" , " exit 1\n " ) ;
4133
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4134
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4135
let run = runs_of ( & h.app, " andy" , " alerts" ) .await[ 0 ] .clone ( ) ;
3b1bc27f Andy McCall 22 days ago
4136
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & h.app.db) .await.unwrap ( ) ;
4a4400c5 Andy McCall 24 days ago ↶
4137
while let Some ( j) = claim ( & h.app) .await {
4138
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4139
let ( name, ph) = ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4140
let exit = if name = = " test" { 1 } else { 0 } ;
4141
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : exit, " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, exit as u32 ) } ) ) .await;
4142
}
4143
let told = | id : i64 | {
4144
let db = h.app.db.clone ( ) ;
4145
async move { db:: notify:: inbox( & db, id, 20 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.failed" ) .collect:: < Vec< _ > > ( ) }
4146
} ;
4147
let ( for_andy, for_bob) = ( told ( andy.id) .await, told ( bob.id) .await) ;
4148
assert_eq! ( ( for_andy.len ( ) , for_bob.len ( ) ) , ( 1 , 1 ) , " the pusher and a watcher" ) ;
4149
assert! ( for_andy[ 0 ] .title.contains ( & format! ( " run #{} on main failed (test)" , run.number) ) , " {}" , for_andy[ 0 ] .title) ;
4150
assert_eq! ( for_andy[ 0 ] .url, format! ( " /andy/alerts/flow/{} " , run.number) ) ;
4151
4152
4153
let runner_id = | | {
4154
let db = h.app.db.clone ( ) ;
4155
async move { sqlx:: query_scalar:: < _ , i64 > ( " SELECT id FROM flow_runners WHERE name = 'test-1'" ) .fetch_one ( & db) .await.unwrap ( ) }
4156
} ;
4157
let mut errors = 0 ;
4158
while errors < 5 {
4159
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/alerts/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
4160
assert_eq! ( st, StatusCode:: CREATED ) ;
4161
while let Some ( j) = claim ( & h.app) .await {
4162
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4163
let st = finish (
4164
& h.app,
4165
id,
4166
serde_json:: json! ( { " exit_code" : null, " duration_ms" : 10 , " error" : " the microVM did not start: /dev/kvm busy" } ) ,
4167
)
4168
.await;
4169
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
4170
errors + = 1 ;
4171
if errors = = 5 {
4172
break ;
4173
}
4174
}
4175
}
4176
let state = | id : i64 | {
4177
let db = h.app.db.clone ( ) ;
4178
async move { db:: runners:: get( & db, id) .await.unwrap ( ) .unwrap ( ) .state }
4179
} ;
4180
let id = runner_id ( ) .await;
4181
assert_eq! ( state ( id) .await, " draining" ) ;
4182
let drained: Vec< _ > =
4183
db:: notify:: inbox( & h.app.db, andy.id, 50 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.runner_drained" ) .collect ( ) ;
4184
assert_eq! ( drained.len ( ) , 1 , " the administrator is told" ) ;
4185
assert! ( drained[ 0 ] .title.contains ( " /dev/kvm busy" ) , " {}" , drained[ 0 ] .title) ;
4186
assert! (
4187
! told ( bob.id) .await.is_empty ( )
4188
& & db:: notify:: inbox( & h.app.db, bob.id, 50 ) .await.unwrap ( ) .iter ( ) .all ( | n | n.kind ! = " flow.runner_drained" )
4189
) ;
4190
4191
db:: runners:: set_state( & h.app.db, id, " active" ) .await.unwrap ( ) ;
4192
assert_eq! ( db:: runners:: erroring( & h.app.db, id, 5 ) .await.unwrap ( ) , None ) ;
4193
}
1870e4fa Andy McCall 23 days ago ↶
4194
4195
4196
4197
# [ tokio ::test ( flavor = " multi_thread" ) ]
4198
async fn owners_run_flow_on_their_own_runners_only ( ) {
4199
use oxy_core:: identity:: SecretKey;
4200
let h = hub ( ) .await;
4201
let register = | name : & 'static str | {
4202
let app = h.app.clone ( ) ;
4203
async move {
4204
let body = format! ( " username={name} &email={name} %40example.com&password=correct-horse-battery" ) ;
4205
let res = oxygenhub:: router( app)
4206
.oneshot (
4207
Request:: post( " /register" )
4208
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
4209
.body ( Body:: from( body) )
4210
.unwrap ( ) ,
4211
)
4212
.await
4213
.unwrap ( ) ;
4214
res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( )
4215
}
4216
} ;
4217
register ( " andy" ) .await;
4218
let bob = register ( " bob" ) .await;
4219
create ( & h.app, " bob" , " game" ) .await;
4220
let web = | method : & 'static str , path : & str , body : Option< String> | {
4221
let ( app, cookie, path) = ( h.app.clone ( ) , bob.clone ( ) , path.to_string ( ) ) ;
4222
async move {
4223
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
4224
if body.is_some ( ) {
4225
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
4226
}
4227
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
4228
String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( )
4229
}
4230
} ;
4231
let csrf_of = | page : & str | {
4232
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
4233
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
4234
} ;
4235
4236
let page = web ( " GET" , " /bob/game/settings" , None ) .await;
4237
assert! ( page.contains ( " register a runner for this repository" ) & & ! page.contains ( " value=\" own\" " ) , " {page}" ) ;
4238
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=own" , csrf_of ( & page) ) ) ) .await;
4239
assert! ( page.contains ( " Register a runner for this repository" ) , " {page}" ) ;
4240
4241
let page = web ( " GET" , " /bob/game/settings" , None ) .await;
4242
let page = web ( " POST" , " /bob/game/settings/runners/token" , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
4243
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
4244
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
4245
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
4246
let identity = SecretKey:: generate( ) .identity ( " bobs" , " " , 1 ) ;
4247
let reg = serde_json:: json! ( { " token" : token, " name" : " bobs" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
4248
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
4249
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4250
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4251
4252
let page = web ( " GET" , " /bob/game/settings" , None ) .await;
4253
assert! ( page.contains ( " value=\" own\" " ) & & ! page.contains ( " value=\" on\" " ) , " {page}" ) ;
4254
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=on" , csrf_of ( & page) ) ) ) .await;
4255
assert! ( page.contains ( " Only site administrators let the site's runners" ) , " {page}" ) ;
4256
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=own" , csrf_of ( & page) ) ) ) .await;
4257
assert! ( page.contains ( " value=\" own\" checked" ) & & page.contains ( " start runs" ) , " {page}" ) ;
4258
assert_eq! ( db:: repos:: get( & h.app.db, " bob" , " game" ) .await.unwrap ( ) .unwrap ( ) .flow, " own" ) ;
4259
4260
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4261
let c = client ( & format! ( " {} /bob/game" , h.base) , work.path ( ) ) ;
4262
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4263
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4264
c.snapshot ( " t" ) .unwrap ( ) ;
4265
c.commit ( " A pipeline" , & [ ] ) .unwrap ( ) ;
4266
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4267
assert_eq! ( runs_of ( & h.app, " bob" , " game" ) .await.len ( ) , 1 , " a run started" ) ;
4268
assert! ( claim ( & h.app) .await.is_none ( ) , " the site's runner is not offered it" ) ;
4269
let body = serde_json:: json! ( { " runner" : " ignored" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
4270
let ( st, job) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( body) ) .await;
4271
assert_eq! ( st, StatusCode:: OK , " {job}" ) ;
4272
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & job) .unwrap ( ) [ " repo" ] , " bob/game" ) ;
4273
}
cd437c4f Andy McCall 22 days ago ↶
4274
4275
# [ tokio ::test ( flavor = " multi_thread" ) ]
4276
async fn a_job_takes_the_artifact_its_producer_kept ( ) {
4277
let h = hub ( ) .await;
4278
create ( & h.app, " andy" , " demo" ) .await;
4279
let url = format! ( " {} /andy/demo" , h.base) ;
4280
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4281
let c = client ( & url, work.path ( ) ) ;
4282
write (
4283
work.path ( ) ,
4284
" .flow/pipeline.kdl" ,
4285
& format! (
4286
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n output {{ \n artifact \" tfplan\" {{ \n path \" /src/out/tfplan\" \n }} \n }} \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n input {{ \n artifact \" tfplan\" from \" plan\" \n path \" /src/tfplan\" \n }} \n exec \" apply\" \n }} \n "
4287
) ,
4288
) ;
4289
c.snapshot ( " test" ) .unwrap ( ) ;
4290
c.commit ( " Plan, then apply that plan" , & [ ] ) .unwrap ( ) ;
4291
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4292
4293
4294
let runs = runs_of ( & h.app, " andy" , " demo" ) .await;
4295
assert_eq! ( runs.len ( ) , 1 , " a run" ) ;
4296
let jobs = db:: flow:: jobs( & h.app.db, runs[ 0 ] .id) .await.unwrap ( ) ;
4297
assert! ( ! jobs.is_empty ( ) , " jobs: {:?}" , runs[ 0 ] ) ;
4298
let p = claim ( & h.app)
4299
.await
4300
.unwrap_or_else ( | | panic! ( " nothing to claim: {:?}" , jobs.iter ( ) .map ( | j | ( & j.name, & j.status, & j.error) ) .collect:: < Vec< _ > > ( ) ) ) ;
4301
assert_eq! ( p[ " job" ] [ " name" ] , " plan" ) ;
4302
let plan_id = p[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4303
let plan = b " the reviewed plan" .to_vec ( ) ;
4304
let ( st, text) = upload ( & h.app, plan_id, " tfplan" , & plan) .await;
4305
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
4306
let blob = oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ;
4307
let mut r = receipt ( " plan" , p[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
4308
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
4309
assert_eq! (
4310
finish ( & h.app, plan_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r } ) ) .await,
4311
StatusCode:: NO_CONTENT
4312
) ;
4313
4314
4315
4316
let a = claim ( & h.app) .await.unwrap ( ) ;
4317
assert_eq! ( a[ " job" ] [ " name" ] , " apply" ) ;
4318
let apply_id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4319
assert_eq! (
4320
a[ " inputs" ] ,
4321
serde_json:: json! ( [ { " artifact" : " tfplan" , " from" : " plan" , " path" : " /src/tfplan" , " blob" : blob } ] ) ,
4322
" the claim names the producer's blob"
4323
) ;
4324
let req = Request:: get( format! ( " /api/v1/flow/jobs/{apply_id} /inputs/0" ) ) .header ( header:: AUTHORIZATION , " Bearer runner-secret" ) ;
4325
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( req.body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
4326
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
4327
assert_eq! ( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) , plan) ;
4328
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{apply_id} /inputs/1" ) , Some ( " runner-secret" ) , None ) .await;
4329
assert_eq! ( st, StatusCode:: NOT_FOUND , " no second input" ) ;
4330
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{apply_id} /inputs/0" ) , None , None ) .await;
4331
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " runners only" ) ;
4332
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{plan_id} /inputs/0" ) , Some ( " runner-secret" ) , None ) .await;
4333
assert_eq! ( st, StatusCode:: CONFLICT , " only a running job's" ) ;
4334
}
417184c7 Andy McCall 22 days ago ↶
4335
4336
# [ tokio ::test ( flavor = " multi_thread" ) ]
4337
async fn a_job_waits_for_approval_and_is_approved_rejected_or_expires ( ) {
4338
let h = hub ( ) .await;
4339
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4340
let bob = db:: accounts:: create_account( & h.app.db, " bob" , " [email protected] " , " x" , " salt" , false ) .await.unwrap ( ) ;
4341
let carol = db:: accounts:: create_account( & h.app.db, " carol" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4342
create ( & h.app, " andy" , " gated" ) .await;
4343
let row = db:: repos:: get( & h.app.db, " andy" , " gated" ) .await.unwrap ( ) .unwrap ( ) ;
4344
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4345
let c = client ( & format! ( " {} /andy/gated" , h.base) , work.path ( ) ) ;
4346
write (
4347
work.path ( ) ,
4348
" .flow/pipeline.kdl" ,
4349
& format! (
47e37402 Andy McCall 22 days ago ↶
4350
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n approval {{ \n by \" maintain\" \n others-only #true\n sealed #false\n }} \n environment \" prod\" \n identity \" https://sts.example\" {{ \n mount-path \" /run/secrets/cloud\" \n }} \n exec \" apply\" \n }} \n "
417184c7 Andy McCall 22 days ago ↶
4351
) ,
4352
) ;
4353
let mut n = 0 ;
4354
let mut push = | | {
4355
n + = 1 ;
4356
write ( work.path ( ) , " n.txt" , & format! ( " {n} \n " ) ) ;
4357
c.snapshot ( " test" ) .unwrap ( ) ;
4358
c.commit ( & format! ( " Push {n} " ) , & [ ] ) .unwrap ( ) ;
4359
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4360
} ;
4361
4362
let plan_passes = | number : i64 | {
4363
let app = h.app.clone ( ) ;
4364
let row = row.clone ( ) ;
4365
async move {
4366
let mut run = None ;
4367
for _ in 0 .. 100 {
4368
if let Some ( r) = db:: flow:: run( & app.db, row.id, number) .await.unwrap ( ) {
4369
run = Some ( r) ;
4370
break ;
4371
}
4372
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4373
}
4374
let run = run.expect ( " the run" ) ;
3b1bc27f Andy McCall 22 days ago
4375
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & app.db) .await.unwrap ( ) ;
417184c7 Andy McCall 22 days ago ↶
4376
let j = claim ( & app) .await.expect ( " plan is claimed" ) ;
4377
assert_eq! ( j[ " job" ] [ " name" ] , " plan" ) ;
4378
let ( id, ph) = ( j[ " job_id" ] .as_i64 ( ) .unwrap ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4379
finish ( & app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " plan" , & ph, 0 ) } ) ) .await;
4380
db:: flow:: run( & app.db, row.id, number) .await.unwrap ( ) .unwrap ( )
4381
}
4382
} ;
4383
4384
4385
push ( ) ;
4386
let run = runs_of ( & h.app, " andy" , " gated" ) .await[ 0 ] .clone ( ) ;
7e01a766 Andy McCall 22 days ago ↶
4387
4388
let heard = | topic : String| match h.app.events.live.since ( 0 , & [ topic] ) {
4389
oxygenhub:: events:: Replay:: Events( e) => e.into_iter ( ) .map ( | e | e.kind ) .collect:: < Vec< _ > > ( ) ,
4390
oxygenhub:: events:: Replay:: Reload => vec! [ " reload" .into ( ) ] ,
4391
} ;
4392
assert! ( heard ( oxygenhub:: events:: topic:: repo( row.id) ) .contains ( & " push" .to_string ( ) ) , " the push is on the repository's topic" ) ;
3b1bc27f Andy McCall 22 days ago
4393
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & h.app.db) .await.unwrap ( ) ;
417184c7 Andy McCall 22 days ago ↶
4394
let early = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true ) .await.unwrap_err ( ) ;
4395
assert! ( early.contains ( " waits for plan first" ) , " {early}" ) ;
4396
let run = plan_passes ( run.number) .await;
4397
assert! ( claim ( & h.app) .await.is_none ( ) , " apply waits for a person, so no runner gets it" ) ;
7e01a766 Andy McCall 22 days ago ↶
4398
assert! ( heard ( oxygenhub:: events:: topic:: run( run.id) ) .contains ( & " jobs" .to_string ( ) ) , " the run's topic hears its jobs" ) ;
dc59d1b5 Andy McCall 22 days ago ↶
4399
4400
4401
let told = | id : i64 | {
4402
let db = h.app.db.clone ( ) ;
4403
async move { db:: notify:: inbox( & db, id, 20 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.approval" ) .count ( ) }
4404
} ;
4405
assert_eq! ( ( told ( carol.id) .await, told ( andy.id) .await, told ( bob.id) .await) , ( 1 , 0 , 0 ) ) ;
417184c7 Andy McCall 22 days ago ↶
4406
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/gated/flow/{} " , run.number) , None , None ) .await;
4407
assert! ( page.contains ( " waits for approval by someone with the <em>maintain</em> role, other than andy" ) , " the run page says so" ) ;
4408
let own = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & andy, true ) .await.unwrap_err ( ) ;
4409
assert! ( own.contains ( " someone other than andy" ) , " {own}" ) ;
4410
let no_role = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & bob, true ) .await.unwrap_err ( ) ;
4411
assert! ( no_role.contains ( " takes the maintain role" ) , " {no_role}" ) ;
4412
assert_eq! ( oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true ) .await.unwrap ( ) , " apply: approved by carol" ) ;
dc59d1b5 Andy McCall 22 days ago ↶
4413
assert_eq! ( told ( carol.id) .await, 1 , " told once" ) ;
417184c7 Andy McCall 22 days ago ↶
4414
let j = claim ( & h.app) .await.expect ( " approved, it runs" ) ;
4415
assert_eq! ( j[ " job" ] [ " name" ] , " apply" ) ;
b644e175 Andy McCall 22 days ago ↶
4416
4417
let token =
4418
j[ " secrets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | s | s[ " path" ] = = " /run/secrets/cloud" ) .unwrap ( ) [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4419
let claims = h.app.oidc ( ) .await.unwrap ( ) .verify ( & token, " https://sts.example" , db:: now_ms( ) ) .expect ( " a valid token" ) ;
4420
assert_eq! ( claims[ " environment" ] , " prod" ) ;
417184c7 Andy McCall 22 days ago ↶
4421
let ( id, ph) = ( j[ " job_id" ] .as_i64 ( ) .unwrap ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4422
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " apply" , & ph, 0 ) } ) ) .await;
4423
let audit = db:: accounts:: audit_rows( & h.app.db, 50 ) .await.unwrap ( ) ;
4424
assert! ( audit.iter ( ) .any ( | r | r.action = = " flow.approved" & & r.actor = = " carol" & & r.target.ends_with ( " #1 apply" ) ) , " audited" ) ;
4425
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/gated/flow/{} " , run.number) , None , None ) .await;
4426
assert! ( page.contains ( " approved by carol" ) , " the run says who" ) ;
b644e175 Andy McCall 22 days ago ↶
4427
let ( _ , page) = call ( & h.app, " GET" , " /andy/gated/deployments" , None , None ) .await;
4428
assert! ( page.contains ( " <strong>prod</strong>" ) & & page.contains ( " >#1</a>" ) & & page.contains ( " <td>carol</td>" ) , " the Deployments page" ) ;
4429
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/gated/deployments" , Some ( " site" ) , None ) .await;
4430
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
4431
assert_eq! ( ( list[ 0 ] [ " environment" ] .as_str ( ) , list[ 0 ] [ " approved_by" ] .as_str ( ) ) , ( Some ( " prod" ) , Some ( " carol" ) ) ) ;
417184c7 Andy McCall 22 days ago ↶
4432
4433
4434
push ( ) ;
4435
let run = plan_passes ( 2 ) .await;
4436
assert_eq! ( oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, false ) .await.unwrap ( ) , " apply: rejected by carol" ) ;
4437
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
4438
let apply = jobs.iter ( ) .find ( | j | j.name = = " apply" ) .unwrap ( ) ;
4439
assert_eq! ( ( apply.status.as_str ( ) , apply.error.as_str ( ) ) , ( " cancelled" , " rejected by carol" ) ) ;
4440
assert! ( claim ( & h.app) .await.is_none ( ) ) ;
4441
4442
4443
push ( ) ;
4444
let run = plan_passes ( 3 ) .await;
4445
assert_eq! ( oxygenhub:: flow:: expire_approvals( & h.app, db:: now_ms( ) + 3_600_000 ) .await.unwrap ( ) , 0 , " not yet" ) ;
4446
assert_eq! ( oxygenhub:: flow:: expire_approvals( & h.app, db:: now_ms( ) + 25 * 3_600_000 ) .await.unwrap ( ) , 1 ) ;
4447
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
4448
let apply = jobs.iter ( ) .find ( | j | j.name = = " apply" ) .unwrap ( ) ;
4449
assert_eq! ( ( apply.status.as_str ( ) , apply.error.as_str ( ) ) , ( " cancelled" , " no approval within 24h" ) ) ;
4450
}
67be8ac0 Andy McCall 22 days ago ↶
4451
4452
# [ tokio ::test ( flavor = " multi_thread" ) ]
4453
async fn live_streams_only_topics_the_viewer_may_hear_and_replays ( ) {
4454
let h = hub ( ) .await;
4455
create ( & h.app, " andy" , " open" ) .await;
4456
h.app.repos.create ( & RepoName:: parse( " andy/shut" ) .unwrap ( ) , None ) .unwrap ( ) ;
4457
db:: repos:: create( & h.app.db, " andy" , " shut" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
4458
let open = db:: repos:: get( & h.app.db, " andy" , " open" ) .await.unwrap ( ) .unwrap ( ) ;
4459
let shut = db:: repos:: get( & h.app.db, " andy" , " shut" ) .await.unwrap ( ) .unwrap ( ) ;
4460
let live = & h.app.events.live;
4461
let first = live.publish ( & oxygenhub:: events:: topic:: repo( open.id) , " push" ) ;
4462
live.publish ( & oxygenhub:: events:: topic:: repo( shut.id) , " push" ) ;
4463
live.publish ( & oxygenhub:: events:: topic:: proposal( open.id, 3 ) , " review" ) ;
4464
4465
4466
let read = | path : String, last : Option< u64 > | {
4467
let app = h.app.clone ( ) ;
4468
async move {
4469
let mut req = Request:: get( & path) ;
4470
if let Some ( l) = last {
4471
req = req.header ( " last-event-id" , l.to_string ( ) ) ;
4472
}
4473
let res = oxygenhub:: router( app) .oneshot ( req.body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
4474
let status = res.status ( ) ;
4475
let mut body = res.into_body ( ) ;
4476
let mut text = String :: new( ) ;
4477
while let Ok ( Some ( Ok ( frame) ) ) = tokio:: time:: timeout( std:: time:: Duration:: from_millis( 300 ) , body.frame ( ) ) .await {
4478
if let Some ( d) = frame.data_ref ( ) {
4479
text.push_str ( & String :: from_utf8_lossy( d) ) ;
4480
}
4481
}
4482
( status, text)
4483
}
4484
} ;
4485
let topics = format! ( " /live?topics=repo:{} ,repo:{} /proposal:3" , open.id, open.id) ;
4486
4487
let ( st, text) = read ( topics.clone ( ) , Some ( first.id) ) .await;
4488
assert_eq! ( st, StatusCode:: OK ) ;
4489
assert! ( text.contains ( " event: change" ) & & text.contains ( & format! ( " \" topic\" :\" repo:{} /proposal:3\" " , open.id) ) , " {text}" ) ;
4490
assert! ( ! text.contains ( & format! ( " \" topic\" :\" repo:{} \" " , shut.id) ) , " never another topic: {text}" ) ;
4491
assert! ( ! text.contains ( & format! ( " id: {} \n " , first.id) ) , " not what was already seen: {text}" ) ;
4492
4493
let ( st, _ ) = read ( format! ( " /live?topics=repo:{} ,repo:{} " , open.id, shut.id) , None ) .await;
4494
assert_eq! ( st, StatusCode:: FORBIDDEN ) ;
4495
assert_eq! ( read ( " /live?topics=user:1" .into ( ) , None ) .await.0 , StatusCode:: FORBIDDEN ) ;
4496
assert_eq! ( read ( " /live?topics=nonsense" .into ( ) , None ) .await.0 , StatusCode:: BAD_REQUEST ) ;
4497
assert_eq! ( read ( " /live?topics=" .into ( ) , None ) .await.0 , StatusCode:: BAD_REQUEST ) ;
4498
4499
for _ in 0 .. oxygenhub:: events:: REPLAY {
4500
live.publish ( " repo:999999" , " push" ) ;
4501
}
4502
let ( _ , text) = read ( topics, Some ( first.id) ) .await;
4503
assert! ( text.contains ( " event: reload" ) , " {text}" ) ;
4504
}
47e37402 Andy McCall 22 days ago ↶
4505
4506
# [ tokio ::test ( flavor = " multi_thread" ) ]
4507
async fn a_sealed_approval_releases_exactly_the_reviewed_plan ( ) {
4508
use oxy_core:: approval:: { SignedApproval, Statement} ;
4509
use oxy_core:: hash:: ObjectRef;
4510
use oxy_core:: identity:: SecretKey;
4511
let h = hub ( ) .await;
4512
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4513
let carol = db:: accounts:: create_account( & h.app.db, " carol" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4514
let sk = SecretKey:: generate( ) ;
4515
let carol_id = sk.identity ( " Carol" , " [email protected] " , 1 ) ;
4516
db:: accounts:: link_identity( & h.app.db, carol.id, & carol_id.hash ( ) .to_hex ( ) , " Carol" , " [email protected] " , & carol_id.encode ( ) )
4517
.await
4518
.unwrap ( ) ;
4519
create ( & h.app, " andy" , " infra" ) .await;
4520
let row = db:: repos:: get( & h.app.db, " andy" , " infra" ) .await.unwrap ( ) .unwrap ( ) ;
4521
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4522
let c = client ( & format! ( " {} /andy/infra" , h.base) , work.path ( ) ) ;
4523
write (
4524
work.path ( ) ,
4525
" .flow/pipeline.kdl" ,
4526
& format! (
4527
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n output {{ \n artifact \" tfplan\" {{ \n path \" /src/out/tfplan\" \n }} \n }} \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n environment \" prod\" \n approval\n input {{ \n artifact \" tfplan\" from \" plan\" \n path \" /src/tfplan\" \n }} \n identity \" https://sts.example\" {{ \n mount-path \" /run/secrets/cloud\" \n }} \n exec \" apply\" \n }} \n "
4528
) ,
4529
) ;
4530
c.snapshot ( " test" ) .unwrap ( ) ;
4531
c.commit ( " Plan, then apply" , & [ ] ) .unwrap ( ) ;
4532
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4533
let run = runs_of ( & h.app, " andy" , " infra" ) .await[ 0 ] .clone ( ) ;
4534
let p = claim ( & h.app) .await.unwrap ( ) ;
4535
let plan_id = p[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4536
let plan = b " the reviewed plan" .to_vec ( ) ;
4537
upload ( & h.app, plan_id, " tfplan" , & plan) .await;
4538
let mut r = receipt ( " plan" , p[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
4539
r[ " outputs" ] = serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ] ) ;
4540
finish ( & h.app, plan_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : r } ) ) .await;
4541
let run = db:: flow:: run( & h.app.db, row.id, run.number) .await.unwrap ( ) .unwrap ( ) ;
4542
4543
4544
let plain = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true ) .await.unwrap_err ( ) ;
4545
assert! ( plain.contains ( & format! ( " oxy seal --flow {} /andy/infra/flow/{} --job apply" , h.base, run.number) ) , " {plain}" ) ;
4546
4547
let ( st, body) =
4548
call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/infra/flow/runs/{} /jobs/apply/approval" , run.number) , Some ( " site" ) , None ) .await;
4549
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4550
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
4551
assert_eq! ( v[ " environment" ] , " prod" ) ;
4552
assert_eq! ( v[ " inputs" ] , serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ] ) , " the reviewed plan" ) ;
4553
let mut repo = [ 0 u8 ; 16 ] ;
4554
repo.copy_from_slice ( & hex:: decode( v[ " repo" ] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ) ;
4555
let oref = | k : & str | ObjectRef:: from_hex( v[ k] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ;
4556
let statement = Statement {
4557
repo,
4558
run: v[ " run" ] .as_str ( ) .unwrap ( ) .into ( ) ,
4559
job: " apply" .into ( ) ,
4560
job_hash: oref ( " job_hash" ) ,
4561
state: oref ( " state" ) ,
4562
inputs: v[ " inputs" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | i | ObjectRef:: from_hex( i.as_str ( ) .unwrap ( ) ) .unwrap ( ) ) .collect ( ) ,
4563
environment: " prod" .into ( ) ,
4564
approve: true ,
4565
} ;
4566
let seal = | s : & Statement, key : & SecretKey, id : & oxy_core:: identity:: Identity| {
4567
let body = serde_json:: json! ( { " signed" : hex:: encode( SignedApproval:: sign( s, id, key) .encode ( ) ) } ) .to_string ( ) ;
4568
let app = h.app.clone ( ) ;
4569
let path = format! ( " /api/v1/repos/andy/infra/flow/runs/{} /jobs/apply/seal" , run.number) ;
4570
async move { call ( & app, " POST" , & path, Some ( " site" ) , Some ( body) ) .await }
4571
} ;
4572
4573
let other_plan = Statement { inputs: vec! [ ObjectRef( [ 9 ; 32 ] ) ] , .. statement.clone ( ) } ;
4574
let ( st, body) = seal ( & other_plan, & sk, & carol_id) .await;
4575
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " approves something else" ) , " {st} {body}" ) ;
4576
let agent = SecretKey:: generate( ) ;
4577
let agent_id = agent.identity ( " Claude Code" , " [email protected] " , 1 ) ;
4578
let ( st, body) = seal ( & statement, & agent, & agent_id) .await;
4579
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not linked to an account" ) , " {st} {body}" ) ;
4580
assert! ( claim ( & h.app) .await.is_none ( ) , " still waiting" ) ;
4581
4582
4583
let ( st, body) = seal ( & statement, & sk, & carol_id) .await;
4584
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4585
assert! ( body.contains ( " approved by carol, sealed" ) , " {body}" ) ;
4586
let name = oxygenhub:: repos:: RepoName:: parse( " andy/infra" ) .unwrap ( ) ;
4587
let state = statement.state;
4588
let kept = h.app.repos.read ( & name, move | r, _ | Ok ( r.store ( ) .approvals_for ( & state) ? ) ) .await.unwrap ( ) ;
4589
assert_eq! ( kept.len ( ) , 1 , " the approval is in the repository" ) ;
4590
let j = claim ( & h.app) .await.expect ( " released" ) ;
4591
assert_eq! ( j[ " job" ] [ " name" ] , " apply" ) ;
4592
let token =
4593
j[ " secrets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | s | s[ " path" ] = = " /run/secrets/cloud" ) .unwrap ( ) [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4594
let claims = h.app.oidc ( ) .await.unwrap ( ) .verify ( & token, " https://sts.example" , db:: now_ms( ) ) .unwrap ( ) ;
4595
assert_eq! ( ( claims[ " environment" ] .as_str ( ) , claims[ " approved_by" ] .as_str ( ) ) , ( Some ( " prod" ) , Some ( " carol" ) ) ) ;
4596
assert_eq! ( claims[ " approval" ] .as_str ( ) , Some ( kept[ 0 ] .to_hex ( ) .as_str ( ) ) ) ;
4597
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/infra/flow/{} " , run.number) , None , None ) .await;
4598
assert! ( page.contains ( " sealed with their key" ) , " the run page says so" ) ;
4599
}
55673c65 Andy McCall 22 days ago ↶
4600
4601
4602
4603
# [ tokio ::test ( flavor = " multi_thread" ) ]
4604
async fn repositories_take_turns_on_the_runners ( ) {
4605
let h = hub ( ) .await;
4606
for name in [ " busy" , " quiet" ] {
4607
create ( & h.app, " andy" , name) .await;
4608
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4609
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
4610
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4611
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4612
c.snapshot ( " test" ) .unwrap ( ) ;
4613
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4614
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4615
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
4616
}
4617
4618
4619
let order: Vec< String> = {
4620
let mut out = Vec :: new( ) ;
4621
for _ in 0 .. 3 {
4622
let c = claim ( & h.app) .await.expect ( " a job to claim" ) ;
4623
out.push ( c[ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4624
}
4625
out
4626
} ;
4627
assert_eq! ( order, [ " andy/busy" , " andy/quiet" , " andy/busy" ] ) ;
4628
}
107eaba5 Andy McCall 21 days ago ↶
4629
4630
4631
4632
# [ tokio ::test ( flavor = " multi_thread" ) ]
4633
async fn priority_then_turns_and_an_expedited_run_first ( ) {
4634
let h = hub ( ) .await;
4635
for name in [ " early" , " later" ] {
4636
create ( & h.app, " andy" , name) .await;
4637
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4638
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
4639
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4640
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4641
c.snapshot ( " test" ) .unwrap ( ) ;
4642
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4643
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4644
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
4645
}
4646
let early = db:: repos:: get( & h.app.db, " andy" , " early" ) .await.unwrap ( ) .unwrap ( ) ;
4647
db:: repos:: set_flow_priority( & h.app.db, early.id, " low" ) .await.unwrap ( ) ;
4648
4649
let repo_of = | c : serde_json:: Value| c[ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4650
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/later" ) ;
4651
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/later" ) ;
4652
4653
let run = & runs_of ( & h.app, " andy" , " early" ) .await[ 0 ] ;
4654
assert! ( db:: flow:: expedite( & h.app.db, run.id, " expedited by andy: an incident" ) .await.unwrap ( ) ) ;
76f93eae Andy McCall 21 days ago ↶
4655
4656
assert_eq! ( db:: flow:: expedited_live( & h.app.db, " andy" ) .await.unwrap ( ) , 1 ) ;
4657
assert_eq! ( db:: flow:: expedited_live( & h.app.db, " someone-else" ) .await.unwrap ( ) , 0 ) ;
107eaba5 Andy McCall 21 days ago ↶
4658
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/early" ) ;
4659
let q = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
4660
assert! (
4661
q.iter ( ) .any ( | e | e.name = = " early" & & e.priority ( ) = = " urgent" & & e.priority_note.contains ( " an incident" ) ) ,
4662
" {:?}" ,
4663
q.iter ( ) .map ( | e | ( & e.name, e.priority ( ) ) ) .collect:: < Vec< _ > > ( )
4664
) ;
4665
4666
db:: accounts:: set_setting( & h.app.db, " flow_priority" , " high" ) .await.unwrap ( ) ;
4667
let q = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
4668
assert! ( q.iter ( ) .filter ( | e | e.name = = " later" ) .all ( | e | e.priority ( ) = = " high" ) ) ;
4669
}