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_" ) ) ;
8a3334b7 Andy McCall 1 month ago ↶
213
let opts = RemoteOptions { token: Some ( token.clone ( ) ) , ca: None , repo_id: None , cache_dir: None } ;
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" ) ;
8a3334b7 Andy McCall 1 month ago ↶
216
let bad = RemoteOptions { token: Some ( " flowjob_00" .into ( ) ) , ca: None , repo_id: None , cache_dir: None } ;
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" ) ;
bf3d0754 Andy McCall 1 month ago ↶
565
566
let a = claim ( & h.app) .await.unwrap ( ) ;
567
assert_eq! ( a[ " repo" ] , " andy/first" ) ;
568
let ( _ , page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
569
assert! ( page.contains ( " Waiting for a runner: 3rd in line" ) & & page.contains ( " Running now: andy/first #1" ) , " {page}" ) ;
570
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/other/plain/flow/runs/1" , None , None ) .await;
571
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " queue_position" ] , 3 ) ;
572
let ( _ , list) = call ( & h.app, " GET" , " /other/plain/flow" , None , None ) .await;
573
assert! ( list.contains ( " queued, 3rd in line" ) , " {list}" ) ;
a0d4bbfb Andy McCall 1 month ago ↶
574
575
576
assert_eq! ( plain[ 0 ] .number, 1 ) ;
90326c96 Andy McCall 1 month ago ↶
577
let checks = checks_when ( & h.app, row.id, & plain[ 0 ] .state, 3 ) .await;
a0d4bbfb Andy McCall 1 month ago ↶
578
assert! ( checks.iter ( ) .all ( | c | c.url.ends_with ( " /other/plain/flow/1" ) ) , " {checks:?}" ) ;
579
let ( st, page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
580
assert! ( st = = StatusCode:: OK & & page.contains ( " Run #1" ) , " {st}" ) ;
9008bf6c Andy McCall 1 month ago ↶
581
assert! ( runs_of ( & h.app, " andy" , " quiet" ) .await.is_empty ( ) , " turned off despite andy/*" ) ;
582
}
1edac4b6 Andy McCall 1 month ago ↶
583
584
585
586
# [ tokio ::test ( flavor = " multi_thread" ) ]
587
async fn runs_start_without_a_push_and_finished_runs_re_run ( ) {
588
let h = hub ( ) .await;
589
590
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
591
create ( & h.app, " andy" , " demo" ) .await;
592
let url = format! ( " {} /andy/demo" , h.base) ;
593
let work = tempfile:: tempdir( ) .unwrap ( ) ;
594
let c = client ( & url, work.path ( ) ) ;
595
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
596
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
597
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
598
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
599
let first = runs_of ( & h.app, " andy" , " demo" ) .await;
600
assert_eq! ( ( first.len ( ) , first[ 0 ] .trigger.as_str ( ) ) , ( 1 , " push" ) ) ;
601
602
603
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
604
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
605
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
606
assert_eq! ( v[ " number" ] , 2 ) ;
607
assert! ( v[ " url" ] .as_str ( ) .unwrap ( ) .ends_with ( " /andy/demo/flow/2" ) ) ;
608
let second = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
609
assert_eq! ( ( second.trigger.as_str ( ) , second.actor.as_str ( ) , second.state.as_str ( ) ) , ( " manual" , " andy" , first[ 0 ] .state.as_str ( ) ) ) ;
610
assert_eq! ( db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) .len ( ) , 3 ) ;
611
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/2" , None , None ) .await;
612
assert! ( page.contains ( " started by andy" ) , " the run page says what started it" ) ;
613
4f692533 Andy McCall 1 month ago ↶
614
615
let head = first[ 0 ] .head_list ( ) [ 0 ] .to_string ( ) ;
616
let ( st, body) =
617
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{head} "}} "# ) ) ) .await;
618
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
619
let on_change = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 3 ) .await.unwrap ( ) .unwrap ( ) ;
620
assert_eq! (
621
( on_change.bookmark.as_str ( ) , on_change.heads.as_str ( ) , on_change.state.as_str ( ) ) ,
622
( " " , head.as_str ( ) , first[ 0 ] .state.as_str ( ) )
623
) ;
624
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/3" , None , None ) .await;
625
assert! ( page.contains ( " title=\" Run on one change, not a bookmark\" >change</span>" ) , " run page" ) ;
626
let ( st, body) =
627
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{} "}} "# , " ab" .repeat ( 32 ) ) ) )
628
.await;
629
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " no change" ) , " {st} {body}" ) ;
630
631
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
632
assert! ( page.contains ( " Sign in with write access to run the pipeline." ) & & ! page.contains ( " primary-btn" ) ) ;
633
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/bookmarks" , None , None ) .await;
634
assert! ( ! page.contains ( " /flow/start" ) , " no run buttons for anonymous viewers" ) ;
635
1edac4b6 Andy McCall 1 month ago ↶
636
637
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
638
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " still queued" ) , " {st} {body}" ) ;
639
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"bookmark":"nope"}"# .into ( ) ) ) .await;
640
assert_eq! ( st, StatusCode:: CONFLICT ) ;
641
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/99/rerun" , Some ( " site" ) , None ) .await;
642
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
643
644
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , None , Some ( " {}" .into ( ) ) ) .await;
645
assert_ne! ( st, StatusCode:: CREATED ) ;
646
647
648
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
649
for r in [ & first[ 0 ] , & second] {
650
for j in db:: flow:: jobs( & h.app.db, r.id) .await.unwrap ( ) {
651
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE id = ?" ) .bind ( j.id) .execute ( & h.app.db) .await.unwrap ( ) ;
652
}
653
sqlx:: query( " UPDATE flow_runs SET status = 'passed', finished_ms = 1 WHERE id = ?" ) .bind ( r.id) .execute ( & h.app.db) .await.unwrap ( ) ;
654
}
655
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
656
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
657
let third = db:: flow:: run( & h.app.db, row.id, 4 ) .await.unwrap ( ) .unwrap ( ) ;
348f8c4b Andy McCall 1 month ago ↶
658
assert_eq! (
659
( third.trigger_label ( ) .as_str ( ) , third.status.as_str ( ) , third.state.as_str ( ) ) ,
660
( " rerun:1" , " queued" , first[ 0 ] .state.as_str ( ) )
661
) ;
c889911b Andy McCall 1 month ago ↶
662
assert_eq! ( third.trigger, " rerun:1:push" , " a rerun keeps the event it repeats" ) ;
1edac4b6 Andy McCall 1 month ago ↶
663
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, & third.state) .await.unwrap ( ) ;
664
assert! (
4f692533 Andy McCall 1 month ago ↶
665
checks.iter ( ) .all ( | c | c.status = = " queued" & & c.url.ends_with ( " /flow/4" ) ) ,
1edac4b6 Andy McCall 1 month ago ↶
666
" the re-run's checks replace the old ones: {checks:?}"
667
) ;
4f692533 Andy McCall 1 month ago ↶
668
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/4" , None , None ) .await;
1edac4b6 Andy McCall 1 month ago ↶
669
assert! ( page.contains ( " re-run of #1 by andy" ) ) ;
670
assert! ( ! page.contains ( " RE-RUN" ) , " anonymous viewers get no button" ) ;
671
672
673
db:: repos:: set_flow( & h.app.db, row.id, " off" ) .await.unwrap ( ) ;
674
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
675
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not enabled" ) , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
676
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
677
assert! ( page.contains ( " Flow is off for this repository" ) , " and the Flow tab says so" ) ;
90326c96 Andy McCall 1 month ago ↶
678
}
679
680
681
682
# [ tokio ::test ( flavor = " multi_thread" ) ]
683
async fn runs_jobs_and_logs_are_in_the_api ( ) {
684
let h = hub ( ) .await;
685
create ( & h.app, " andy" , " demo" ) .await;
686
let url = format! ( " {} /andy/demo" , h.base) ;
687
let work = tempfile:: tempdir( ) .unwrap ( ) ;
688
let c = client ( & url, work.path ( ) ) ;
689
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
690
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
691
let head = c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
692
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
693
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
694
695
696
let a = claim ( & h.app) .await.unwrap ( ) ;
697
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
698
let recs =
699
serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " stdout" , " text" : " one" } , { " t" : 3 , " stream" : " stderr" , " text" : " three" } ] } ) ;
700
let ( st, _ ) = call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( recs.to_string ( ) ) ) .await;
701
assert! ( st.is_success ( ) , " {st}" ) ;
702
703
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs" , None , None ) .await;
704
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
705
let runs: Vec< hub_api:: FlowRun> = serde_json:: from_str( & body) .unwrap ( ) ;
706
assert_eq! ( runs.len ( ) , 1 ) ;
707
assert_eq! ( ( runs[ 0 ] .number, runs[ 0 ] .state.as_str ( ) , runs[ 0 ] .trigger.as_str ( ) ) , ( 1 , run.state.as_str ( ) , " push" ) ) ;
708
assert! ( runs[ 0 ] .url.ends_with ( " /andy/demo/flow/1" ) & & runs[ 0 ] .jobs.is_empty ( ) ) ;
709
710
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , None , None ) .await;
711
let one: hub_api:: FlowRun = serde_json:: from_str( & body) .unwrap ( ) ;
712
let names: Vec< ( & str , & str ) > = one.jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.status.as_str ( ) ) ) .collect ( ) ;
713
assert_eq! ( names.len ( ) , 3 ) ;
714
let running = one.jobs.iter ( ) .find ( | j | j.status = = " running" ) .unwrap ( ) ;
715
assert_eq! ( running.name, a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) ;
716
assert! ( one.jobs.iter ( ) .any ( | j | j.name = = " test" & & j.depends_on = = [ " build" ] ) ) ;
717
718
let job = running.name.clone ( ) ;
719
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log" ) , None , None ) .await;
720
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
721
let lines: Vec< hub_api:: FlowLogLine> = serde_json:: from_str( & body) .unwrap ( ) ;
722
assert! ( lines.iter ( ) .any ( | l | l.text = = " three" ) , " {body}" ) ;
723
let last = lines.last ( ) .unwrap ( ) .seq;
724
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log?after={last} " ) , None , None ) .await;
725
assert_eq! ( body, " []" , " nothing after the last line" ) ;
726
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
727
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1/jobs/nope/log" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
728
729
730
let by = | key : String| {
731
let app = h.app.clone ( ) ;
732
async move {
733
let ( st, body) = call ( & app, " GET" , & format! ( " /api/v1/repos/andy/demo/checks/{key} " ) , None , None ) .await;
734
assert_eq! ( st, StatusCode:: OK , " {key}: {body}" ) ;
735
serde_json:: from_str:: < Vec< hub_api:: Check> > ( & body) .unwrap ( ) .len ( )
736
}
737
} ;
738
checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
739
assert_eq! ( by ( run.state.clone ( ) ) .await, 3 ) ;
740
assert_eq! ( by ( head.to_hex ( ) ) .await, 3 , " a single head names its state" ) ;
741
assert_eq! ( by ( " main" .into ( ) ) .await, 3 ) ;
742
let ( st, _ ) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/checks/no-such-bookmark" , None , None ) .await;
743
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
744
745
746
let ( st, body) = call ( & h.app, " GET" , " /api/v1/users/andy/repos" , None , None ) .await;
747
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
748
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
749
assert_eq! ( v[ " status" ] , 404 , " {body}" ) ;
1edac4b6 Andy McCall 1 month ago ↶
750
}
cc19697f Andy McCall 1 month ago ↶
751
752
async fn get_bytes ( app : & Shared, path : & str ) -> ( StatusCode, Vec< u8 > ) {
753
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
754
let status = res.status ( ) ;
755
( status, res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) )
756
}
757
758
# [ tokio ::test ( flavor = " multi_thread" ) ]
759
async fn signed_receipts_are_checked_kept_and_shown ( ) {
760
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectRef, ObjectType} ;
761
use oxy_core:: identity:: SecretKey;
762
use oxy_core:: objects:: StateObject;
763
use oxy_core:: receipt:: SignedReceipt;
764
let h = hub ( ) .await;
765
create ( & h.app, " andy" , " demo" ) .await;
766
let url = format! ( " {} /andy/demo" , h.base) ;
767
let work = tempfile:: tempdir( ) .unwrap ( ) ;
768
let c = client ( & url, work.path ( ) ) ;
769
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
770
c.snapshot ( " test" ) .unwrap ( ) ;
771
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
772
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
773
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
774
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
775
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads.clone ( ) ) .encode ( ) ) ;
776
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
777
let sk = SecretKey:: generate( ) ;
778
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
779
let sign = | r : & serde_json:: Value, state : ObjectRef, sk : & SecretKey, id : & oxy_core:: identity:: Identity| {
780
let r: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
781
let run_url = format! ( " {url} /flow/{} " , run.number) ;
782
hex:: encode( SignedReceipt:: sign( r.to_oxy ( ) , repo_id, state, & run_url, id, sk) .encode ( ) )
783
} ;
784
785
let a = claim ( & h.app) .await.unwrap ( ) ;
786
let b = claim ( & h.app) .await.unwrap ( ) ;
787
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
788
let ( a_id, b_id) = ( a[ " job_id" ] .as_i64 ( ) .unwrap ( ) , b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
789
790
let rb = receipt ( " build" , & ph, 0 ) ;
791
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : rb, " signed_receipt" : sign ( & rb, state, & sk, & runner) } ) ;
792
assert_eq! ( finish ( & h.app, a_id, body) .await, StatusCode:: NO_CONTENT ) ;
793
794
let other = SecretKey:: generate( ) ;
795
let rl = receipt ( " lint" , & ph, 0 ) ;
796
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : rl,
797
" signed_receipt" : sign ( & rl, state, & other, & other.identity ( " mallory" , " " , 1 ) ) } ) ;
798
assert_eq! ( finish ( & h.app, b_id, body) .await, StatusCode:: NO_CONTENT ) ;
799
800
let t = claim ( & h.app) .await.unwrap ( ) ;
801
let rt = receipt ( " test" , & ph, 0 ) ;
802
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 500 , " receipt" : rt,
803
" signed_receipt" : sign ( & rt, ObjectRef( [ 9 ; 32 ] ) , & sk, & runner) } ) ;
804
assert_eq! ( finish ( & h.app, t[ " job_id" ] .as_i64 ( ) .unwrap ( ) , body) .await, StatusCode:: NO_CONTENT ) ;
805
806
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
807
let job = | n : & str | jobs.iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) .clone ( ) ;
808
assert_eq! ( job ( " build" ) .status, " passed" ) ;
809
assert_eq! ( ( job ( " build" ) .receipt_attest.len ( ) , job ( " build" ) .receipt_signer.as_str ( ) ) , ( 64 , " test-1 (OxygenFlow runner)" ) ) ;
810
assert_eq! ( job ( " lint" ) .status, " passed" , " the job's result stands" ) ;
811
assert! ( job ( " lint" ) .receipt_attest.is_empty ( ) ) ;
812
assert! ( job ( " lint" ) .receipt_note.contains ( " not the key this runner signs with" ) , " {}" , job ( " lint" ) .receipt_note) ;
813
assert! ( job ( " test" ) .receipt_note.contains ( " another state" ) , " {}" , job ( " test" ) .receipt_note) ;
814
815
816
817
let kept = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .receipts ( & heads) .unwrap ( ) ;
818
assert_eq! ( kept.len ( ) , 1 ) ;
819
let attest = job ( " build" ) .receipt_attest;
820
let ( st, bytes) = get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{attest} " ) ) .await;
821
assert_eq! ( st, StatusCode:: OK ) ;
822
let sr = SignedReceipt:: decode( & bytes) .unwrap ( ) ;
823
sr.verify ( ) .unwrap ( ) ;
824
assert_eq! ( ( sr.state ( ) , sr.receipt.job_name.as_str ( ) ) , ( state, " build" ) ) ;
825
assert_eq! ( get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{} " , " 0" .repeat ( 64 ) ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
826
827
828
829
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
830
assert! ( page.contains ( " signed receipt</a> by test-1" ) , " run page" ) ;
831
assert! ( page.contains ( " receipt not kept" ) , " run page" ) ;
832
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/change/{} " , heads[ 0 ] .to_hex ( ) ) , None , None ) .await;
833
assert! ( page.contains ( " <dt>Receipts</dt>" ) & & page.contains ( & format! ( " /andy/demo/receipts/{attest} " ) ) , " change page" ) ;
834
}
a3c8288b Andy McCall 1 month ago ↶
835
836
async fn upload ( app : & Shared, job : i64 , name : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
837
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /artifacts/{name} " ) )
838
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
839
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
840
.body ( Body:: from( bytes.to_vec ( ) ) )
841
.unwrap ( ) ;
842
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
843
let status = res.status ( ) ;
844
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
845
}
846
847
# [ tokio ::test ( flavor = " multi_thread" ) ]
848
async fn artifacts_are_kept_named_by_the_receipt_and_served ( ) {
849
let h = hub ( ) .await;
850
create ( & h.app, " andy" , " demo" ) .await;
851
let url = format! ( " {} /andy/demo" , h.base) ;
852
let work = tempfile:: tempdir( ) .unwrap ( ) ;
853
let c = client ( & url, work.path ( ) ) ;
854
write (
855
work.path ( ) ,
856
" .flow/pipeline.kdl" ,
857
& format! (
858
" 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 "
859
) ,
860
) ;
861
c.snapshot ( " test" ) .unwrap ( ) ;
862
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
863
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
864
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
865
let a = claim ( & h.app) .await.unwrap ( ) ;
866
assert_eq! ( a[ " job" ] [ " outputs" ] , serde_json:: json! ( [ { " name" : " tool" , " path" : " /src/out/tool" } ] ) , " the runner is told" ) ;
867
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
868
869
let body: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 253 ) as u8 ) .collect ( ) ;
870
let ( st, text) = upload ( & h.app, id, " other" , & body) .await;
871
assert_eq! ( st, StatusCode:: BAD_REQUEST , " an undeclared artifact: {text}" ) ;
872
let ( st, text) = upload ( & h.app, id, " tool" , & body) .await;
873
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
874
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
875
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( & body) .to_hex ( ) , " the same blob the runner computes" ) ;
876
877
878
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
879
assert_eq! (
880
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
881
StatusCode:: BAD_REQUEST
882
) ;
883
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
884
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r } ) ) .await, StatusCode:: NO_CONTENT ) ;
885
886
887
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
888
let link = format! ( " /andy/demo/flow/{} /artifacts/build/tool" , run.number) ;
889
assert! ( page.contains ( & link) , " run page lists the artifact" ) ;
890
let ( st, got) = get_bytes ( & h.app, & link) .await;
891
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) ) ;
892
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 ↶
893
45977501 Andy McCall 27 days ago ↶
894
895
896
{
897
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
898
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
899
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
900
hosted.save_config ( ) .unwrap ( ) ;
901
}
902
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
903
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
904
let ( st, got) = get_bytes ( & h.app, & link) .await;
905
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) , " the artifact survives gc" ) ;
906
10708ab8 Andy McCall 1 month ago ↶
907
908
909
let form = | f : String| {
910
Request:: post( " /register" ) .header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) .body ( Body:: from( f) ) .unwrap ( )
911
} ;
912
let res = oxygenhub:: router( h.app.clone ( ) )
913
.oneshot ( form ( " username=andy&email=a%40example.com&password=correct-horse-battery" .into ( ) ) )
914
.await
915
.unwrap ( ) ;
916
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
917
let with_cookie = | method : & str , path : & str , body : Option< String> | {
918
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
919
if body.is_some ( ) {
920
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
921
}
922
req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( )
923
} ;
924
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
925
db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
926
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( with_cookie ( " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None ) ) .await.unwrap ( ) ;
927
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
928
let attach = format! ( " /andy/demo/flow/{} /artifacts/build/tool/attach" , run.number) ;
929
assert! ( page.contains ( & attach) , " a writer is offered the release" ) ;
930
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
931
let csrf = page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
932
let release_id = db:: releases:: list( & h.app.db, row.id) .await.unwrap ( ) [ 0 ] .id;
933
let res = oxygenhub:: router( h.app.clone ( ) )
934
.oneshot ( with_cookie ( " POST" , & attach, Some ( format! ( " _csrf={csrf} &release={release_id} " ) ) ) )
935
.await
936
.unwrap ( ) ;
937
assert_eq! ( res.status ( ) , StatusCode:: SEE_OTHER ) ;
938
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/releases/tag/v1" , None , None ) .await;
939
assert! (
940
page.contains ( " >tool</a>" ) & & page.contains ( & format! ( " built by <a href=\" /andy/demo/flow/{0} \" >run #{0} </a> build" , run.number) ) ,
941
" {page}"
942
) ;
943
let ( _ , api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases" , None , None ) .await;
944
let api: serde_json:: Value = serde_json:: from_str( & api) .unwrap ( ) ;
945
let asset = & api[ 0 ] [ " assets" ] [ 0 ] ;
946
assert_eq! (
947
( asset[ " name" ] .as_str ( ) , asset[ " built_by" ] [ " run" ] .as_i64 ( ) , asset[ " built_by" ] [ " job" ] .as_str ( ) ) ,
948
( Some ( " tool" ) , Some ( run.number) , Some ( " build" ) )
949
) ;
950
assert! ( asset[ " built_by" ] [ " receipt" ] .is_null ( ) , " that receipt was not signed" ) ;
a3c8288b Andy McCall 1 month ago ↶
951
}
df940d75 Andy McCall 1 month ago ↶
952
d29070fa Andy McCall 1 month ago ↶
953
954
a36bab6b Andy McCall 27 days ago ↶
955
async fn upload_report_file ( app : & Shared, job : i64 , name : & str , path : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
956
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /reports/{name} /files" ) )
957
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
958
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
959
.header ( " X-Flow-Report-Path" , path)
960
.body ( Body:: from( bytes.to_vec ( ) ) )
961
.unwrap ( ) ;
962
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
963
let status = res.status ( ) ;
964
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
965
}
966
967
968
969
970
# [ tokio ::test ( flavor = " multi_thread" ) ]
971
async fn reports_are_kept_file_by_file_and_indexed ( ) {
972
let h = hub ( ) .await;
973
create ( & h.app, " andy" , " demo" ) .await;
974
let url = format! ( " {} /andy/demo" , h.base) ;
975
let work = tempfile:: tempdir( ) .unwrap ( ) ;
976
let c = client ( & url, work.path ( ) ) ;
977
write (
978
work.path ( ) ,
979
" .flow/pipeline.kdl" ,
980
& format! (
981
" 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 "
982
) ,
983
) ;
984
c.snapshot ( " test" ) .unwrap ( ) ;
985
c.commit ( " Measure frames" , & [ ] ) .unwrap ( ) ;
986
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
987
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
988
let a = claim ( & h.app) .await.unwrap ( ) ;
989
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
990
991
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 " ;
992
let page = b " <p>frames</p>" .to_vec ( ) ;
993
let data: Vec< u8 > = ( 0 .. 100_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
994
let files: [ ( & str , & [ u8 ] ) ; 3 ] = [ ( " data/frames.csv" , & data) , ( " index.html" , & page) , ( " report.toml" , toml) ] ;
995
996
assert_eq! ( upload_report_file ( & h.app, id, " tool" , " report.toml" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a report" ) ;
997
assert_eq! ( upload_report_file ( & h.app, id, " frame-timing" , " ../x" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a clean path" ) ;
998
let mut listed = vec! [ ] ;
999
for ( path, bytes) in files {
1000
let ( st, text) = upload_report_file ( & h.app, id, " frame-timing" , path, bytes) .await;
1001
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1002
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1003
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( bytes) .to_hex ( ) ) ;
1004
listed.push ( serde_json:: json! ( { " path" : path, " blob" : blob, " size" : bytes.len ( ) } ) ) ;
1005
}
1006
let mut missing = listed.clone ( ) ;
1007
missing.push ( serde_json:: json! ( { " path" : " assets/x.js" , " blob" : " ab" .repeat ( 32 ) , " size" : 1 } ) ) ;
1008
let ( st, text) =
1009
upload ( & h.app, id, " frame-timing" , & serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : missing } ) ) .unwrap ( ) ) .await;
1010
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a file never uploaded: {text}" ) ;
1011
assert! ( text.contains ( " assets/x.js" ) , " {text}" ) ;
1012
let manifest = serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : listed } ) ) .unwrap ( ) ;
1013
let ( st, text) = upload ( & h.app, id, " frame-timing" , & manifest) .await;
1014
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1015
1016
let ( kind, title, summary, files_n, bytes) : ( String , String , String , i64 , i64 ) =
1017
sqlx:: query_as( " SELECT kind, title, summary, files, bytes FROM reports WHERE job_id = ? AND name = 'frame-timing'" )
1018
.bind ( id)
1019
.fetch_one ( & h.app.db)
1020
.await
1021
.unwrap ( ) ;
1022
assert_eq! ( ( kind.as_str ( ) , title.as_str ( ) , summary.as_str ( ) ) , ( " performance" , " Frame timing" , " Median 16.4 ms" ) ) ;
1023
assert_eq! ( ( files_n, bytes) , ( 3 , ( data.len ( ) + page.len ( ) + toml.len ( ) ) as i64 ) ) ;
1024
let rid: i64 = sqlx:: query_scalar( " SELECT id FROM reports WHERE job_id = ?" ) .bind ( id) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
1025
let m = oxygenhub:: reports:: metrics( & h.app.db, rid) .await.unwrap ( ) ;
1026
assert_eq! ( ( m[ 0 ] .key.as_str ( ) , m[ 0 ] .value, m[ 0 ] .fail_above) , ( " median_ms" , 16. 4 , Some ( 20. 0 ) ) ) ;
1027
let log = db:: flow:: log( & h.app.db, id, 0 , 1000 ) .await.unwrap ( ) ;
1028
assert! ( log.iter ( ) .any ( | r | r.text.contains ( " report frame-timing: performance \" Frame timing\" , 1 metric" ) ) , " {log:?}" ) ;
1029
let _ = run;
1030
1031
1032
{
1033
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
1034
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
1035
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
1036
hosted.save_config ( ) .unwrap ( ) ;
1037
}
1038
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
1039
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
1040
let hosted = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
1041
for ( path, bytes) in files {
1042
assert_eq! ( hosted.read_blob ( & oxy_core:: repo:: Repo:: blob_ref( bytes) ) .unwrap ( ) , bytes, " {path} survives gc" ) ;
1043
}
1044
}
1045
d29070fa Andy McCall 1 month ago ↶
1046
# [ tokio ::test ( flavor = " multi_thread" ) ]
1047
async fn a_failing_job_keeps_an_artifact_outside_its_receipt ( ) {
1048
let h = hub ( ) .await;
1049
create ( & h.app, " andy" , " demo" ) .await;
1050
let url = format! ( " {} /andy/demo" , h.base) ;
1051
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1052
let c = client ( & url, work.path ( ) ) ;
1053
write (
1054
work.path ( ) ,
1055
" .flow/pipeline.kdl" ,
1056
& format! (
336c953a Andy McCall 1 month ago ↶
1057
" 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 ↶
1058
) ,
1059
) ;
1060
c.snapshot ( " test" ) .unwrap ( ) ;
1061
c.commit ( " Test" , & [ ] ) .unwrap ( ) ;
1062
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1063
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1064
let a = claim ( & h.app) .await.unwrap ( ) ;
336c953a Andy McCall 1 month ago ↶
1065
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 ↶
1066
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
0b203bbf Andy McCall 1 month ago ↶
1067
1068
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>"# ;
1069
let ( st, text) = upload ( & h.app, id, " junit" , xml) .await;
d29070fa Andy McCall 1 month ago ↶
1070
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1071
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1072
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 1 ) ;
1073
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1074
assert_eq! (
1075
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
1076
StatusCode:: BAD_REQUEST ,
1077
" a failing receipt names no outputs"
1078
) ;
1079
r[ " outputs" ] = serde_json:: json! ( [ ] ) ;
1080
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r } ) ) .await, StatusCode:: NO_CONTENT ) ;
1081
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
1082
assert! ( page.contains ( & format! ( " /andy/demo/flow/{} /artifacts/test/junit" , run.number) ) , " listed" ) ;
1083
assert! ( page.contains ( " not in its receipt" ) , " marked" ) ;
0b203bbf Andy McCall 1 month ago ↶
1084
assert! ( page.contains ( " 2 tests: 1 failed" ) , " counts beside the job" ) ;
1085
assert! ( page.contains ( " tests.test_io::test_write" ) & & page.contains ( " disk full" ) & & page.contains ( " OSError" ) , " the failure" ) ;
1086
assert! ( page.contains ( " tests: 2 tests: 1 failed in 2.0 s" ) , " said in the job's log" ) ;
d29070fa Andy McCall 1 month ago ↶
1087
}
1088
df940d75 Andy McCall 1 month ago ↶
1089
1090
1091
1092
# [ tokio ::test ( flavor = " multi_thread" ) ]
1093
async fn a_release_is_audited_from_authors_to_receipts ( ) {
1094
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
1095
use oxy_core:: identity:: SecretKey;
1096
use oxy_core:: objects:: StateObject;
1097
use oxy_core:: receipt:: SignedReceipt;
1098
let h = hub ( ) .await;
1099
create ( & h.app, " andy" , " demo" ) .await;
1100
let url = format! ( " {} /andy/demo" , h.base) ;
1101
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1102
let mut c = client ( & url, work.path ( ) ) ;
1103
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
1104
write (
1105
work.path ( ) ,
1106
" .flow/pipeline.kdl" ,
1107
& format! (
1108
" 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 "
1109
) ,
1110
) ;
1111
c.snapshot ( " test" ) .unwrap ( ) ;
1112
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
1113
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
1114
c.tag_create ( " v1" , & heads, " first" ) .unwrap ( ) ;
1115
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1116
1117
1118
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1119
let a = claim ( & h.app) .await.unwrap ( ) ;
1120
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1121
let body: Vec< u8 > = b " #!/bin/sh\n echo tool\n " .to_vec ( ) ;
1122
let ( _ , text) = upload ( & h.app, id, " tool" , & body) .await;
1123
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1124
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
1125
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1126
let heads_run: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1127
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads_run) .encode ( ) ) ;
1128
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
1129
let sk = SecretKey:: generate( ) ;
1130
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
1131
let fr: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
1132
let signed = hex:: encode( SignedReceipt:: sign( fr.to_oxy ( ) , repo_id, state, & format! ( " {url} /flow/1" ) , & runner, & sk) .encode ( ) ) ;
1133
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 900 , " receipt" : r, " signed_receipt" : signed } ) ;
1134
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
1135
1136
1137
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1138
let rel = db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
1139
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " tool" , " " , & body) .await.unwrap ( ) ;
1140
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " notes.txt" , " " , b " by hand\n " ) .await.unwrap ( ) ;
1141
1142
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1" , None , None ) .await;
1143
assert_eq! ( st, StatusCode:: OK ) ;
1144
for want in [
1145
" Tag v1 is signed" ,
1146
" The one change in the release is authenticated" ,
1147
" tool: signed receipt from run #1 build" ,
1148
" a runner key of this hub" ,
1149
" tool: built from the tagged state" ,
1150
" tool: pinned environment" ,
1151
" notes.txt: no build found" ,
1152
" <svg class=\" trust-graph\" " ,
bf5101d3 Andy McCall 1 month ago ↶
1153
1154
1155
" Tag signed by" ,
1156
" 1 of 1 changes signed or sealed" ,
1157
" 1 of 2 assets built and attested by OxygenFlow" ,
1158
" attested build" ,
1159
" uploaded by hand" ,
1160
" receipt verified" ,
1161
" class=\" shackle\" " ,
1162
" <title>notes.txt:" ,
df940d75 Andy McCall 1 month ago ↶
1163
] {
1164
assert! ( page.contains ( want) , " missing {want:?}" ) ;
1165
}
bf5101d3 Andy McCall 1 month ago ↶
1166
let svg = & page[ page.find ( " <svg class=\" trust-graph\" " ) .unwrap ( ) .. ] ;
1167
assert! ( svg[ .. 200 ] .contains ( " width=\" " ) & & svg[ .. 200 ] .contains ( " height=\" " ) , " the graph has its own size: {}" , & svg[ .. 200 ] ) ;
df940d75 Andy McCall 1 month ago ↶
1168
let ( _ , releases) = call ( & h.app, " GET" , " /andy/demo/releases" , None , None ) .await;
1169
assert! ( releases.contains ( " /andy/demo/releases/trust/v1" ) , " the release links its audit" ) ;
1170
1171
1172
let ( st, json) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1?format=json" , None , None ) .await;
1173
assert_eq! ( st, StatusCode:: OK ) ;
1174
let t: serde_json:: Value = serde_json:: from_str( & json) .unwrap ( ) ;
1175
assert_eq! ( ( t[ " tag" ] .as_str ( ) , t[ " tag_signature" ] .as_str ( ) , t[ " changes" ] .as_i64 ( ) ) , ( Some ( " v1" ) , Some ( " valid" ) , Some ( 1 ) ) ) ;
1176
assert_eq! ( t[ " authors" ] [ 0 ] [ " who" ] [ " name" ] , " Andy" ) ;
1177
let tool = t[ " assets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | a | a[ " name" ] = = " tool" ) .unwrap ( ) ;
1178
let hex_receipt = tool[ " build" ] [ " signed_receipt" ] .as_str ( ) .unwrap ( ) ;
1179
let sr = SignedReceipt:: decode( & hex:: decode( hex_receipt) .unwrap ( ) ) .unwrap ( ) ;
1180
sr.verify ( ) .unwrap ( ) ;
1181
assert_eq! ( sr.receipt.outputs[ 0 ] .to_hex ( ) , blob) ;
1182
let audit = t[ " audit" ] .as_array ( ) .unwrap ( ) ;
1183
assert! ( audit.iter ( ) .any ( | c | c[ " verdict" ] = = " warn" & & c[ " title" ] .as_str ( ) .unwrap ( ) .starts_with ( " notes.txt" ) ) ) ;
1184
assert! ( ! audit.iter ( ) .any ( | c | c[ " verdict" ] = = " fail" ) , " {audit:?}" ) ;
1185
1186
let ( st, api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v1" , None , None ) .await;
1187
assert_eq! ( st, StatusCode:: OK ) ;
1188
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & api) .unwrap ( ) [ " assets" ] .as_array ( ) .unwrap ( ) .len ( ) , 2 ) ;
1189
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1190
assert_eq! ( call ( & h.app, " GET" , " /andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1191
}
b6b7e4cf Andy McCall 1 month ago ↶
1192
1193
1194
1195
1196
# [ tokio ::test ( flavor = " multi_thread" ) ]
1197
async fn runners_register_show_their_history_and_are_drained_and_removed ( ) {
1198
use oxy_core:: identity:: SecretKey;
1199
let h = hub ( ) .await;
1200
create ( & h.app, " andy" , " demo" ) .await;
1201
1202
let res = oxygenhub:: router( h.app.clone ( ) )
1203
.oneshot (
1204
Request:: post( " /register" )
1205
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1206
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery" ) )
1207
.unwrap ( ) ,
1208
)
1209
.await
1210
.unwrap ( ) ;
1211
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1212
let admin = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
1213
db:: accounts:: set_admin( & h.app.db, admin.id, true ) .await.unwrap ( ) ;
1214
let web = | method : & 'static str , path : String, body : Option< String> | {
1215
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1216
async move {
1217
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1218
if body.is_some ( ) {
1219
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1220
}
1221
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1222
let st = res.status ( ) ;
1223
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1224
}
1225
} ;
1226
let csrf_of = | page : & str | {
1227
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1228
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1229
} ;
1230
let ( st, page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1231
assert_eq! ( st, StatusCode:: OK ) ;
1232
assert! ( page.contains ( " No runners yet" ) ) ;
1233
assert_eq! ( call ( & h.app, " GET" , " /admin/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
1234
1235
1236
let ( _ , page) = web ( " POST" , " /admin/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1237
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1238
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1239
assert! ( token.starts_with ( " flowreg_" ) , " {token}" ) ;
1240
1241
1242
let sk = SecretKey:: generate( ) ;
1243
let identity = sk.identity ( " plex (OxygenFlow runner)" , " " , 1 ) ;
1244
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 } ) ;
1245
let reg = serde_json:: json! ( { " token" : token, " name" : " plex" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1246
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1247
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1248
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1249
assert! ( credential.starts_with ( " flowrunner_" ) ) ;
1250
assert_eq! (
1251
call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 ,
1252
StatusCode:: FORBIDDEN ,
1253
" used once"
1254
) ;
1255
1256
1257
let claim_as = | who : String| {
1258
let app = h.app.clone ( ) ;
1259
let info = info.clone ( ) ;
1260
async move {
1261
let body = serde_json:: json! ( { " runner" : " ignored" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1262
call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await
1263
}
1264
} ;
1265
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " nothing queued" ) ;
1266
assert_eq! ( claim ( & h.app) .await, None ) ;
1267
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1268
assert! (
1269
page.contains ( " >plex</strong>" ) & & page.contains ( " st-online" ) & & page.contains ( " x86_64 · linux" ) & & page.contains ( " big-disk" ) ,
1270
" {page}"
1271
) ;
1272
assert! ( page.contains ( " >test-1</strong>" ) & & page.contains ( " hub.toml" ) , " the configured runner, by its claimed name" ) ;
1273
let plex = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
1274
assert_eq! ( plex.key, identity.hash ( ) .to_hex ( ) , " bound at registration" ) ;
1275
1276
1277
let url = format! ( " {} /andy/demo" , h.base) ;
1278
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1279
let c = client ( & url, work.path ( ) ) ;
1280
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1281
c.snapshot ( " test" ) .unwrap ( ) ;
1282
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1283
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1284
runs_of ( & h.app, " andy" , " demo" ) .await;
1285
let ( st, body) = claim_as ( credential.clone ( ) ) .await;
1286
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1287
let job: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1288
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1289
assert! (
1290
page.contains ( " andy/demo" ) & & page.contains ( job[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) & & page.contains ( " Running now</dt><dd>1 job" ) ,
1291
" {page}"
1292
) ;
b66e70b7 Andy McCall 1 month ago ↶
1293
1294
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1295
assert! ( page.contains ( " <h2>Queue</h2>" ) & & page.contains ( " <td>plex</td>" ) , " {page}" ) ;
1296
let ( _ , tokens) = web ( " GET" , " /settings/tokens" .into ( ) , None ) .await;
1297
let ( _ , made) = web ( " POST" , " /settings/tokens" .into ( ) , Some ( format! ( " _csrf={} &name=admin&scopes=user%3Aread" , csrf_of ( & tokens) ) ) ) .await;
1298
let i = made.find ( " oxyhub_" ) .unwrap ( ) ;
1299
let admin_token: String = made[ i.. ] .chars ( ) .take_while ( | c | c.is_ascii_alphanumeric ( ) | | * c = = ' _' ) .collect ( ) ;
1300
let ( st, body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1301
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1302
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1303
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1304
assert_eq! ( ( plex_row[ " running" ] .as_i64 ( ) , plex_row[ " arch" ] .as_str ( ) ) , ( Some ( 1 ) , Some ( " x86_64" ) ) ) ;
7f5f6c05 Andy McCall 1 month ago ↶
1305
1306
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1307
let ( st, _ ) = web (
1308
" POST" ,
1309
format! ( " /admin/runners/{} /limits" , plex.id) ,
1310
Some ( format! ( " _csrf={} &labels=gpu%2C+plex&timeout_minutes=5" , csrf_of ( & page) ) ) ,
1311
)
1312
.await;
1313
assert_eq! ( st, StatusCode:: SEE_OTHER ) ;
1314
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1315
assert! ( page.contains ( " >gpu<" ) & & page.contains ( " 5 min for its jobs" ) , " {page}" ) ;
1316
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1317
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1318
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1319
assert! ( plex_row[ " labels" ] .as_array ( ) .unwrap ( ) .iter ( ) .any ( | l | l = = " gpu" ) , " {plex_row}" ) ;
1320
let ( st, _ ) = web (
1321
" POST" ,
1322
format! ( " /admin/runners/{} /limits" , plex.id) ,
1323
Some ( format! ( " _csrf={} &labels=bad%20label%21&timeout_minutes=" , csrf_of ( & page) ) ) ,
1324
)
1325
.await;
1326
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a label with a space or punctuation is refused" ) ;
b66e70b7 Andy McCall 1 month ago ↶
1327
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners/queue" , Some ( & admin_token) , None ) .await;
1328
let queue: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1329
assert! ( queue.as_array ( ) .unwrap ( ) .iter ( ) .any ( | q | q[ " status" ] = = " running" & & q[ " runner" ] = = " plex" ) , " {queue}" ) ;
1330
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/runners/{} /jobs" , plex.id) , Some ( & admin_token) , None ) .await;
1331
let jobs: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1332
assert_eq! ( jobs[ 0 ] [ " repo" ] , " andy/demo" , " {jobs}" ) ;
1333
assert_eq! ( call ( & h.app, " GET" , " /api/v1/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
b6b7e4cf Andy McCall 1 month ago ↶
1334
1335
1336
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1337
let csrf = csrf_of ( & page) ;
1338
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=draining" ) ) ) .await;
1339
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " a draining runner takes nothing" ) ;
1340
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1341
assert! ( page.contains ( " is running jobs: drain it first" ) , " {page}" ) ;
1342
1343
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1344
let done = call (
1345
& h.app,
1346
" POST" ,
1347
& format! ( " /api/v1/flow/jobs/{id} /finish" ) ,
1348
Some ( & credential) ,
1349
Some ( r # "{"exit_code":0,"duration_ms":5}"# .into ( ) ) ,
1350
)
1351
.await;
1352
assert_eq! ( done.0 , StatusCode:: NO_CONTENT ) ;
1353
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=disabled" ) ) ) .await;
1354
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: FORBIDDEN ) ;
1355
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1356
assert! ( page.contains ( " Removed plex." ) , " {page}" ) ;
1357
assert_eq! ( claim_as ( credential) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1358
}
74be9796 Andy McCall 1 month ago ↶
1359
1360
1361
1362
# [ tokio ::test ( flavor = " multi_thread" ) ]
1363
async fn scoped_runners_take_only_their_scope ( ) {
1364
use oxy_core:: identity:: SecretKey;
1365
let h = hub ( ) .await;
1366
create ( & h.app, " andy" , " demo" ) .await;
1367
create ( & h.app, " andy" , " other" ) .await;
1368
let register = | scope : db:: runners:: Scope, name : & 'static str | {
1369
let app = h.app.clone ( ) ;
1370
async move {
1371
let token = format! ( " flowreg_{name} " ) ;
1372
db:: runners:: add_registration( & app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & scope) .await.unwrap ( ) ;
1373
let identity = SecretKey:: generate( ) .identity ( name, " " , 1 ) ;
1374
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1375
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1376
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1377
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1378
serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( )
1379
}
1380
} ;
1381
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1382
let only_demo = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } , " demo-runner" ) .await;
1383
let carols = register ( db:: runners:: Scope { owner: " carol" .into ( ) , repo_id: None } , " carol-runner" ) .await;
1384
let andys = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: None } , " andy-runner" ) .await;
1385
let claim_as = | who : String| {
1386
let app = h.app.clone ( ) ;
1387
async move {
1388
let body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
1389
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await;
1390
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
1391
}
1392
} ;
1393
1394
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1395
let c = client ( & format! ( " {} /andy/other" , h.base) , work.path ( ) ) ;
1396
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1397
c.snapshot ( " t" ) .unwrap ( ) ;
1398
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1399
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1400
assert! ( ! runs_of ( & h.app, " andy" , " other" ) .await.is_empty ( ) ) ;
1401
assert_eq! ( claim_as ( only_demo.clone ( ) ) .await, None , " nothing of andy/demo is queued" ) ;
1402
assert_eq! ( claim_as ( carols) .await, None , " nothing of carol's" ) ;
1403
assert_eq! ( claim_as ( andys) .await.as_deref ( ) , Some ( " andy/other" ) , " the owner's runner takes its repositories' jobs" ) ;
1404
1405
assert_eq! ( claim ( & h.app) .await.unwrap ( ) [ " repo" ] , " andy/other" ) ;
1406
assert_eq! ( claim_as ( only_demo) .await, None , " still nothing of its own" ) ;
1407
}
7974adb0 Andy McCall 1 month ago ↶
1408
1409
1410
1411
1412
# [ tokio ::test ( flavor = " multi_thread" ) ]
1413
async fn repository_admins_add_and_remove_their_own_runners ( ) {
1414
use oxy_core:: identity:: SecretKey;
1415
let h = hub ( ) .await;
1416
let res = oxygenhub:: router( h.app.clone ( ) )
1417
.oneshot (
1418
Request:: post( " /register" )
1419
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1420
.body ( Body:: from( " username=andy&email=andy%40example.com&password=correct-horse-battery" ) )
1421
.unwrap ( ) ,
1422
)
1423
.await
1424
.unwrap ( ) ;
1425
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1426
create ( & h.app, " andy" , " demo" ) .await;
1427
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1428
let web = | method : & 'static str , path : String, body : Option< String> | {
1429
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1430
async move {
1431
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1432
if body.is_some ( ) {
1433
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1434
}
1435
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1436
let st = res.status ( ) ;
1437
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1438
}
1439
} ;
1440
let csrf_of = | page : & str | {
1441
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1442
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1443
} ;
1444
let ( st, page) = web ( " GET" , " /andy/demo/settings" .into ( ) , None ) .await;
1445
assert_eq! ( st, StatusCode:: OK ) ;
1446
assert! ( page.contains ( " <h2>Runners</h2>" ) , " the owner sees the section" ) ;
1447
let ( _ , page) = web ( " POST" , " /andy/demo/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1448
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1449
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1450
let identity = SecretKey:: generate( ) .identity ( " mine" , " " , 1 ) ;
1451
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1452
let reg = serde_json:: json! ( { " token" : token, " name" : " mine" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1453
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1454
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1455
let mine = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " mine" ) .unwrap ( ) ;
1456
assert_eq! ( mine.scope ( ) , db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } ) ;
1457
1458
claim ( & h.app) .await;
1459
let site = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " test-1" ) .unwrap ( ) ;
1460
let ( _ , page) = web ( " GET" , " /andy/demo/settings" .into ( ) , None ) .await;
1461
assert! ( page.contains ( " <strong>mine</strong>" ) & & page.contains ( " this repository" ) , " {page}" ) ;
1462
assert! ( page.contains ( " <strong>test-1</strong>" ) & & page.contains ( " the whole site" ) , " {page}" ) ;
1463
let csrf = csrf_of ( & page) ;
1464
let ( st, _ ) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , site.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1465
assert_eq! ( st, StatusCode:: NOT_FOUND , " not the repository's own" ) ;
1466
let ( _ , page) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , mine.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1467
assert! ( page.contains ( " mine removed" ) , " {page}" ) ;
1468
assert! ( db:: runners:: get( & h.app.db, mine.id) .await.unwrap ( ) .is_none ( ) ) ;
1469
1470
1471
let ( _ , page) = web ( " GET" , " /orgs/new" .into ( ) , None ) .await;
1472
web ( " POST" , " /orgs/new" .into ( ) , Some ( format! ( " _csrf={} &name=acme&display_name=Acme" , csrf_of ( & page) ) ) ) .await;
1473
let ( st, page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1474
assert_eq! ( st, StatusCode:: OK ) ;
1475
let ( _ , page) = web ( " POST" , " /orgs/acme/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1476
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1477
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1478
let identity = SecretKey:: generate( ) .identity ( " acme-runner" , " " , 1 ) ;
1479
let reg = serde_json:: json! ( { " token" : token, " name" : " acme-runner" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1480
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 , StatusCode:: OK ) ;
1481
let acme = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " acme-runner" ) .unwrap ( ) ;
1482
assert_eq! ( acme.scope ( ) , db:: runners:: Scope { owner: " acme" .into ( ) , repo_id: None } ) ;
1483
let ( _ , page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1484
assert! ( page.contains ( " <strong>acme-runner</strong>" ) & & page.contains ( " the organisation's repositories" ) , " {page}" ) ;
1485
let ( _ , page) = web ( " POST" , format! ( " /orgs/acme/settings/runners/{} /remove" , acme.id) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1486
assert! ( page.contains ( " acme-runner removed" ) , " {page}" ) ;
1487
}
c4c87f48 Andy McCall 1 month ago ↶
1488
1489
1490
1491
# [ tokio ::test ( flavor = " multi_thread" ) ]
1492
async fn jobs_that_ask_for_labels_wait_for_a_runner_with_them ( ) {
1493
use oxy_core:: identity:: SecretKey;
1494
let h = hub ( ) .await;
1495
create ( & h.app, " andy" , " demo" ) .await;
1496
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1497
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
1498
let cross = " job \" cross\" {\n sandbox \" alpine\" \n runs-on \" riscv64\" \n exec \" true\" \n }\n " ;
1499
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( cross) ) ;
1500
c.snapshot ( " t" ) .unwrap ( ) ;
1501
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1502
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1503
assert! ( ! runs_of ( & h.app, " andy" , " demo" ) .await.is_empty ( ) ) ;
1504
1505
let mut taken = Vec :: new( ) ;
1506
while let Some ( j) = claim ( & h.app) .await {
1507
taken.push ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
1508
}
1509
assert! ( ! taken.contains ( & " cross" .to_string ( ) ) & & taken.contains ( & " lint" .to_string ( ) ) , " {taken:?}" ) ;
1510
let queue = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
1511
let waiting = queue.iter ( ) .find ( | q | q.job = = " cross" ) .unwrap ( ) ;
1512
assert_eq! ( ( waiting.status.as_str ( ) , waiting.needs ( ) ) , ( " queued" , vec! [ " riscv64" .to_string ( ) ] ) ) ;
1513
1514
let token = " flowreg_riscv" ;
1515
db:: runners:: add_registration( & h.app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & Default :: default( ) ) .await.unwrap ( ) ;
1516
let identity = SecretKey:: generate( ) .identity ( " visionfive" , " " , 1 ) ;
1517
let info = serde_json:: json! ( { " arch" : " riscv64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ " riscv64" ] , " max_jobs" : 1 } ) ;
1518
let reg = serde_json:: json! ( { " token" : token, " name" : " visionfive" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1519
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1520
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1521
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1522
let claim_body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1523
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( claim_body) ) .await;
1524
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1525
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] , " cross" ) ;
1526
}
070a7b64 Andy McCall 1 month ago ↶
1527
1528
1529
1530
1531
1532
# [ tokio ::test ( flavor = " multi_thread" ) ]
1533
async fn a_job_no_runner_reports_is_ended_after_the_time_limit ( ) {
1534
let h = hub ( ) .await;
1535
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1536
create ( & h.app, " andy" , " lost" ) .await;
1537
let url = format! ( " {} /andy/lost" , h.base) ;
1538
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1539
let c = client ( & url, work.path ( ) ) ;
1540
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1541
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1542
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1543
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1544
assert_eq! ( runs_of ( & h.app, " andy" , " lost" ) .await.len ( ) , 1 ) ;
1545
let job = claim ( & h.app) .await.expect ( " a job to claim" ) ;
1546
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1547
1548
1549
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 0 ) ;
1550
1551
let old = db:: now_ms( ) - 24 * 3600 * 1000 ;
1552
sqlx:: query( " UPDATE flow_jobs SET started_ms = ? WHERE id = ?" ) .bind ( old) .bind ( id) .execute ( & h.app.db) .await.unwrap ( ) ;
1553
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 1 ) ;
1554
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1555
assert_eq! ( j.status, " error" ) ;
1556
assert! ( j.error.starts_with ( " lost:" ) , " {}" , j.error) ;
1557
let run = & runs_of ( & h.app, " andy" , " lost" ) .await[ 0 ] ;
1558
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
1559
assert! ( jobs.iter ( ) .all ( | j | j.status ! = " running" ) , " nothing left running" ) ;
1560
1561
assert_ne! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 } ) ) .await, StatusCode:: NO_CONTENT ) ;
1562
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " error" ) ;
1563
}
b0612e7d Andy McCall 1 month ago ↶
1564
1565
1566
1567
# [ tokio ::test ( flavor = " multi_thread" ) ]
1568
async fn a_runner_is_offered_only_jobs_whose_image_it_holds ( ) {
1569
let h = hub ( ) .await;
1570
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1571
create ( & h.app, " andy" , " images" ) .await;
1572
let url = format! ( " {} /andy/images" , h.base) ;
1573
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1574
let c = client ( & url, work.path ( ) ) ;
1575
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1576
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1577
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1578
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1579
assert_eq! ( runs_of ( & h.app, " andy" , " images" ) .await.len ( ) , 1 ) ;
1580
let claim_with = | images : Vec< String> | {
1581
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " images" : images } ) .to_string ( ) ;
1582
let app = h.app.clone ( ) ;
1583
async move {
1584
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await;
1585
( st, body)
1586
}
1587
} ;
1588
let ( st, _ ) = claim_with ( vec! [ " ab" .repeat ( 32 ) ] ) .await;
1589
assert_eq! ( st, StatusCode:: NO_CONTENT , " no job for a runner without the image" ) ;
1590
let jobs = db:: flow:: jobs( & h.app.db, runs_of ( & h.app, " andy" , " images" ) .await[ 0 ] .id) .await.unwrap ( ) ;
1591
let rootfs: serde_json:: Value = serde_json:: from_str( & jobs[ 0 ] .sandbox) .unwrap ( ) ;
1592
let rootfs = rootfs[ " rootfs" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1593
let ( st, body) = claim_with ( vec! [ " ab" .repeat ( 32 ) , rootfs] ) .await;
1594
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1595
}
eef677c4 Andy McCall 1 month ago ↶
1596
1597
1598
1599
1600
# [ tokio ::test ( flavor = " multi_thread" ) ]
1601
async fn a_job_its_runner_no_longer_holds_is_ended_at_its_next_claim ( ) {
1602
let h = hub ( ) .await;
1603
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1604
create ( & h.app, " andy" , " held" ) .await;
1605
let url = format! ( " {} /andy/held" , h.base) ;
1606
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1607
let c = client ( & url, work.path ( ) ) ;
1608
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1609
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1610
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1611
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1612
assert_eq! ( runs_of ( & h.app, " andy" , " held" ) .await.len ( ) , 1 ) ;
1613
let id = claim ( & h.app) .await.expect ( " a job to claim" ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1614
let claim_holding = | holding : Vec< i64 > | {
1615
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " holding" : holding } ) .to_string ( ) ;
1616
let app = h.app.clone ( ) ;
1617
async move { call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await.0 }
1618
} ;
1619
claim_holding ( vec! [ id] ) .await;
1620
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " running" , " a held job stays" ) ;
1621
claim_holding ( vec! [ ] ) .await;
1622
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1623
assert_eq! ( j.status, " error" ) ;
1624
assert! ( j.error.starts_with ( " lost:" ) & & j.error.contains ( " no longer holds" ) , " {}" , j.error) ;
1625
let run = & runs_of ( & h.app, " andy" , " held" ) .await[ 0 ] ;
1626
assert! ( db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.status ! = " running" ) , " the run settles" ) ;
1627
}
d7ce41ac Andy McCall 1 month ago ↶
1628
1629
1630
1631
1632
# [ tokio ::test ( flavor = " multi_thread" ) ]
1633
async fn a_scoped_job_runs_only_when_a_push_touches_its_paths ( ) {
1634
let h = hub ( ) .await;
1635
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1636
create ( & h.app, " andy" , " scoped" ) .await;
1637
let url = format! ( " {} /andy/scoped" , h.base) ;
1638
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1639
let c = client ( & url, work.path ( ) ) ;
1640
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \" packaging/**\" \n exec \" true\" \n }\n " ;
1641
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1642
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1643
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1644
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1645
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1646
let names = | app : Shared, n : usize | async move {
1647
let mut runs = Vec :: new( ) ;
1648
for _ in 0 .. 100 {
1649
runs = runs_of ( & app, " andy" , " scoped" ) .await;
1650
if runs.len ( ) > = n {
1651
break ;
1652
}
1653
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1654
}
1655
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1656
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1657
j.sort ( ) ;
745d225d Andy McCall 1 month ago ↶
1658
1659
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = ?" ) .bind ( runs[ 0 ] .id) .execute ( & app.db) .await.unwrap ( ) ;
d7ce41ac Andy McCall 1 month ago ↶
1660
j
1661
} ;
1662
assert_eq! ( names ( h.app.clone ( ) , 1 ) .await, [ " build" , " dist" , " lint" , " test" ] , " the first run has every job" ) ;
1663
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1664
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1665
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1666
assert_eq! ( names ( h.app.clone ( ) , 2 ) .await, [ " build" , " lint" , " test" ] , " dist is out of scope" ) ;
1667
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1668
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
1669
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1670
assert_eq! ( names ( h.app.clone ( ) , 3 ) .await, [ " build" , " dist" , " lint" , " test" ] , " a version bump runs dist" ) ;
1671
}
5dead1f4 Andy McCall 1 month ago ↶
1672
745d225d Andy McCall 1 month ago ↶
1673
1674
1675
1676
# [ tokio ::test ( flavor = " multi_thread" ) ]
1677
async fn runs_are_cancelled_by_hand_or_by_a_newer_push ( ) {
1678
let h = hub ( ) .await;
1679
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1680
create ( & h.app, " andy" , " stop" ) .await;
1681
let url = format! ( " {} /andy/stop" , h.base) ;
1682
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1683
let c = client ( & url, work.path ( ) ) ;
1684
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \n exec \" true\" \n }\n " ;
1685
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1686
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1687
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1688
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1689
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1690
let wait_runs = | app : Shared, n : usize | async move {
1691
for _ in 0 .. 100 {
1692
let runs = runs_of ( & app, " andy" , " stop" ) .await;
1693
if runs.len ( ) > = n {
1694
return runs;
1695
}
1696
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1697
}
1698
panic! ( " no run {n}" ) ;
1699
} ;
1700
let first = wait_runs ( h.app.clone ( ) , 1 ) .await;
1701
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = ?" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
1702
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE run_id = ?" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
1703
1704
1705
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1706
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
1707
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1708
wait_runs ( h.app.clone ( ) , 2 ) .await;
1709
let job = claim ( & h.app) .await.expect ( " a job of run 2" ) ;
1710
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1711
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1712
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1713
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1714
let runs = wait_runs ( h.app.clone ( ) , 3 ) .await;
1715
let third = & runs[ 0 ] ;
1716
let mut second = runs[ 1 ] .clone ( ) ;
1717
for _ in 0 .. 100 {
1718
second = db:: flow:: run_by_id( & h.app.db, runs[ 1 ] .id) .await.unwrap ( ) .unwrap ( ) ;
1719
if ! second.live ( ) {
1720
break ;
1721
}
1722
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1723
}
1724
assert_eq! ( second.status, " cancelled" , " a newer push supersedes a run still going" ) ;
1725
let jobs = db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) ;
1726
assert! ( jobs.iter ( ) .all ( | j | j.status = = " cancelled" & & j.error = = " superseded by run #3" ) , " {jobs:?}" ) ;
1727
let mut names: Vec< String> = db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1728
names.sort ( ) ;
1729
assert_eq! ( names, [ " build" , " dist" , " lint" , " test" ] , " run 3 also checks what run 2 would have: the version bump" ) ;
1730
let mut checks = vec! [ ] ;
1731
for _ in 0 .. 100 {
1732
checks = db:: hooks:: checks_for_state( & h.app.db, second.repo_id, & second.state) .await.unwrap ( ) ;
1733
if checks.iter ( ) .all ( | c | c.status = = " cancelled" ) {
1734
break ;
1735
}
1736
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1737
}
1738
assert! ( checks.iter ( ) .all ( | c | c.status = = " cancelled" ) , " {checks:?}" ) ;
1739
1740
let ( st, _ ) =
1741
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( r # "{"records":[]}"# .into ( ) ) ) .await;
1742
assert_eq! ( st, StatusCode:: CONFLICT ) ;
1743
1744
1745
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
1746
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1747
let third = db:: flow:: run_by_id( & h.app.db, third.id) .await.unwrap ( ) .unwrap ( ) ;
1748
assert_eq! ( third.status, " cancelled" ) ;
1749
assert! ( db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.error = = " cancelled by andy" ) ) ;
1750
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
1751
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " already cancelled" ) , " {st} {body}" ) ;
1752
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , None , None ) .await;
1753
assert_ne! ( st, StatusCode:: OK , " anonymous callers cannot cancel" ) ;
1754
let ( _ , page) = call ( & h.app, " GET" , " /andy/stop/flow/3" , None , None ) .await;
1755
assert! ( page.contains ( " ⊘" ) & & ! page.contains ( " CANCEL</button>" ) , " a cancelled run shows so, with no button" ) ;
1756
}
1757
5dead1f4 Andy McCall 1 month ago ↶
1758
1759
1760
1761
# [ tokio ::test ( flavor = " multi_thread" ) ]
1762
async fn a_job_runs_under_its_own_time_limit_within_the_sites ( ) {
1763
let h = hub ( ) .await;
1764
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1765
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_DEFAULT , " 30" ) .await.unwrap ( ) ;
1766
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_MAX , " 120" ) .await.unwrap ( ) ;
1767
create ( & h.app, " andy" , " limits" ) .await;
1768
let url = format! ( " {} /andy/limits" , h.base) ;
1769
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1770
let c = client ( & url, work.path ( ) ) ;
1771
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 " ;
1772
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1773
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1774
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1775
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1776
assert_eq! ( runs_of ( & h.app, " andy" , " limits" ) .await.len ( ) , 1 ) ;
1777
let mut limits = std:: collections:: HashMap:: new( ) ;
1778
let mut ids = std:: collections:: HashMap:: new( ) ;
1779
while let Some ( j) = claim ( & h.app) .await {
1780
let name = j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1781
limits.insert ( name.clone ( ) , j[ " timeout_s" ] .as_u64 ( ) .unwrap ( ) ) ;
1782
ids.insert ( name, j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
1783
}
1784
assert_eq! ( limits[ " slow" ] , 5400 , " its own" ) ;
1785
assert_eq! ( limits[ " huge" ] , 7200 , " cut to the site's maximum" ) ;
1786
assert_eq! ( limits[ " build" ] , 1800 , " the site's default" ) ;
1787
1788
let started = db:: now_ms( ) - 40 * 60 * 1000 ;
1789
sqlx:: query( " UPDATE flow_jobs SET started_ms = ?" ) .bind ( started) .execute ( & h.app.db) .await.unwrap ( ) ;
1790
oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) ;
1791
let status = | id : i64 | {
1792
let app = h.app.clone ( ) ;
1793
async move { db:: flow:: job( & app.db, id) .await.unwrap ( ) .unwrap ( ) .status }
1794
} ;
1795
assert_eq! ( status ( ids[ " build" ] ) .await, " error" , " past its 30 minutes" ) ;
1796
assert_eq! ( status ( ids[ " slow" ] ) .await, " running" , " within its 90" ) ;
1797
assert_eq! ( status ( ids[ " huge" ] ) .await, " running" , " within its 120" ) ;
1798
}
c889911b Andy McCall 1 month ago ↶
1799
1800
1801
1802
1803
# [ tokio ::test ( flavor = " multi_thread" ) ]
1804
async fn on_decides_which_events_start_a_job ( ) {
1805
let h = hub ( ) .await;
1806
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1807
create ( & h.app, " andy" , " events" ) .await;
1808
let url = format! ( " {} /andy/events" , h.base) ;
1809
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1810
let c = client ( & url, work.path ( ) ) ;
1811
let text = format! (
1812
" on {{ \n push \" main\" \n manual\n }} \n {} job \" dist\" {{ \n sandbox \" alpine\" \n on {{ \n tag \" v*\" \n }} \n exec \" true\" \n }} \n " ,
1813
pipeline ( " " )
1814
) ;
1815
1816
let text = text.replacen (
1817
" on {\n push \" main\" \n manual\n }\n schema-version 1\n " ,
1818
" schema-version 1\n on {\n push \" main\" \n manual\n }\n " ,
1819
1 ,
1820
) ;
1821
write ( work.path ( ) , " .flow/pipeline.kdl" , & text) ;
1822
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1823
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1824
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1825
let jobs_of_latest = | app : Shared, n : usize | async move {
1826
let mut runs = Vec :: new( ) ;
1827
for _ in 0 .. 100 {
1828
runs = runs_of ( & app, " andy" , " events" ) .await;
1829
if runs.len ( ) > = n {
1830
break ;
1831
}
1832
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1833
}
1834
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1835
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1836
j.sort ( ) ;
1837
( runs[ 0 ] .clone ( ) , j)
1838
} ;
1839
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 1 ) .await;
1840
assert_eq! ( j, [ " build" , " lint" , " test" ] , " a push to main: no dist" ) ;
1841
1842
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
1843
c.tag_create ( " v1.0.0" , & heads, " first" ) .unwrap ( ) ;
1844
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1845
let ( run, j) = jobs_of_latest ( h.app.clone ( ) , 2 ) .await;
1846
assert_eq! ( j, [ " dist" ] , " a tag runs only what names it" ) ;
1847
assert_eq! ( run.trigger, " tag:v1.0.0" ) ;
1848
1849
let row = db:: repos:: get( & h.app.db, " andy" , " events" ) .await.unwrap ( ) .unwrap ( ) ;
1850
db:: flow:: finish(
1851
& h.app.db,
1852
db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) [ 0 ] .id,
1853
db:: flow:: Outcome { status: " passed" , exit_code: Some ( 0 ) , error: " " , receipt: " " , receipt_hash: " " } ,
1854
)
1855
.await
1856
.unwrap ( ) ;
1857
let run = db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) ;
1858
oxygenhub:: flow:: rerun( & h.app, & row, & run, " andy" ) .await.unwrap ( ) ;
1859
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 3 ) .await;
1860
assert_eq! ( j, [ " dist" ] , " the rerun is the tag's run again" ) ;
1861
1862
c.bookmark_create ( " develop" , None ) .unwrap ( ) ;
1863
assert! ( c.push_bookmark ( " origin" , " develop" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1864
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
1865
assert_eq! ( runs_of ( & h.app, " andy" , " events" ) .await.len ( ) , 3 , " no run for develop" ) ;
348f8c4b Andy McCall 1 month ago ↶
1866
}
1867
1868
1869
1870
# [ tokio ::test ( flavor = " multi_thread" ) ]
1871
async fn scheduled_jobs_run_when_due ( ) {
1872
let h = hub ( ) .await;
1873
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1874
create ( & h.app, " andy" , " nightly" ) .await;
1875
let url = format! ( " {} /andy/nightly" , h.base) ;
1876
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1877
let c = client ( & url, work.path ( ) ) ;
1878
1879
let extra = " job \" nightly\" {\n sandbox \" alpine\" \n on {\n schedule \" 17 4 29 2 *\" \n }\n exec \" true\" \n }\n " ;
1880
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1881
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1882
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1883
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1884
assert_eq! ( runs_of ( & h.app, " andy" , " nightly" ) .await.len ( ) , 1 ) ;
1885
let first = runs_of ( & h.app, " andy" , " nightly" ) .await;
1886
let names: Vec< String> = db:: flow:: jobs( & h.app.db, first[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1887
assert! ( ! names.contains ( & " nightly" .to_string ( ) ) , " a push does not run a scheduled job" ) ;
1888
use flow_core:: pipeline:: When;
1889
let not_yet = When { minute: 16 , hour: 4 , day: 29 , month: 2 , weekday: 4 } ;
1890
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, not_yet) .await, 0 ) ;
1891
let due = When { minute: 17 , .. not_yet } ;
1892
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, due) .await, 1 ) ;
1893
let runs = runs_of ( & h.app, " andy" , " nightly" ) .await;
1894
assert_eq! ( runs.len ( ) , 2 ) ;
1895
assert_eq! ( runs[ 0 ] .trigger, " schedule" ) ;
1896
let names: Vec< String> = db:: flow:: jobs( & h.app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1897
assert_eq! ( names, [ " nightly" ] ) ;
1898
assert_eq! ( runs[ 0 ] .bookmark, " main" , " on the default bookmark" ) ;
c889911b Andy McCall 1 month ago ↶
1899
}
2a83f530 Andy McCall 1 month ago ↶
1900
1901
# [ tokio ::test ( flavor = " multi_thread" ) ]
1902
async fn variables_from_three_levels_reach_a_job_inner_overriding_outer ( ) {
1903
use oxygenhub:: levels:: Scope;
1904
use oxygenhub:: variables;
1905
let h = hub ( ) .await;
1906
let app = & h.app;
1907
1908
let res = oxygenhub:: router( app.clone ( ) )
1909
.oneshot (
1910
Request:: post( " /register" )
1911
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1912
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery" ) )
1913
.unwrap ( ) ,
1914
)
1915
.await
1916
.unwrap ( ) ;
1917
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1918
let andy = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
1919
db:: accounts:: create_org( & app.db, " acme" , " Acme" , andy.id) .await.unwrap ( ) ;
1920
create ( app, " acme" , " app" ) .await;
1921
let page = | path : & str , form : Option< String> | {
1922
let mut req =
1923
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
1924
if form.is_some ( ) {
1925
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1926
}
1927
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
1928
let app = app.clone ( ) ;
1929
async move {
1930
let res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
1931
let status = res.status ( ) ;
1932
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
1933
( status, body)
1934
}
1935
} ;
1936
let csrf_of = | page : & str | {
1937
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
1938
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1939
} ;
1940
1941
1942
1943
variables:: set( & app.db, Scope:: Site, " CC" , " gcc" ) .await.unwrap ( ) ;
1944
variables:: set( & app.db, Scope:: Site, " LANG" , " C" ) .await.unwrap ( ) ;
1945
let org = db:: accounts:: org_by_name( & app.db, " acme" ) .await.unwrap ( ) .unwrap ( ) ;
1946
variables:: set( & app.db, Scope:: Org( org.id) , " CC" , " clang" ) .await.unwrap ( ) ;
1947
let ( st, settings) = page ( " /acme/app/settings" , None ) .await;
1948
assert_eq! ( st, StatusCode:: OK , " {settings}" ) ;
1949
assert! ( settings.contains ( " Inherited" ) & & settings.contains ( " the acme organisation" ) & & settings.contains ( " overridden" ) , " {settings}" ) ;
1950
let csrf = csrf_of ( & settings) ;
1951
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=RUST_LOG&value=debug" ) ) ) .await;
1952
assert! ( body.contains ( " RUST_LOG set." ) & & body.contains ( " <code>debug</code>" ) , " {body}" ) ;
1953
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=FLOW_REPO&value=x" ) ) ) .await;
1954
assert! ( body.contains ( " not FLOW_" ) , " {body}" ) ;
1955
let repo = db:: repos:: get( & app.db, " acme" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
1956
db:: repos:: set_flow( & app.db, repo.id, " on" ) .await.unwrap ( ) ;
1957
let env = variables:: env_for_repo( & app.db, & repo) .await.unwrap ( ) ;
1958
assert_eq! (
1959
env.iter ( ) .map ( | ( k , v ) | ( k.as_str ( ) , v.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
1960
[ ( " CC" , " clang" ) , ( " LANG" , " C" ) , ( " RUST_LOG" , " debug" ) ] ,
1961
" the organisation's CC wins over the site's"
1962
) ;
1963
1964
1965
let url = format! ( " {} /acme/app" , h.base) ;
1966
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1967
let c = client ( & url, work.path ( ) ) ;
1968
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1969
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1970
c.snapshot ( " test" ) .unwrap ( ) ;
1971
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1972
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1973
let runs = runs_of ( app, " acme" , " app" ) .await;
1974
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
1975
let got = claim ( app) .await.unwrap_or_else ( | | panic! ( " a job: {:?}" , runs[ 0 ] ) ) ;
1976
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " LANG" : " C" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
1977
1978
1979
let ( _ , admin) = page ( " /admin" , None ) .await;
1980
assert! ( admin.contains ( " Variables" ) & & admin.contains ( " <code>gcc</code>" ) , " {admin}" ) ;
1981
let ( _ , body) = page ( " /admin/variables" , Some ( format! ( " _csrf={} &action=delete&name=LANG" , csrf_of ( & admin) ) ) ) .await;
1982
assert! ( body.contains ( " LANG removed." ) , " {body}" ) ;
1983
let got = claim ( app) .await.expect ( " another job" ) ;
1984
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
1985
}
4ac01a73 Andy McCall 1 month ago ↶
1986
1987
# [ tokio ::test ( flavor = " multi_thread" ) ]
1988
async fn secrets_a_job_declares_come_with_the_claim_and_never_reach_the_log ( ) {
1989
let h = hub ( ) .await;
1990
let app = & h.app;
1991
let res = oxygenhub:: router( app.clone ( ) )
1992
.oneshot (
1993
Request:: post( " /register" )
1994
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
1995
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery" ) )
1996
.unwrap ( ) ,
1997
)
1998
.await
1999
.unwrap ( ) ;
2000
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
2001
let page = | path : & str , form : Option< String> | {
2002
let mut req =
2003
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
2004
if form.is_some ( ) {
2005
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
2006
}
2007
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
2008
let app = app.clone ( ) ;
2009
async move {
2010
let res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
2011
let status = res.status ( ) ;
2012
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
2013
( status, body)
2014
}
2015
} ;
2016
let csrf_of = | page : & str | {
2017
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
2018
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
2019
} ;
2020
create ( app, " andy" , " demo" ) .await;
2021
2022
2023
let ( _ , admin) = page ( " /admin" , None ) .await;
2024
let csrf = csrf_of ( & admin) ;
2025
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=deploy-token&value=site-level-value" ) ) ) .await;
2026
assert! ( body.contains ( " deploy-token set." ) & & ! body.contains ( " site-level-value" ) , " {body}" ) ;
2027
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=bad+name&value=x" ) ) ) .await;
2028
assert! ( body.contains ( " name is 1 to 64 of" ) , " {body}" ) ;
2029
let ( _ , settings) = page ( " /andy/demo/settings" , None ) .await;
2030
assert! (
2031
settings.contains ( " Secrets" ) & & settings.contains ( " <code>deploy-token</code>" ) & & settings.contains ( " the site</td>" ) ,
2032
" {settings}"
2033
) ;
2034
let csrf_r = csrf_of ( & settings) ;
2035
let ( _ , body) =
2036
page ( " /andy/demo/settings/secrets" , Some ( format! ( " _csrf={csrf_r} &action=set&name=deploy-token&value=hunter22-repo-value" ) ) ) .await;
2037
assert! ( body.contains ( " deploy-token set." ) & & body.contains ( " overridden" ) & & ! body.contains ( " hunter22" ) , " {body}" ) ;
2038
let repo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2039
assert! (
2040
! String :: from_utf8_lossy( & oxygenhub:: secrets:: list( & app.db, oxygenhub:: levels:: Scope:: Repo( repo.id) ) .await.unwrap ( ) [ 0 ] .value)
2041
.contains ( " hunter22" ) ,
2042
" sealed at rest"
2043
) ;
2044
2045
2046
let url = format! ( " {} /andy/demo" , h.base) ;
2047
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2048
let c = client ( & url, work.path ( ) ) ;
2049
write (
2050
work.path ( ) ,
2051
" .flow/pipeline.kdl" ,
2052
& 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 " ) ,
2053
) ;
2054
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2055
c.snapshot ( " test" ) .unwrap ( ) ;
2056
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2057
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2058
let runs = runs_of ( app, " andy" , " demo" ) .await;
2059
let jobs = match runs.first ( ) {
2060
Some ( r) => db:: flow:: jobs( & app.db, r.id) .await.unwrap ( ) .iter ( ) .map ( | j | format! ( " {} :{} " , j.name, j.status) ) .collect:: < Vec< _ > > ( ) ,
2061
None => vec! [ ] ,
2062
} ;
2063
let mut deploy = None ;
2064
for _ in 0 .. 50 {
2065
let Some ( got) = claim ( app) .await else {
2066
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2067
continue ;
2068
} ;
2069
if got[ " job" ] [ " name" ] = = " deploy" {
2070
deploy = Some ( got) ;
2071
break ;
2072
}
2073
assert_eq! ( got[ " secrets" ] , serde_json:: Value:: Null, " {got}" ) ;
2074
}
2075
let got = deploy.unwrap_or_else ( | | panic! ( " the deploy job: runs {runs:?} jobs {jobs:?}" ) ) ;
2076
assert_eq! (
2077
got[ " secrets" ] ,
2078
serde_json:: json! ( [ { " name" : " deploy-token" , " path" : " /run/secrets/token" , " value" : " hunter22-repo-value" } ] ) ,
2079
" the repository's value, the missing one left out: {got}"
2080
) ;
2081
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2082
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2083
assert! ( log.iter ( ) .any ( | l | l.stream = = " flow" & & l.text.contains ( " secret \" missing\" is not set" ) ) , " {log:?}" ) ;
2084
2085
2086
let records = serde_json:: json! ( { " records" : [
2087
{ " t" : 1 , " stream" : " out" , " text" : " token=hunter22-repo-value" } ,
2088
{ " t" : 2 , " stream" : " err" , " text" : " b64 aHVudGVyMjItcmVwby12YWx1ZQ== end" } ,
2089
] } ) ;
2090
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2091
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2092
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2093
let texts: Vec< & str > = log.iter ( ) .map ( | l | l.text.as_str ( ) ) .collect ( ) ;
2094
assert! ( texts.contains ( & " token=***" ) & & texts.contains ( & " b64 *** end" ) , " {texts:?}" ) ;
2095
assert! ( ! texts.iter ( ) .any ( | t | t.contains ( " hunter22" ) ) , " {texts:?}" ) ;
2096
}
d1727c57 Andy McCall 1 month ago ↶
2097
2098
# [ tokio ::test ( flavor = " multi_thread" ) ]
2099
async fn a_job_s_identity_is_a_signed_token_the_discovery_document_lets_anyone_verify ( ) {
2100
let h = hub ( ) .await;
2101
let app = & h.app;
2102
let ( st, disc) = call ( app, " GET" , " /.well-known/openid-configuration" , None , None ) .await;
2103
assert_eq! ( st, StatusCode:: OK , " {disc}" ) ;
2104
let disc: serde_json:: Value = serde_json:: from_str( & disc) .unwrap ( ) ;
2105
assert_eq! ( disc[ " issuer" ] , h.base.as_str ( ) ) ;
2106
assert_eq! ( disc[ " jwks_uri" ] , format! ( " {} /.well-known/jwks.json" , h.base) ) ;
2107
let ( st, jwks) = call ( app, " GET" , " /.well-known/jwks.json" , None , None ) .await;
2108
assert_eq! ( st, StatusCode:: OK , " {jwks}" ) ;
2109
let jwks: serde_json:: Value = serde_json:: from_str( & jwks) .unwrap ( ) ;
2110
assert_eq! ( jwks[ " keys" ] [ 0 ] [ " alg" ] , " RS256" ) ;
2111
let kid = jwks[ " keys" ] [ 0 ] [ " kid" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2112
2113
create ( app, " andy" , " demo" ) .await;
2114
let url = format! ( " {} /andy/demo" , h.base) ;
2115
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2116
let c = client ( & url, work.path ( ) ) ;
2117
write (
2118
work.path ( ) ,
2119
" .flow/pipeline.kdl" ,
2120
& pipeline ( " job \" deploy\" {\n sandbox \" alpine\" \n identity \" sts.amazonaws.com\" {\n mount-path \" /run/secrets/aws-token\" \n }\n exec \" deploy\" \n }\n " ) ,
2121
) ;
2122
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2123
c.snapshot ( " test" ) .unwrap ( ) ;
2124
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2125
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2126
let mut deploy = None ;
2127
for _ in 0 .. 50 {
2128
let Some ( got) = claim ( app) .await else {
2129
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2130
continue ;
2131
} ;
2132
if got[ " job" ] [ " name" ] = = " deploy" {
2133
deploy = Some ( got) ;
2134
break ;
2135
}
2136
}
2137
let got = deploy.expect ( " the deploy job" ) ;
2138
let secrets = got[ " secrets" ] .as_array ( ) .expect ( " secrets" ) ;
2139
assert_eq! ( secrets.len ( ) , 1 ) ;
2140
assert_eq! (
2141
( secrets[ 0 ] [ " name" ] .as_str ( ) , secrets[ 0 ] [ " path" ] .as_str ( ) ) ,
2142
( Some ( " identity:sts.amazonaws.com" ) , Some ( " /run/secrets/aws-token" ) )
2143
) ;
2144
let token = secrets[ 0 ] [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2145
let issuer = app.oidc ( ) .await.unwrap ( ) ;
2146
assert_eq! ( issuer.kid, kid, " the key the JWKS publishes signed it" ) ;
2147
let claims = issuer.verify ( & token, " sts.amazonaws.com" , db:: now_ms( ) ) .expect ( " verifies" ) ;
2148
assert_eq! ( claims[ " sub" ] , " repo:andy/demo:bookmark:main" ) ;
2149
assert_eq! ( claims[ " repository" ] , " andy/demo" ) ;
2150
assert_eq! ( claims[ " job" ] , " deploy" ) ;
2151
assert_eq! ( claims[ " run_number" ] , 1 ) ;
2152
assert_eq! ( claims[ " runner" ] , " test-1" ) ;
2153
assert_eq! ( claims[ " iss" ] , h.base.as_str ( ) ) ;
2154
assert! ( claims[ " state" ] .as_str ( ) .unwrap ( ) .len ( ) = = 64 ) ;
2155
2156
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2157
let records = serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " out" , " text" : format! ( " TOKEN={token} end" ) } ] } ) ;
2158
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2159
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2160
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2161
assert! ( log.iter ( ) .any ( | l | l.text = = " TOKEN=*** end" ) , " {log:?}" ) ;
2162
}
efd29a00 Andy McCall 1 month ago ↶
2163
2164
# [ tokio ::test ( flavor = " multi_thread" ) ]
2165
async fn jobs_are_offered_only_to_runners_of_their_architecture ( ) {
2166
let h = hub ( ) .await;
2167
let app = & h.app;
2168
create ( app, " andy" , " demo" ) .await;
2169
let url = format! ( " {} /andy/demo" , h.base) ;
2170
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2171
let c = client ( & url, work.path ( ) ) ;
2172
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 ) ) ) ;
2173
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2174
c.snapshot ( " test" ) .unwrap ( ) ;
2175
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2176
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2177
let claim_as = | arch : & 'static str , archs : serde_json:: Value| {
2178
let app = app.clone ( ) ;
2179
async move {
2180
let body = serde_json:: json! ( {
2181
" runner" : format! ( " {arch} -1" ) , " runtimes" : [ " microvm" ] ,
2182
" info" : { " arch" : arch, " os" : " linux" , " version" : " t" , " labels" : [ ] , " max_jobs" : 1 } ,
2183
" image_archs" : archs,
2184
} ) ;
2185
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await;
2186
match st {
2187
StatusCode:: NO_CONTENT => None ,
2188
StatusCode:: OK => {
2189
Some ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
2190
}
2191
other => panic! ( " claim: {other} {body}" ) ,
2192
}
2193
}
2194
} ;
2195
2196
2197
let rootfs = pipeline ( " " ) .split ( " cas://blob/" ) .nth ( 1 ) .unwrap ( ) [ .. 64 ] .to_string ( ) ;
2198
for _ in 0 .. 50 {
2199
if ! runs_of ( app, " andy" , " demo" ) .await.is_empty ( ) {
2200
break ;
2201
}
2202
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2203
}
2204
let runs = runs_of ( app, " andy" , " demo" ) .await;
2205
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
2206
let mut taken = vec! [ ] ;
2207
while let Some ( j) = claim_as ( " x86_64" , serde_json:: json! ( { rootfs.clone ( ) : " x86_64" } ) ) .await {
2208
taken.push ( j) ;
2209
}
2210
assert_eq! ( taken, [ " build" , " lint" ] , " test waits for build; flash is for riscv64" ) ;
2211
2212
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await.as_deref ( ) , Some ( " flash" ) ) ;
2213
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await, None ) ;
2214
let ( _ , queue) = call ( app, " GET" , " /admin/runners" , None , None ) .await;
2215
let _ = queue;
2216
}
4d2e44d7 Andy McCall 1 month ago ↶
2217
2218
2219
2220
2221
2222
# [ tokio ::test ( flavor = " multi_thread" ) ]
2223
async fn a_declined_job_goes_to_another_runner_or_ends_unplaceable ( ) {
2224
let h = hub ( ) .await;
2225
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2226
create ( & h.app, " andy" , " declines" ) .await;
2227
let url = format! ( " {} /andy/declines" , h.base) ;
2228
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2229
let c = client ( & url, work.path ( ) ) ;
2230
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2231
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2232
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2233
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2234
let run = runs_of ( & h.app, " andy" , " declines" ) .await[ 0 ] .clone ( ) ;
2235
let claim_as = | token : & 'static str , name : & 'static str | {
2236
let app = h.app.clone ( ) ;
2237
async move {
2238
let body = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
2239
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( body) ) .await;
2240
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) )
2241
}
2242
} ;
2243
let decline = | token : & 'static str , id : i64 , reason : & 'static str | {
2244
let app = h.app.clone ( ) ;
2245
async move {
2246
let body = serde_json:: json! ( { " reason" : reason } ) .to_string ( ) ;
2247
call ( & app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /decline" ) , Some ( token) , Some ( body) ) .await.0
2248
}
2249
} ;
2250
let job = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " a job" ) ;
2251
2252
assert_eq! ( decline ( " runner-two" , job, " not mine" ) .await, StatusCode:: CONFLICT , " only the runner it runs on may decline it" ) ;
2253
assert_eq! ( decline ( " runner-secret" , job, " the MicroVM tier needs /dev/kvm" ) .await, StatusCode:: NO_CONTENT ) ;
2254
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2255
assert_eq! ( ( j.status.as_str ( ) , j.runner.as_str ( ) ) , ( " queued" , " " ) ) ;
2256
assert_eq! ( j.waiting, " declined by earth-1: the MicroVM tier needs /dev/kvm; waiting for another runner" ) ;
2257
2258
let next = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " the other ready job" ) ;
2259
assert_ne! ( next, job) ;
2260
let page = call ( & h.app, " GET" , & format! ( " /andy/declines/flow/{} " , run.number) , None , None ) .await.1 ;
2261
assert! ( page.contains ( " Waiting: declined by earth-1: the MicroVM tier needs /dev/kvm" ) , " {page}" ) ;
2262
assert_eq! ( claim_as ( " runner-two" , " plex-1" ) .await, Some ( job) , " another runner takes it" ) ;
2263
assert_eq! ( decline ( " runner-two" , job, " rootfs 36fc4e323804 is not on this runner" ) .await, StatusCode:: NO_CONTENT ) ;
2264
assert_eq! ( claim_as ( " runner-three" , " narvi" ) .await, Some ( job) ) ;
2265
assert_eq! ( decline ( " runner-three" , job, " rootfs 36fc4e323804 is for x86_64; this runner is riscv64" ) .await, StatusCode:: NO_CONTENT ) ;
2266
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2267
assert_eq! ( j.status, " error" ) ;
2268
assert_eq! (
2269
j.error,
2270
" 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"
2271
) ;
2272
let r = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex-1" ) .unwrap ( ) ;
2273
let declined = db:: flow:: declines_of_runner( & h.app.db, r.id, 10 ) .await.unwrap ( ) ;
2274
assert_eq! ( declined.len ( ) , 1 ) ;
2275
assert_eq! ( ( declined[ 0 ] .job_id, declined[ 0 ] .reason.as_str ( ) ) , ( job, " rootfs 36fc4e323804 is not on this runner" ) ) ;
2276
assert_eq! ( r.tier_list ( ) , vec! [ " microvm" ] ) ;
2277
}
554ccacf Andy McCall 1 month ago ↶
2278
2279
2280
2281
2282
# [ tokio ::test ( flavor = " multi_thread" ) ]
2283
async fn a_coverage_artifact_becomes_a_coverage_report ( ) {
2284
let h = hub ( ) .await;
2285
create ( & h.app, " andy" , " cov" ) .await;
2286
let url = format! ( " {} /andy/cov" , h.base) ;
2287
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2288
let c = client ( & url, work.path ( ) ) ;
2289
write (
2290
work.path ( ) ,
2291
" .flow/pipeline.kdl" ,
2292
& format! (
2293
" 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 "
2294
) ,
2295
) ;
2296
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 " ) ;
2297
c.snapshot ( " test" ) .unwrap ( ) ;
2298
c.commit ( " Code and coverage" , & [ ] ) .unwrap ( ) ;
2299
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2300
let run = runs_of ( & h.app, " andy" , " cov" ) .await.remove ( 0 ) ;
2301
assert! ( ! call ( & h.app, " GET" , " /andy/cov" , None , None ) .await.1. contains ( " >Coverage</a>" ) , " no tab before a report" ) ;
2302
let id = claim ( & h.app) .await.unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2303
2304
let ( st, text) = upload ( & h.app, id, " coverage" , b " this is not a report" ) .await;
2305
assert_eq! ( st, StatusCode:: OK , " a bad report never fails the upload: {text}" ) ;
2306
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 \
2307
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 " ;
2308
let ( st, text) = upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await;
2309
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
2310
let log: Vec< String> = db:: flow:: log( & h.app.db, id, 0 , 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.text ) .collect ( ) ;
2311
assert! ( log.iter ( ) .any ( | l | l.contains ( " coverage: this is not an LCOV report" ) ) , " {log:?}" ) ;
2312
assert! (
2313
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)" ) ,
2314
" {log:?}"
2315
) ;
2316
2317
let ( st, page) = call ( & h.app, " GET" , " /andy/cov/coverage" , None , None ) .await;
2318
assert_eq! ( st, StatusCode:: OK ) ;
c987c865 Andy McCall 1 month ago ↶
2319
assert! (
2320
page.contains ( " >Coverage</a>" )
2321
& & page.contains ( " 50.0%" )
2322
& & page.contains ( " <strong>2</strong> of <strong>4</strong> relevant lines covered" ) ,
2323
" {page}"
2324
) ;
554ccacf Andy McCall 1 month ago ↶
2325
assert! ( page.contains ( " 1 of 2" ) & & page.contains ( " /andy/cov/coverage/" ) & & page.contains ( " /file/src/lib.rs" ) , " {page}" ) ;
2326
let report = crate_report ( & h.app, run.id) .await;
2327
let ( st, file) = call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/lib.rs" ) , None , None ) .await;
2328
assert_eq! ( st, StatusCode:: OK , " {file}" ) ;
2329
assert_eq! ( file.matches ( " class=\" cov-hit\" " ) .count ( ) , 2 , " {file}" ) ;
2330
assert_eq! ( file.matches ( " class=\" cov-miss\" " ) .count ( ) , 2 , " {file}" ) ;
2331
assert! ( file.contains ( " 3×" ) , " {file}" ) ;
2332
assert_eq! ( call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/gone.rs" ) , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
2333
let ( _ , badge) = call ( & h.app, " GET" , " /andy/cov/coverage/badge.svg" , None , None ) .await;
2334
assert! ( badge.contains ( " coverage: 50.0%" ) , " {badge}" ) ;
2335
let ( _ , run_page) = call ( & h.app, " GET" , & format! ( " /andy/cov/flow/{} " , run.number) , None , None ) .await;
2336
assert! ( run_page.contains ( " coverage 50.0%" ) , " {run_page}" ) ;
617c2840 Andy McCall 1 month ago ↶
2337
2338
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/cov/coverage" , None , None ) .await;
2339
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
2340
assert_eq! ( ( list[ 0 ] [ " percent" ] .as_f64 ( ) , list[ 0 ] [ " format" ] .as_str ( ) ) , ( Some ( 50. 0 ) , Some ( " lcov" ) ) , " {list}" ) ;
2341
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/cov/coverage/{report} " ) , None , None ) .await;
2342
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
2343
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 ↶
2344
2345
2346
2347
let row = db:: repos:: get( & h.app.db, " andy" , " cov" ) .await.unwrap ( ) .unwrap ( ) ;
2348
let check = | checks : Vec< db:: hooks:: Check> | checks.into_iter ( ) .find ( | c | c.name = = " coverage" ) .expect ( " a coverage check" ) ;
2349
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
2350
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " passed" , " 50.0%" ) ) ;
2351
assert! ( c.url.ends_with ( & format! ( " /andy/cov/coverage?report={report} " ) ) , " {}" , c.url) ;
2352
oxygenhub:: coverage:: set_rules( & h.app.db, row.id, oxygenhub:: coverage:: Rules { min: Some ( 60. 0 ) , max_drop: None } ) .await.unwrap ( ) ;
2353
assert_eq! ( upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
2354
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
2355
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " failed" , " 50.0%: below the minimum of 60%" ) ) ;
554ccacf Andy McCall 1 month ago ↶
2356
}
2357
2358
async fn crate_report ( app : & Shared, run_id : i64 ) -> i64 {
2359
oxygenhub:: coverage:: for_run( & app.db, run_id) .await.unwrap ( ) [ 0 ] .id
2360
}
36e5a9c1 Andy McCall 1 month ago ↶
2361
2362
2363
2364
# [ tokio ::test ( flavor = " multi_thread" ) ]
2365
async fn junit_reports_make_a_tests_tab ( ) {
2366
let h = hub ( ) .await;
2367
create ( & h.app, " andy" , " t" ) .await;
2368
let url = format! ( " {} /andy/t" , h.base) ;
2369
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2370
let c = client ( & url, work.path ( ) ) ;
2371
write (
2372
work.path ( ) ,
2373
" .flow/pipeline.kdl" ,
2374
& format! (
2375
" 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 "
2376
) ,
2377
) ;
2378
assert! ( ! call ( & h.app, " GET" , " /andy/t" , None , None ) .await.1. contains ( " >Tests</a>" ) ) ;
2379
let case = | name : & str , ok : bool , secs : & str | {
2380
if ok {
2381
format! ( r # "<testcase classname="t.suite" name="{name} " time="{secs} "/>"# )
2382
} else {
2383
format! (
2384
r # "<testcase classname="t.suite" name="{name} " time="{secs} "><failure message="{name} broke">AssertionError</failure></testcase>"#
2385
)
2386
}
2387
} ;
2388
let runs = [
2389
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.5" ) , case ( " c" , true , " 0.2" ) ] ,
2390
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , false , " 2.6" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
2391
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.4" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
2392
] ;
2393
for ( i, cases) in runs.iter ( ) .enumerate ( ) {
2394
write ( work.path ( ) , " n.txt" , & i.to_string ( ) ) ;
2395
c.snapshot ( " test" ) .unwrap ( ) ;
2396
c.commit ( & format! ( " Run {i} " ) , & [ ] ) .unwrap ( ) ;
2397
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2398
2399
let mut a = None ;
2400
for _ in 0 .. 100 {
2401
a = claim ( & h.app) .await;
2402
if a.is_some ( ) {
2403
break ;
2404
}
2405
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2406
}
2407
let a = a.expect ( " a job to claim" ) ;
2408
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2409
let xml = format! ( " <testsuites><testsuite name=\" t\" >{} </testsuite></testsuites>" , cases.concat ( ) ) ;
2410
assert_eq! ( upload ( & h.app, id, " junit" , xml.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
2411
let failed = cases.iter ( ) .any ( | c | c.contains ( " <failure" ) ) ;
2412
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , failed as u32 ) ;
2413
if ! failed {
2414
r[ " outputs" ] = serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( xml.as_bytes ( ) ) .to_hex ( ) ] ) ;
2415
}
2416
let code = failed as i32 ;
2417
assert_eq! (
2418
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : code, " duration_ms" : 700 , " receipt" : r } ) ) .await,
2419
StatusCode:: NO_CONTENT
2420
) ;
2421
}
2422
let ( st, page) = call ( & h.app, " GET" , " /andy/t/tests" , None , None ) .await;
2423
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
2424
if let Some ( out) = std:: env:: var_os( " OXYHUB_DUMP_TESTS_PAGE" ) {
2425
std:: fs:: write( out, & page) .unwrap ( ) ;
2426
}
2427
assert! ( page.contains ( " >Tests</a>" ) , " the tab" ) ;
2428
assert! ( page.contains ( " 100%" ) & & page.contains ( " of 4 tests passed" ) , " the latest run passes" ) ;
2429
assert! ( page.contains ( " 1 fixed" ) & & page.contains ( " t.suite::b" ) , " b fixed against the run before" ) ;
2430
assert! ( page.contains ( " 1 ✕" ) & & page.contains ( " 2 ✓" ) , " b is flaky: {page}" ) ;
2431
assert! ( page.contains ( " <svg class=\" cov-chart\" " ) , " charts" ) ;
2432
2433
let slow = page.find ( " Slowest tests" ) .unwrap ( ) ;
2434
assert! ( page[ slow.. ] .find ( " t.suite::b" ) .unwrap ( ) < page[ slow.. ] .find ( " t.suite::d" ) .unwrap ( ) ) ;
2435
2436
let row = db:: repos:: get( & h.app.db, " andy" , " t" ) .await.unwrap ( ) .unwrap ( ) ;
2437
let second = oxygenhub:: junit:: on_bookmark( & h.app.db, row.id, " main" , 10 ) .await.unwrap ( ) [ 1 ] .id;
2438
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/t/tests?report={second} " ) , None , None ) .await;
2439
assert! ( page.contains ( " 1 failing" ) & & page.contains ( " Newly failing" ) & & page.contains ( " b broke" ) , " {page}" ) ;
2440
assert! ( page.contains ( " 1 new tests" ) , " d is new" ) ;
617c2840 Andy McCall 1 month ago ↶
2441
2442
2443
2444
let ( st, list) = call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests" , None , None ) .await;
2445
assert_eq! ( st, StatusCode:: OK , " {list}" ) ;
2446
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
2447
assert_eq! ( list.as_array ( ) .unwrap ( ) .len ( ) , 3 ) ;
2448
assert_eq! ( ( list[ 0 ] [ " tests" ] .as_i64 ( ) , list[ 1 ] [ " failed" ] .as_i64 ( ) ) , ( Some ( 4 ) , Some ( 1 ) ) ) ;
2449
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} ?status=failed" ) , None , None ) .await;
2450
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
2451
assert_eq! ( one[ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 1 ) ;
2452
assert_eq! ( ( one[ " cases" ] [ 0 ] [ " name" ] .as_str ( ) , one[ " cases" ] [ 0 ] [ " message" ] .as_str ( ) ) , ( Some ( " b" ) , Some ( " b broke" ) ) ) ;
2453
let ( _ , all) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} " ) , None , None ) .await;
2454
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & all) .unwrap ( ) [ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 4 ) ;
2455
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests/99999" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
36e5a9c1 Andy McCall 1 month ago ↶
2456
}