@ 261105.4 · fetching
current
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

import { getPRWorkload } from '../../lib/ci/workload.js';

const queueURL = 'https://ci.nodejs.org/queue/api/json?tree=items[id,task[url]]';
const computerURL = 'https://ci.nodejs.org/computer/api/json?' +
  'tree=computer[executors[currentExecutable[url,queueId]],' +
  'oneOffExecutors[currentExecutable[url,queueId]]]';
const prJobURL = 'https://ci.nodejs.org/job/node-test-pull-request/';
const jsonResponse = data => new Response(JSON.stringify(data), {
  headers: { 'content-type': 'application/json' }
});
const requestFor = (data, computers = { computer: [] }) => ({
  fetch: async url => jsonResponse(url === computerURL ? computers : data)
});
const executable = (number, queueId = number) => ({
  currentExecutable: { url: `${prJobURL}${number}/`, queueId }
});

describe('PR CI workload', () => {
  it('counts only the canonical PR job, including repeated queued requests', async() => {
    const items = [
      { id: 1, task: { url: prJobURL } },
      { id: 2, task: { url: prJobURL } },
      { id: 3, task: { url: prJobURL.slice(0, -1) } },
      { task: { url: 'https://ci.nodejs.org/job/node-test-commit/' } },
      { task: { url: 'https://ci.nodejs.org/job/node-test-pull-request-other/' } },
      { task: { url: 'https://ci.nodejs.org/job/folder/job/node-test-pull-request/' } },
      { task: { url: `${prJobURL}123/` } },
      { task: { url: `${prJobURL}?other=job` } },
      { task: { url: 'https://example.com/job/node-test-pull-request/' } }
    ];
    assert.equal(await getPRWorkload(requestFor({ items })), 3);
  });

  it('returns zero when idle using two GETs with a shared deadline', async(t) => {
    const signal = new AbortController().signal;
    const timeout = t.mock.method(AbortSignal, 'timeout', () => signal);
    const fetch = t.mock.fn(async(url, options) => {
      assert.ok([queueURL, computerURL].includes(url));
      assert.equal(options.method, 'GET');
      assert.equal(options.redirect, 'error');
      assert.equal(options.signal, signal);
      return jsonResponse(url === queueURL ? { items: [] } : { computer: [] });
    });
    assert.equal(await getPRWorkload({ fetch }), 0);
    assert.deepEqual(fetch.mock.calls.map(call => call.arguments[0]), [queueURL, computerURL]);
    assert.equal(timeout.mock.callCount(), 1);
    assert.deepEqual(timeout.mock.calls[0].arguments, [20_000]);
  });

  it('ignores unrelated task types that do not export a URL', async() => {
    const items = [
      { task: {} },
      { task: { _class: 'example.CustomTask', url: null } },
      { id: 1, task: { url: prJobURL } }
    ];
    assert.equal(await getPRWorkload(requestFor({ items })), 1);
  });

  it('counts active PR builds when the waiting queue has no PR jobs', async() => {
    const computers = {
      computer: [{
        executors: Array.from({ length: 20 }, (_, index) => executable(77871 + index)),
        oneOffExecutors: []
      }]
    };
    assert.equal(await getPRWorkload(requestFor({ items: [] }, computers)), 20);
  });

  it('combines queued requests with regular and one-off executors without duplicates', async() => {
    const items = [
      { id: 1, task: { url: prJobURL } },
      { id: 2, task: { url: prJobURL } }
    ];
    const computers = {
      computer: [{
        executors: [
          executable(123, 1),
          executable(124, 3),
          { currentExecutable: null },
          { currentExecutable: { url: 'https://ci.nodejs.org/job/node-test-commit/123/' } }
        ],
        oneOffExecutors: [
          executable(124, 3),
          // An older active run must count even when newer builds have finished.
          executable(1, -1),
          { currentExecutable: { url: `${prJobURL}123`, queueId: 1 } },
          { currentExecutable: {} }
        ]
      }]
    };
    assert.equal(await getPRWorkload(requestFor({ items }, computers)), 4);
  });

  it('ignores other job and child URLs on executors', async() => {
    const urls = [
      'https://example.com/job/node-test-pull-request/123/',
      'https://ci.nodejs.org/job/folder/job/node-test-pull-request/123/',
      'https://ci.nodejs.org/job/node-test-pull-request-other/123/',
      `${prJobURL}123/child/`, `${prJobURL}123?other=job`, prJobURL
    ];
    const computers = {
      computer: [{
        executors: urls.map(url => ({ currentExecutable: { url, queueId: 1 } })),
        oneOffExecutors: []
      }]
    };
    assert.equal(await getPRWorkload(requestFor({ items: [] }, computers)), 0);
  });

  for (const id of [undefined, null, -1, '123', 0.5]) {
    it(`rejects malformed PR queue IDs: ${id}`, async() => {
      await assert.rejects(getPRWorkload(requestFor({
        items: [{ id, task: { url: prJobURL } }]
      })), /invalid queue item ID/);
    });
  }

  for (const computers of [
    null, {}, { computer: null }, { computer: {} },
    { computer: [null] }, { computer: [{}] },
    { computer: [{ executors: [], oneOffExecutors: null }] }
  ]) {
    it(`rejects malformed executor data: ${JSON.stringify(computers)}`, async() => {
      await assert.rejects(getPRWorkload(requestFor({ items: [] }, computers)),
        /invalid executor/);
    });
  }

  for (const executor of [
    null, [], { currentExecutable: false }, { currentExecutable: [] },
    { currentExecutable: { url: 123 } },
    { currentExecutable: { url: `${prJobURL}123/` } }
  ]) {
    it(`rejects a malformed executable: ${JSON.stringify(executor)}`, async() => {
      await assert.rejects(getPRWorkload(requestFor({ items: [] }, {
        computer: [{ executors: [executor], oneOffExecutors: [] }]
      })), /invalid exec/);
    });
  }

  it('does not return a partial queue count if the executor query fails', async() => {
    const error = new Error('Connection reset');
    const request = {
      async fetch(url) {
        if (url === computerURL) throw error;
        return jsonResponse({ items: [{ id: 1, task: { url: prJobURL } }] });
      }
    };
    await assert.rejects(getPRWorkload(request), error);
  });

  for (const data of [null, {}, [], { items: null }, { items: {} }]) {
    it(`rejects malformed queue data: ${JSON.stringify(data)}`, async() => {
      await assert.rejects(getPRWorkload(requestFor(data)), /invalid queue/);
    });
  }

  for (const item of [null, false, [], {}, { task: null }, { task: 'job' }, { task: [] }]) {
    it(`rejects a malformed queue item: ${JSON.stringify(item)}`, async() => {
      await assert.rejects(getPRWorkload(requestFor({ items: [item] })), /invalid queue item/);
    });
  }

  it('rejects a malformed task URL instead of reporting an empty queue', async() => {
    await assert.rejects(getPRWorkload(requestFor({ items: [{ task: { url: 123 } }] })),
      /invalid queue task URL/);
  });

  it('propagates HTTP errors without returning a queue count', async() => {
    const request = {
      fetch: async() => new Response('Forbidden', { status: 403, statusText: 'Forbidden' })
    };
    await assert.rejects(getPRWorkload(request), /403/);
  });

  it('propagates network failures', async() => {
    const error = new Error('Connection reset');
    const request = { async fetch() { throw error; } };
    await assert.rejects(getPRWorkload(request), error);
  });

  it('propagates invalid JSON errors', async() => {
    const request = { fetch: async() => new Response('<html>Not JSON</html>') };
    await assert.rejects(getPRWorkload(request), error => {
      assert.equal(error.message, 'Jenkins returned invalid JSON');
      assert.ok(error.cause instanceof SyntaxError);
      return true;
    });
  });
});