@@ -9,12 +9,28 @@ import { fileURLToPath } from 'node:url';
99const binaryURL = new URL ( '../../bin/ncu-ci.js' , import . meta. url ) ;
1010const requestURL = new URL ( '../../lib/request.js' , import . meta. url ) ;
1111const undiciURL = import . meta. resolve ( 'undici' ) ;
12+ const jobURL = 'https://ci.nodejs.org/job/node-test-pull-request/' ;
1213const root = ( data , extra = { } ) => ( {
1314 path : '/api/json' , tree : 'quietingDown' , data, ...extra
1415} ) ;
1516const job = ( data , extra = { } ) => ( {
1617 path : '/job/node-test-pull-request/api/json' , tree : 'disabled,buildable' , data, ...extra
1718} ) ;
19+ const queue = ( data , extra = { } ) => ( {
20+ path : '/queue/api/json' , tree : 'items[id,task[url]]' , data, ...extra
21+ } ) ;
22+ const computers = ( data , extra = { } ) => ( {
23+ path : '/computer/api/json' ,
24+ tree : 'computer[executors[currentExecutable[url,queueId]],' +
25+ 'oneOffExecutors[currentExecutable[url,queueId]]]' ,
26+ data,
27+ ...extra
28+ } ) ;
29+ const idleComputers = ( ) => computers ( { computer : [ ] } ) ;
30+ const running = ( number , queueId = number ) => ( {
31+ currentExecutable : { url : `${ jobURL } ${ number } /` , queueId }
32+ } ) ;
33+
1834function run ( t , command , responses , credentials = {
1935 username : 'test' , jenkins_token : 'test-jenkins-token'
2036} ) {
@@ -169,3 +185,132 @@ describe('ncu-ci available', () => {
169185 assert . doesNotMatch ( result . stdout + result . stderr , / s e c r e t - i n v a l i d - t o k e n / ) ;
170186 } ) ;
171187} ) ;
188+
189+ describe ( 'ncu-ci workload' , ( ) => {
190+ it ( 'prints only the number of unfinished and queued PR jobs' , ( t ) => {
191+ const result = run ( t , 'workload' , [ queue ( {
192+ items : [
193+ { id : 1 , task : { url : jobURL } } ,
194+ { task : { url : 'https://ci.nodejs.org/job/node-test-commit/' } } ,
195+ { task : { url : `${ jobURL } 123/` } } ,
196+ { task : { url : 'https://example.org/job/node-test-pull-request/' } } ,
197+ { task : { url : 'https://ci.nodejs.org/job/node-test-pull-request-other/' } } ,
198+ { id : 2 , task : { url : jobURL . slice ( 0 , - 1 ) } } ,
199+ { task : { } } ,
200+ { task : { url : null } } ,
201+ { id : 3 , task : { url : jobURL } }
202+ ]
203+ } ) , idleComputers ( ) ] ) ;
204+ assert . equal ( result . status , 0 , result . stderr ) ;
205+ assert . equal ( result . stdout , '3\n' ) ;
206+ assert . equal ( result . stderr , '' ) ;
207+ } ) ;
208+
209+ it ( 'includes all 20 working PR builds when the waiting queue is empty' , ( t ) => {
210+ const result = run ( t , 'workload' , [ queue ( { items : [ ] } ) , computers ( {
211+ computer : [ {
212+ executors : Array . from ( { length : 3 } , ( _ , i ) => running ( i + 1 ) ) ,
213+ oneOffExecutors : Array . from ( { length : 17 } , ( _ , i ) => running ( i + 4 ) )
214+ } ]
215+ } ) ] ) ;
216+ assert . equal ( result . status , 0 , result . stderr ) ;
217+ assert . equal ( result . stdout , '20\n' ) ;
218+ assert . equal ( result . stderr , '' ) ;
219+ assert . equal ( result . trace . jsonReads , 2 ) ;
220+ } ) ;
221+
222+ it ( 'combines queued and working PR builds without double counting transitions' , ( t ) => {
223+ const result = run ( t , 'workload' , [ queue ( {
224+ items : [
225+ { id : 10 , task : { url : jobURL } } ,
226+ { id : 11 , task : { url : jobURL } } ,
227+ { id : 12 , task : { url : jobURL } }
228+ ]
229+ } ) , computers ( {
230+ computer : [ {
231+ executors : [ running ( 101 , 10 ) , running ( 102 , 20 ) , { currentExecutable : null } , { } ] ,
232+ oneOffExecutors : [
233+ running ( 102 , 20 ) ,
234+ { currentExecutable : { url : `${ jobURL } 102` , queueId : 20 } } ,
235+ { currentExecutable : { url : 'https://ci.nodejs.org/job/node-test-commit/1/' } } ,
236+ { currentExecutable : { url : 'https://example.org/job/node-test-pull-request/1/' } } ,
237+ { currentExecutable : { } }
238+ ]
239+ } ]
240+ } ) ] ) ;
241+ assert . equal ( result . status , 0 , result . stderr ) ;
242+ assert . equal ( result . stdout , '4\n' ) ;
243+ assert . equal ( result . stderr , '' ) ;
244+ } ) ;
245+
246+ for ( const items of [ [ ] , [ { task : { url : 'https://ci.nodejs.org/job/node-test-commit/' } } ] ] ) {
247+ it ( `prints zero for a valid queue with no PR jobs: ${ JSON . stringify ( items ) } ` , ( t ) => {
248+ const result = run ( t , 'workload' , [ queue ( { items } ) , idleComputers ( ) ] ) ;
249+ assert . equal ( result . status , 0 , result . stderr ) ;
250+ assert . equal ( result . stdout , '0\n' ) ;
251+ assert . equal ( result . stderr , '' ) ;
252+ } ) ;
253+ }
254+
255+ for ( const data of [ null , { } , { items : null } , { items : { } } ] ) {
256+ it ( `fails without a misleading count for invalid queue data: ${ JSON . stringify ( data ) } ` , ( t ) => {
257+ assertFailure ( run ( t , 'workload' , [ queue ( data ) ] ) ) ;
258+ } ) ;
259+ }
260+
261+ for ( const item of [ null , { } , { task : null } , { task : false } , { task : { url : 123 } } ] ) {
262+ it ( `fails for a malformed queue item: ${ JSON . stringify ( item ) } ` , ( t ) => {
263+ assertFailure ( run ( t , 'workload' , [ queue ( { items : [ item ] } ) ] ) ) ;
264+ } ) ;
265+ }
266+
267+ for ( const id of [ undefined , null , - 1 , 1.5 , '1' , Number . MAX_SAFE_INTEGER + 1 ] ) {
268+ it ( `rejects a queued PR item with invalid ID: ${ JSON . stringify ( id ) } ` , ( t ) => {
269+ assertFailure ( run ( t , 'workload' , [ queue ( { items : [ { id, task : { url : jobURL } } ] } ) ] ) ) ;
270+ } ) ;
271+ }
272+
273+ for ( const data of [
274+ null ,
275+ { } ,
276+ { computer : null } ,
277+ { computer : { } } ,
278+ { computer : [ { executors : { } , oneOffExecutors : [ ] } ] } ,
279+ { computer : [ { executors : [ ] , oneOffExecutors : null } ] } ,
280+ { computer : [ { executors : [ { currentExecutable : false } ] , oneOffExecutors : [ ] } ] } ,
281+ { computer : [ { executors : [ ] , oneOffExecutors : [ { currentExecutable : { url : 1 } } ] } ] }
282+ ] ) {
283+ it ( `fails without a partial count for invalid executor data: ${ JSON . stringify ( data ) } ` , ( t ) => {
284+ assertFailure ( run ( t , 'workload' , [
285+ queue ( { items : [ { id : 1 , task : { url : jobURL } } ] } ) , computers ( data )
286+ ] ) ) ;
287+ } ) ;
288+ }
289+
290+ it ( 'reports executor API failure without printing the already counted queue' , ( t ) => {
291+ const result = run ( t , 'workload' , [
292+ queue ( { items : [ { id : 1 , task : { url : jobURL } } ] } ) ,
293+ computers ( null , { status : 503 , statusText : 'Service Unavailable' } )
294+ ] ) ;
295+ assertFailure ( result , / 5 0 3 / ) ;
296+ assert . equal ( result . trace . jsonReads , 1 ) ;
297+ assert . equal ( result . trace . cancellations , 1 ) ;
298+ } ) ;
299+
300+ it ( 'reports HTTP failures instead of printing zero' , ( t ) => {
301+ const result = run ( t , 'workload' , [ queue ( null , { status : 403 , statusText : 'Forbidden' } ) ] ) ;
302+ assertFailure ( result , / 4 0 3 / ) ;
303+ assert . equal ( result . trace . jsonReads , 0 ) ;
304+ assert . equal ( result . trace . cancellations , 1 ) ;
305+ } ) ;
306+
307+ it ( 'reports malformed JSON instead of printing zero' , ( t ) => {
308+ assertFailure ( run ( t , 'workload' , [ queue ( null , { jsonError : 'Invalid queue JSON' } ) ] ) ,
309+ / J e n k i n s r e t u r n e d i n v a l i d J S O N / ) ;
310+ } ) ;
311+
312+ it ( 'reports a network failure instead of printing zero' , ( t ) => {
313+ assertFailure ( run ( t , 'workload' , [ queue ( null , { error : 'Connection reset' } ) ] ) ,
314+ / C o n n e c t i o n r e s e t / ) ;
315+ } ) ;
316+ } ) ;
0 commit comments