Skip to content
18 changes: 18 additions & 0 deletions QualityControl/common/library/typedef/DetectorSummary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

/**
* @typedef {object} DetectorSummary
* @property {string} name - Human-readable detector name.
* @property {string} type - Detector type identifier.
*/
15 changes: 7 additions & 8 deletions QualityControl/lib/controllers/FilterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,16 @@ export class FilterController {

/**
* HTTP GET endpoint for retrieving a list of run types from Bookkeeping
* @param {Request} req - HTTP request
* @param {Request} _ - HTTP request
* @param {Response} res - HTTP response to provide run types information
*/
async getFilterConfigurationHandler(req, res) {
getFilterConfigurationHandler(_, res) {
try {
let runTypes = [];
if (this._filterService) {
runTypes = await this._filterService.runTypes;
}
const runTypes = this._filterService?.runTypes ?? [];
const detectors = this._filterService?.detectors ?? [];
res.status(200).json({
runTypes,
detectors,
});
} catch (error) {
res.status(503).json({ error: error.message || error });
Expand All @@ -77,10 +76,10 @@ export class FilterController {

/**
* HTTP GET endpoint for retrieving a list of ongoing runs from Runs Mode Service
* @param {Request} req HTTP Request
* @param {Request} _ HTTP Request
* @param {Response} res HTTP Response with the ongoing runs
*/
getOngoingRunsHandler(req, res) {
getOngoingRunsHandler(_, res) {
const ongoingRuns = this._runsModeService?.ongoingRuns ?? [];
res.status(200).json({ ongoingRuns });
}
Expand Down
18 changes: 18 additions & 0 deletions QualityControl/lib/services/BookkeepingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { wrapRunStatus } from '../dtos/BookkeepingDto.js';
const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database';
const GET_RUN_TYPES_PATH = '/api/runTypes';
const GET_RUN_PATH = '/api/runs';
export const GET_DETECTORS_PATH = '/api/detectors';

const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`;

Expand Down Expand Up @@ -181,6 +182,23 @@ export class BookkeepingService {
}
}

/**
* Retrieves the information about the detectors from the Bookkeeping service.
* @returns {Promise<object[]>} Array of detector summaries.
*/
async retrieveDetectorSummaries() {
const { data } = await httpGetJson(
this._hostname,
this._port,
this._createPath(GET_DETECTORS_PATH),
{
protocol: this._protocol,
rejectUnauthorized: false,
},
);
return Array.isArray(data) ? data : [];
}

/**
* Helper method to construct a URL path with the required authentication token.
* Appends the service's token as a query parameter to the provided path.
Expand Down
27 changes: 26 additions & 1 deletion QualityControl/lib/services/FilterService.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class FilterService {
this._logger = LogManager.getLogger(LOG_FACILITY);
this._bookkeepingService = bookkeepingService;
this._runTypes = [];
this.initFilters();
this._detectors = Object.freeze([]);

this._runTypesRefreshInterval = config?.bookkeeping?.runTypesRefreshInterval ??
(config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);
Expand All @@ -48,6 +48,7 @@ export class FilterService {
async initFilters() {
await this._bookkeepingService.connect();
await this.getRunTypes();
await this._initializeDetectors();
}

/**
Expand All @@ -71,6 +72,30 @@ export class FilterService {
}
}

/**
* This method is used to retrieve the list of detectors from the bookkeeping service
* @returns {Promise<undefined>} Resolves when the list of detectors is available
*/
async _initializeDetectors() {
if (!this._bookkeepingService?.active) {
return;
}
try {
const detectorSummaries = await this._bookkeepingService.retrieveDetectorSummaries();
this._detectors = Object.freeze(detectorSummaries.map(({ name, type }) => Object.freeze({ name, type })));
} catch (error) {
this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`);
}
}

/**
* Returns a list of detector summaries.
* @returns {Readonly<DetectorSummary[]>} An immutable array of detector summaries.
*/
get detectors() {
return this._detectors;
}

/**
* Returns the interval in milliseconds for how often the list of run types should be refreshed.
* @returns {number} Interval in milliseconds for refreshing the list of run types.
Expand Down
26 changes: 18 additions & 8 deletions QualityControl/test/lib/controllers/FiltersController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,35 +37,45 @@ export const filtersControllerTestSuite = async () => {
});
});

suite('getFilterConfigurationHandler', async () => {
test('should successfully retrieve run types from Bookkeeping service', async () => {
suite('getFilterConfigurationHandler', () => {
test('should successfully retrieve run types and detectors from Bookkeeping service', async () => {
const filterService = sinon.createStubInstance(FilterService);
const mockedRunTypes = ['runType1', 'runType2'];
const mockedDetectors = [
{
name: 'ITS',
type: 'PHYSICAL',
},
];
sinon.stub(filterService, 'runTypes').get(() => mockedRunTypes);
sinon.stub(filterService, 'detectors').get(() => mockedDetectors);

const res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
const req = {};
const filterController = new FilterController(filterService);
await filterController.getFilterConfigurationHandler(req, res);
filterController.getFilterConfigurationHandler(req, res);
ok(res.status.calledWith(200), 'Response status was not 200');
ok(res.json.calledWith({ runTypes: mockedRunTypes }), 'Run types were not sent back');
ok(
res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors }),
'Response should include runTypes and detectors',
);
});
test('should return an empty array if bookkeeping service is not defined', async () => {
test('should return an empty arrays if bookkeeping service is not defined', () => {
const bkpService = null;
const res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
const req = {};
const filterController = new FilterController(bkpService);
await filterController.getFilterConfigurationHandler(req, res);
filterController.getFilterConfigurationHandler(req, res);
ok(res.status.calledWith(200), 'Response status was not 200');
ok(
res.json.calledWith({ runTypes: [] }),
'Run types were not sent as an empty array',
res.json.calledWith({ runTypes: [], detectors: [] }),
'runTypes and detectors were not sent as an empty array',
);
});
});
Expand Down
76 changes: 75 additions & 1 deletion QualityControl/test/lib/services/BookkeepingService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { suite, test, before, beforeEach, afterEach } from 'node:test';
import nock from 'nock';
import { stub, restore } from 'sinon';

import { BookkeepingService } from '../../../lib/services/BookkeepingService.js';
import { BookkeepingService, GET_DETECTORS_PATH } from '../../../lib/services/BookkeepingService.js';
import { RunStatus } from '../../../common/library/runStatus.enum.js';

/**
Expand All @@ -34,6 +34,7 @@ export const bookkeepingServiceTestSuite = async () => {
},
};
before(() => nock.cleanAll());

suite('Create a new instance of BookkeepingService', () => {
test('should successfully initialize Bookkeeping Service', () => {
const bookkeepingService = new BookkeepingService(VALID_CONFIG.bookkeeping);
Expand Down Expand Up @@ -91,6 +92,7 @@ export const bookkeepingServiceTestSuite = async () => {
strictEqual(service._token, 'my-token');
});
});

suite('connect', () => {
let service = null;
let validConfig = null;
Expand Down Expand Up @@ -136,6 +138,7 @@ export const bookkeepingServiceTestSuite = async () => {
ok(service.error.includes('simulated failure'));
});
});

suite('simulateConnection', () => {
let service = null;

Expand Down Expand Up @@ -332,5 +335,76 @@ export const bookkeepingServiceTestSuite = async () => {
strictEqual(runStatus, RunStatus.BOOKKEEPING_UNAVAILABLE);
});
});
suite('Retrieve detector summaries', () => {
let bkpService = null;

before(() => {
bkpService = new BookkeepingService(VALID_CONFIG.bookkeeping);
bkpService.validateConfig(); // ensures internal fields like _hostname/_port/_token are set
bkpService.connect();
});

afterEach(() => {
nock.cleanAll();
});

test('should handle all detector types correctly', async () => {
const mockResponse = {
data: [
{ id: 1, name: 'ACO', type: 'PHYSICAL', createdAt: 1765468282000, updatedAt: 1765468282000 },
{ id: 2, name: 'EVS', type: 'AOT-EVENT', createdAt: 1765468282000, updatedAt: 1765468282000 },
{ id: 3, name: 'GLO', type: 'QC', createdAt: 1765468282000, updatedAt: 1765468282000 },
{ id: 4, name: 'MUD', type: 'MUON-GLO', createdAt: 1765468282000, updatedAt: 1765468282000 },
{ id: 5, name: 'VTX', type: 'AOT-GLO', createdAt: 1765468282000, updatedAt: 1765468282000 },
{ id: 6, name: 'TST', type: 'VIRTUAL', createdAt: 1765468282000, updatedAt: 1765468282000 },
],
};

nock(VALID_CONFIG.bookkeeping.url)
.get(GET_DETECTORS_PATH)
.query({ token: VALID_CONFIG.bookkeeping.token })
.reply(200, mockResponse);

const result = await bkpService.retrieveDetectorSummaries();

ok(Array.isArray(result));
strictEqual(result.length, mockResponse.data.length);

// Verify detector data is preserved
deepStrictEqual(result, mockResponse.data);
});

test('should return empty array when data is not an array', async () => {
const mockResponse = {
data: null,
};

nock(VALID_CONFIG.bookkeeping.url)
.get(GET_DETECTORS_PATH)
.query({ token: VALID_CONFIG.bookkeeping.token })
.reply(200, mockResponse);

const result = await bkpService.retrieveDetectorSummaries();

ok(Array.isArray(result));
strictEqual(result.length, 0);
});

test('should return empty array when data is empty array', async () => {
const mockResponse = {
data: [],
};

nock(VALID_CONFIG.bookkeeping.url)
.get(GET_DETECTORS_PATH)
.query({ token: VALID_CONFIG.bookkeeping.token })
.reply(200, mockResponse);

const result = await bkpService.retrieveDetectorSummaries();

ok(Array.isArray(result));
strictEqual(result.length, 0);
});
});
});
};
27 changes: 26 additions & 1 deletion QualityControl/test/lib/services/FilterService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or submit itself to any jurisdiction.
*/

import { deepStrictEqual } from 'node:assert';
import { deepStrictEqual, ok } from 'node:assert';
import { suite, test, beforeEach, afterEach } from 'node:test';
import { FilterService } from '../../../lib/services/FilterService.js';
import { RunStatus } from '../../../common/library/runStatus.enum.js';
Expand All @@ -32,6 +32,7 @@ export const filterServiceTestSuite = async () => {
connect: stub(),
retrieveRunTypes: stub(),
retrieveRunInformation: stub(),
retrieveDetectorSummaries: stub(),
active: true, // assume the bookkeeping service is active by default
};
filterService = new FilterService(bookkeepingServiceMock, configMock);
Expand Down Expand Up @@ -63,6 +64,11 @@ export const filterServiceTestSuite = async () => {
deepStrictEqual(filterServiceWithCustomConfig._runTypesRefreshInterval, 5000);
});

test('should init _detectors on instantiation', async () => {
deepStrictEqual(filterService._detectors, []);
ok(Object.isFrozen(filterService._detectors));
});

test('should init filters on instantiation', async () => {
const initFiltersStub = stub(filterService, 'initFilters');
await filterService.initFilters();
Expand All @@ -71,7 +77,26 @@ export const filterServiceTestSuite = async () => {
});

suite('initFilters', async () => {
test('should call _initializeDetectors', async () => {
const initializeDetectorsStub = stub(filterService, '_initializeDetectors');
await filterService.initFilters();
ok(initializeDetectorsStub.calledOnce);
});

test('should set _detectors on _initializeDetectors call', async () => {
const DETECTOR_SUMMARIES = [
{
name: 'Detector human-readable name',
type: 'Detector type identifier',
},
];

bookkeepingServiceMock.retrieveDetectorSummaries.resolves(DETECTOR_SUMMARIES);
await filterService._initializeDetectors();

deepStrictEqual(filterService._detectors, DETECTOR_SUMMARIES);
ok(Object.isFrozen(filterService._detectors));
});
});

suite('getRunTypes', async () => {
Expand Down
Loading
Loading