Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
*/
7 changes: 3 additions & 4 deletions QualityControl/lib/controllers/FilterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,11 @@ export class FilterController {
*/
async getFilterConfigurationHandler(req, 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 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';
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
28 changes: 27 additions & 1 deletion QualityControl/lib/services/FilterService.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
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 @@
async initFilters() {
await this._bookkeepingService.connect();
await this.getRunTypes();
await this._initializeDetectors();
}

/**
Expand All @@ -71,6 +72,31 @@
}
}

/**
* 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() {
try {
if (!this._bookkeepingService.active) {
return;
}

Check failure on line 84 in QualityControl/lib/services/FilterService.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

Trailing spaces not allowed
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
20 changes: 15 additions & 5 deletions QualityControl/test/lib/controllers/FiltersController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ export const filtersControllerTestSuite = async () => {
});

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

const res = {
status: sinon.stub().returnsThis(),
Expand All @@ -51,9 +58,12 @@ export const filtersControllerTestSuite = async () => {
const filterController = new FilterController(filterService);
await 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', async () => {
const bkpService = null;
const res = {
status: sinon.stub().returnsThis(),
Expand All @@ -64,8 +74,8 @@ export const filtersControllerTestSuite = async () => {
await 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
74 changes: 74 additions & 0 deletions QualityControl/test/lib/services/BookkeepingService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,80 @@ export const bookkeepingServiceTestSuite = async () => {
ok(Object.values(RunStatus).includes(runStatus));
});

suite('Retrieve detector summaries', () => {
const GET_DETECTORS_PATH = '/api/detectors';

let bkpService = null;

beforeEach(() => {
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);
});
});

test('should return ONGOING status when timeO2End is not present', async () => {
const mockResponse = { data: { timeO2End: undefined } };

Expand Down
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
42 changes: 42 additions & 0 deletions QualityControl/test/setup/testSetupForBkp.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,48 @@ export const initializeNockForBkp = () => {
},
},
});
nock(BKP_URL)
.persist()
.get(`/api/detectors${TOKEN_PATH}`)
.reply(200, {
data: [
{
id: 17,
name: 'ACO',
type: 'PHYSICAL',
createdAt: 1765468282000,
updatedAt: 1765468282000,
},
{
id: 1,
name: 'CPV',
type: 'PHYSICAL',
createdAt: 1765468282000,
updatedAt: 1765468282000,
},
{
id: 23,
name: 'EVS',
type: 'AOT-EVENT',
createdAt: 1765468282000,
updatedAt: 1765468282000,
},
{
id: 21,
name: 'GLO',
type: 'QC',
createdAt: 1765468282000,
updatedAt: 1765468282000,
},
{
id: 15,
name: 'TST',
type: 'VIRTUAL',
createdAt: 1765468282000,
updatedAt: 1765468282000,
},
],
});
nock(BKP_URL)
.persist()
.get(`/api/runs/0${TOKEN_PATH}`)
Expand Down
Loading