diff --git a/api/jest.config.ts b/api/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..09e402b93abdd342aa1ba638532a1805527a247d --- /dev/null +++ b/api/jest.config.ts @@ -0,0 +1,31 @@ +import type { Config } from '@jest/types'; +import * as fs from 'fs'; +import * as dotenv from 'dotenv'; + +['.env.defaults', '.env'].forEach((f) => dotenv.config({ path: f })); + +const srcPath = 'src/engine/connectors'; +const engine_type = process.env.ENGINE_TYPE; // if there no engine all tests will run + +export default async (): Promise<Config.InitialOptions> => { + const dirs = (await fs.promises.readdir(srcPath)) + .filter((dir) => dir !== engine_type) + .map((dir) => `${srcPath}/${dir}`); + + return { + moduleFileExtensions: ['js', 'json', 'ts'], + testPathIgnorePatterns: dirs, + rootDir: 'src', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + setupFiles: ['dotenv/config'], + moduleNameMapper: { + '^src/(.*)$': '<rootDir>/$1', + }, + }; +}; diff --git a/api/package-lock.json b/api/package-lock.json index f91260c22f0d301c9b44718acff90118bbaedd2d..3cf24e6b82faa64c029d614fdcbcfc1cf4d543f7 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -29,7 +29,7 @@ "@eclass/semantic-release-docker": "^2.0.0", "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", - "@nestjs/testing": "^8.0.0", + "@nestjs/testing": "^8.2.2", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/gitlab": "^7.0.4", @@ -2105,13 +2105,13 @@ "dev": true }, "node_modules/@nestjs/testing": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.0.6.tgz", - "integrity": "sha512-HRXGM5RlGa+o+kxWI9DQCALndSvL3Remjg1cZVFp2w2s5eXRPpiFMo9puXtu9DSc4tz78xYcQGmEaeYNTB7gvg==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.2.2.tgz", + "integrity": "sha512-TeNdmjSMKKCw6Z4duu5h4FuGtBV0SXqM4qxLytFJ6ZobaODjqh85u9fWqJlthH+XuHslVYsmNBkCeNrgzyrL8A==", "dev": true, "dependencies": { "optional": "0.1.4", - "tslib": "2.3.0" + "tslib": "2.3.1" }, "funding": { "type": "opencollective", @@ -2132,6 +2132,12 @@ } } }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, "node_modules/@nestjs/typeorm": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-8.0.2.tgz", @@ -14165,6 +14171,24 @@ "randombytes": "^2.1.0" } }, + "@nestjs/testing": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.2.2.tgz", + "integrity": "sha512-TeNdmjSMKKCw6Z4duu5h4FuGtBV0SXqM4qxLytFJ6ZobaODjqh85u9fWqJlthH+XuHslVYsmNBkCeNrgzyrL8A==", + "dev": true, + "requires": { + "optional": "0.1.4", + "tslib": "2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "node_modules/serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", diff --git a/api/package.json b/api/package.json index 00f6a57ccb3c5028ae54daed1fcb0237ebe8137a..bd9aef5a817c42537a4ddfb453266f0be3855be9 100644 --- a/api/package.json +++ b/api/package.json @@ -18,7 +18,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test:e2e": "jest --config ./test/jest.e2e-config.ts", + "test:we2e": "jest --watch --config ./test/jest.e2e-config.ts", "prepare": "cd .. && husky install api/.husky" }, "dependencies": { @@ -42,7 +43,7 @@ "@eclass/semantic-release-docker": "^2.0.0", "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", - "@nestjs/testing": "^8.0.0", + "@nestjs/testing": "^8.2.2", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/gitlab": "^7.0.4", diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts index ddf5dedfa9585bdffa46ef5a90ca643d49eacf7a..efcb1cd621628c288d0f7d6d5b674275add8d4b8 100644 --- a/api/src/common/interfaces/utilities.interface.ts +++ b/api/src/common/interfaces/utilities.interface.ts @@ -1 +1,14 @@ export type Dictionary<T> = { [key: string]: T }; + +export enum MIME_TYPES { + ERROR = 'text/plain+error', + WARNING = 'text/plain+warning', + USER_WARNING = 'text/plain+user_error', + HIGHCHARTS = 'application/vnd.highcharts+json', + JSON = 'application/json', + JSONBTREE = 'application/binary-tree+json', + PFA = 'application/pfa+json', + JSONDATA = 'application/vnd.dataresource+json', + HTML = 'text/html', + TEXT = 'text/plain', +} diff --git a/api/src/engine/connectors/csv/main.connector.ts b/api/src/engine/connectors/csv/main.connector.ts index 05864adb55c7141de3ec49bd6854b5fdcdb793ee..c9c2b6fd71a69082dbfc95d670c0a8d7a9d4ee87 100644 --- a/api/src/engine/connectors/csv/main.connector.ts +++ b/api/src/engine/connectors/csv/main.connector.ts @@ -41,18 +41,16 @@ export default class CSVService implements IEngineService { throw new Error('Method not implemented.'); } - getExperiment(uuid: string): Experiment | Promise<Experiment> { + getExperiment(id: string): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); } - removeExperiment( - uuid: string, - ): PartialExperiment | Promise<PartialExperiment> { + removeExperiment(id: string): PartialExperiment | Promise<PartialExperiment> { throw new Error('Method not implemented.'); } editExperient( - uuid: string, + id: string, expriment: ExperimentEditInput, ): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 462b41003fe536e8976563b11b60e52e562361a5..0b884a9061ef1ad789cb6886f4d125670476337b 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -45,18 +45,16 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - getExperiment(uuid: string): Experiment | Promise<Experiment> { + getExperiment(id: string): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); } - removeExperiment( - uuid: string, - ): PartialExperiment | Promise<PartialExperiment> { + removeExperiment(id: string): PartialExperiment | Promise<PartialExperiment> { throw new Error('Method not implemented.'); } editExperient( - uuid: string, + id: string, expriment: ExperimentEditInput, ): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); diff --git a/api/src/engine/connectors/exareme/converters.ts b/api/src/engine/connectors/exareme/converters.ts index e9bc1d7c51908d9b833500d70e573774b908a221..5ca4b79db61a06924af99aebb4e0ca44670de630 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,7 +1,9 @@ +import { MIME_TYPES } from 'src/common/interfaces/utilities.interface'; import { Category } from 'src/engine/models/category.model'; -import { AlgorithmParameter } from 'src/engine/models/experiment/algorithm-parameter.model'; +import { Dataset } from 'src/engine/models/dataset.model'; import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { AlgorithmParamInput } from 'src/engine/models/experiment/input/algorithm-parameter.input'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; import { Group } from 'src/engine/models/group.model'; import { ResultUnion } from 'src/engine/models/result/common/result-union.model'; @@ -9,14 +11,19 @@ import { GroupResult, GroupsResult, } from 'src/engine/models/result/groups-result.model'; +import { HeatMapResult } from 'src/engine/models/result/heat-map-result.model'; +import { LineChartResult } from 'src/engine/models/result/line-chart-result.model'; import { RawResult } from 'src/engine/models/result/raw-result.model'; import { Variable } from 'src/engine/models/variable.model'; import { Entity } from './interfaces/entity.interface'; import { ExperimentData } from './interfaces/experiment/experiment.interface'; +import { ResultChartExperiment } from './interfaces/experiment/result-chart-experiment.interface'; import { ResultExperiment } from './interfaces/experiment/result-experiment.interface'; import { Hierarchy } from './interfaces/hierarchy.interface'; import { VariableEntity } from './interfaces/variable-entity.interface'; import { + dataROCToLineResult, + dataToHeatmap, descriptiveModelToTables, descriptiveSingleToTables, transformToAlgorithms, @@ -43,6 +50,14 @@ export const dataToCategory = (data: Entity): Category => { }; }; +export const dataToDataset = (data: Entity): Dataset => { + return { + id: data.code, + label: data.label, + isLongitudinal: !!data.code.toLowerCase().includes('longitudinal'), + }; +}; + export const dataToVariable = (data: VariableEntity): Variable => { return { id: data.code, @@ -56,11 +71,11 @@ export const dataToVariable = (data: VariableEntity): Variable => { }; }; -const algoParamInputToData = (param: AlgorithmParameter) => { +const algoParamInputToData = (param: AlgorithmParamInput) => { return { - name: param.name, - label: param.name, - value: param.value.join(','), + name: param.id, + label: param.id, + value: param.value, }; }; @@ -69,7 +84,7 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { ((data.transformations?.length > 0 || data.interactions?.length > 0) && { single: data.transformations?.map((t) => ({ - var_name: t.name, + var_name: t.id, unary_operation: t.operation, })) || [], interactions: @@ -79,7 +94,7 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { }) || null; - return { + const params = { algorithm: { parameters: [ { @@ -97,11 +112,6 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { label: 'pathology', value: data.domain, }, - { - name: 'y', - label: 'y', - value: data.variables.join(','), - }, ...(formula ? [ { @@ -112,10 +122,59 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { : []), ].concat(data.algorithm.parameters.map(algoParamInputToData)), type: data.algorithm.type ?? 'string', - name: data.algorithm.name, + name: data.algorithm.id, }, name: data.name, }; + + if (data.coVariables && data.coVariables.length) { + let separator = ','; + + const design = params.algorithm.parameters.find((p) => p.name === 'design'); + const excludes = [ + 'Multiple Histograms', + 'CART', + 'ID3', + 'Naive Bayes Training', + ]; + + if (design && !excludes.includes(data.algorithm.id)) { + separator = design.value === 'additive' ? '+' : '*'; + } + + params.algorithm.parameters.push({ + name: 'x', + label: 'x', + value: data.coVariables.join(separator), + }); + } + + if (data.variables) { + let variables = data.variables.join(','); + + if (data.algorithm.id === 'TTEST_PAIRED') { + const varCount = data.variables.length; + variables = data.variables + ?.reduce( + (vectors: string, v, i) => + (i + 1) % 2 === 0 + ? `${vectors}${v},` + : varCount === i + 1 + ? `${vectors}${v}-${data.variables[0]}` + : `${vectors}${v}-`, + '', + ) + .replace(/,$/, ''); + } + + params.algorithm.parameters.push({ + name: 'y', + label: 'y', + value: variables, + }); + } + + return params; }; export const descriptiveDataToTableResult = ( @@ -143,31 +202,56 @@ export const descriptiveDataToTableResult = ( return [result]; }; -export const dataToExperiment = (data: ExperimentData): Experiment => { - const expTransform = transformToExperiment.evaluate(data); +export const dataToExperiment = ( + data: ExperimentData, +): Experiment | undefined => { + try { + const expTransform = transformToExperiment.evaluate(data); - const exp: Experiment = { - ...expTransform, - results: [], - }; + const exp: Experiment = { + ...expTransform, + results: [], + }; - exp.results = data.result - ? data.result - .map((result) => dataToResult(result, exp.algorithm.name)) - .flat() - : []; + exp.results = data.result + ? data.result + .map((result) => dataToResult(result, exp.algorithm.id)) + .flat() + : []; - return exp; + return exp; + } catch (e) { + return { + id: data.uuid, + name: data.name, + status: 'error', + variables: [], + domain: data['domain'] ?? '', + datasets: [], + algorithm: { + id: 'unknown', + }, + }; + } }; export const dataToAlgorithms = (data: string): Algorithm[] => { return transformToAlgorithms.evaluate(data); }; -export const dataToRaw = (result: ResultExperiment): RawResult[] => { +export const dataToRaw = ( + algo: string, + result: ResultExperiment, +): RawResult[] => { + let data = result; + + if (algo === 'CART') { + data = { ...data, type: MIME_TYPES.JSONBTREE }; + } + return [ { - rawdata: result.data, + rawdata: data, }, ]; }; @@ -179,8 +263,10 @@ export const dataToResult = ( switch (result.type.toLowerCase()) { case 'application/json': return dataJSONtoResult(result, algo); + case 'application/vnd.highcharts+json': + return dataHighchartToResult(result as ResultChartExperiment, algo); default: - return dataToRaw(result); + return dataToRaw(algo, result); } }; @@ -192,6 +278,20 @@ export const dataJSONtoResult = ( case 'descriptive_stats': return descriptiveDataToTableResult(result); default: - return []; + return dataToRaw(algo, result); + } +}; + +export const dataHighchartToResult = ( + result: ResultChartExperiment, + algo: string, +): Array<typeof ResultUnion> => { + switch (result.data.chart.type) { + case 'heatmap': + return [dataToHeatmap.evaluate(result) as HeatMapResult]; + case 'area': + return [dataROCToLineResult.evaluate(result) as LineChartResult]; + default: + return dataToRaw(algo, result); } }; diff --git a/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts b/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts index 7412cb0560c9eda88fe61dd4ebbf8128c94e2c62..cd5ceb45a4539812ec15cb94755f2302bb99de63 100644 --- a/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts +++ b/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts @@ -5,6 +5,7 @@ export interface ExperimentData { uuid?: string; status?: string; createdBy?: string; + created: string; shared?: boolean; viewed?: boolean; result?: ResultExperiment[]; diff --git a/api/src/engine/connectors/exareme/interfaces/experiment/result-chart-experiment.interface.ts b/api/src/engine/connectors/exareme/interfaces/experiment/result-chart-experiment.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef0ba339600d418e2ed02a484dfebcfc5ab8b32b --- /dev/null +++ b/api/src/engine/connectors/exareme/interfaces/experiment/result-chart-experiment.interface.ts @@ -0,0 +1,9 @@ +export interface ResultChartExperiment { + data: { + chart: { + type: string; + }; + series: unknown; + }; + type: string; +} diff --git a/api/src/engine/connectors/exareme/interfaces/test-utilities.ts b/api/src/engine/connectors/exareme/interfaces/test-utilities.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ad1c79e8384dfb1a12fe8dbe75511720dfe4e1f --- /dev/null +++ b/api/src/engine/connectors/exareme/interfaces/test-utilities.ts @@ -0,0 +1,81 @@ +import { IEngineService } from 'src/engine/engine.interfaces'; +import { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; + +const TIMEOUT_DURATION_SECONDS = 60 * 10; + +const TEST_PATHOLOGIES = { + dementia: { + code: 'dementia', + datasets: [ + { + code: 'desd-synthdata', + }, + { code: 'edsd' }, + { code: 'ppmi' }, + { code: 'fake_longitudinal' }, + ], + }, + mentalhealth: { + code: 'mentalhealth', + datasets: [{ code: 'demo' }], + }, + tbi: { + code: 'tbi', + datasets: [{ code: 'dummy_tbi' }], + }, +}; + +const createExperiment = async ( + input: ExperimentCreateInput, + service: IEngineService, +): Promise<Experiment | undefined> => { + return await service.createExperiment(input, false); +}; + +const waitForResult = ( + id: string, + service: IEngineService, +): Promise<Experiment> => + new Promise((resolve, reject) => { + let elapsed = 0; + const timerId = setInterval(async () => { + const experiment = await service.getExperiment(id); + + const loading = experiment ? experiment.status === 'pending' : true; + + if (!loading) { + clearInterval(timerId); + resolve(experiment); + } + + if (elapsed > TIMEOUT_DURATION_SECONDS) { + clearInterval(timerId); + reject( + `Query experiment ${experiment.id} timeout after ${TIMEOUT_DURATION_SECONDS} s`, + ); + } + + elapsed = elapsed + 0.3; + }, 300); + }); + +const uid = (): string => + 'xxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + + return v.toString(16); + }); + +const generateNumber = (): string => { + return Math.round(Math.random() * 10000).toString(); +}; +export { + createExperiment, + uid, + waitForResult, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, +}; diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index 146fb4308b44b7a189b1f31cadcd37e0c35cd8c0..5733366f9125337a555ff7f7df7faad52dae6627 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -24,7 +24,7 @@ import { Group } from 'src/engine/models/group.model'; import { Variable } from 'src/engine/models/variable.model'; import { dataToAlgorithms, - dataToCategory, + dataToDataset, dataToExperiment, dataToGroup, dataToVariable, @@ -74,7 +74,7 @@ export default class ExaremeService implements IEngineService { return { ...resultAPI.data, - experiments: resultAPI.data.experiments.map(dataToExperiment), + experiments: resultAPI.data.experiments?.map(dataToExperiment) ?? [], }; } @@ -86,8 +86,8 @@ export default class ExaremeService implements IEngineService { return dataToAlgorithms(resultAPI.data); } - async getExperiment(uuid: string): Promise<Experiment> { - const path = this.options.baseurl + `experiments/${uuid}`; + async getExperiment(id: string): Promise<Experiment> { + const path = this.options.baseurl + `experiments/${id}`; const resultAPI = await firstValueFrom( this.httpService.get<ExperimentData>(path), @@ -97,10 +97,10 @@ export default class ExaremeService implements IEngineService { } async editExperient( - uuid: string, + id: string, expriment: ExperimentEditInput, ): Promise<Experiment> { - const path = this.options.baseurl + `experiments/${uuid}`; + const path = this.options.baseurl + `experiments/${id}`; const resultAPI = await firstValueFrom( this.httpService.patch<ExperimentData>(path, expriment), @@ -109,16 +109,16 @@ export default class ExaremeService implements IEngineService { return dataToExperiment(resultAPI.data); } - async removeExperiment(uuid: string): Promise<PartialExperiment> { - const path = this.options.baseurl + `experiments/${uuid}`; + async removeExperiment(id: string): Promise<PartialExperiment> { + const path = this.options.baseurl + `experiments/${id}`; try { await firstValueFrom(this.httpService.delete(path)); return { - uuid: uuid, + id: id, }; } catch (error) { - throw new BadRequestException(`${uuid} does not exists`); + throw new BadRequestException(`${id} does not exists`); } } @@ -141,7 +141,7 @@ export default class ExaremeService implements IEngineService { label: data.label, groups: groups, rootGroup: dataToGroup(data.metadataHierarchy), - datasets: data.datasets ? data.datasets.map(dataToCategory) : [], + datasets: data.datasets ? data.datasets.map(dataToDataset) : [], variables: data.metadataHierarchy ? this.flattenVariables(data.metadataHierarchy, groups) : [], @@ -172,22 +172,22 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } - getExperimentREST(uuid: string): Observable<string> { - const path = this.options.baseurl + `experiments/${uuid}`; + getExperimentREST(id: string): Observable<string> { + const path = this.options.baseurl + `experiments/${id}`; return this.httpService .get<string>(path) .pipe(map((response) => response.data)); } - deleteExperiment(uuid: string): Observable<string> { - const path = this.options.baseurl + `experiments/${uuid}`; + deleteExperiment(id: string): Observable<string> { + const path = this.options.baseurl + `experiments/${id}`; return this.httpService.delete(path).pipe(map((response) => response.data)); } - editExperimentREST(uuid: string): Observable<string> { - const path = this.options.baseurl + `experiments/${uuid}`; + editExperimentREST(id: string): Observable<string> { + const path = this.options.baseurl + `experiments/${id}`; return this.httpService .patch(path, this.req.body) diff --git a/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8770e901a69a2cfbb5575f079e2d7c65bcd32b0f --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts @@ -0,0 +1,74 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + + const modelSlug = `3c-${generateNumber()}`; + const algorithmId = 'THREE_C'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus', 'righthippocampus', 'leftcaudate'], + coVariables: ['gender', 'agegroup'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code === 'ppmi' || d.code === 'edsd') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { id: 'dx', value: 'alzheimerbroadcategory' }, + { id: 'c2_feature_selection_method', value: 'RF' }, + { id: 'c2_num_clusters_method', value: 'Euclidean' }, + { id: 'c2_num_clusters', value: '6' }, + { id: 'c2_clustering_method', value: 'Euclidean' }, + { id: 'c3_feature_selection_method', value: 'RF' }, + { id: 'c3_classification_method', value: 'RF' }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..447b8f457dafe655ee12d7d518bcf8b0fc7d37c7 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts @@ -0,0 +1,87 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + + const modelSlug = `calibration-belt-${generateNumber()}`; + const algorithmId = 'CALIBRATION_BELT'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['mortality_gose'], + coVariables: ['mortality_core'], + datasets: TEST_PATHOLOGIES.tbi.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.tbi.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'devel', + value: 'external', + }, + { + id: 'max_deg', + value: '4', + }, + { + id: 'confLevels', + value: '0.80,0.95', + }, + { + id: 'thres', + value: '0.95', + }, + { + id: 'num_points', + value: '200', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult).toBeTruthy(); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8b0085449c7a0229044d66cd73b09a727149ce1 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts @@ -0,0 +1,72 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `cart-${generateNumber()}`; + const algorithmId = 'CART'; + + const input: ExperimentCreateInput = { + name: modelSlug, + coVariables: ['lefthippocampus', 'righthippocampus'], + variables: ['alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { id: 'max_depth', value: '3' }, + { id: 'no_split_points', value: '10' }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const r0 = experimentResult.results[0] as RawResult; + + expect(r0?.rawdata['data'].gain).toEqual(0.6142216049382716); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5808b159dae93067640e66ade63f82a15ab3debb --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts @@ -0,0 +1,76 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GroupsResult } from 'src/engine/models/result/groups-result.model'; +import { TableResult } from 'src/engine/models/result/table-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `statistics-${generateNumber()}`; + const algorithmId = 'DESCRIPTIVE_STATS'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus', 'alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [], + }, + filter: '', + transformations: [ + { + id: 'lefthippocampus', + operation: 'standardize', + }, + ], + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const r0 = experimentResult.results[0] as GroupsResult; + const table = r0.groups[0].results[0] as TableResult; + + expect(table.data[0][2]).toEqual(474); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ef963a6ca5ab93cbe07bf0ff422e40a94744413 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts @@ -0,0 +1,69 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `id3-${generateNumber()}`; + const algorithmId = 'ID3'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['alzheimerbroadcategory'], + coVariables: ['gender', 'agegroup'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [{ id: 'iterations_max_number', value: '20' }], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(2); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data']['data'][0][2]).toEqual('+80y'); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..57054721ff0675b01d5865ca312bcaa5335e143f --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts @@ -0,0 +1,84 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `kmeans-${generateNumber()}`; + const algorithmId = 'KMEANS'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['leftacgganteriorcingulategyrus', 'rightcerebellumexterior'], + coVariables: ['alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'k', + value: '4', + }, + { + id: 'e', + value: '1', + }, + { + id: 'iterations_max_number', + value: '1000', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect( + data.rawdata['data'][2]['leftacgganteriorcingulategyrus'], + ).toBeCloseTo(4.197, 2); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0766164343410b79c98588100c3a43d037b4df1 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts @@ -0,0 +1,82 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `kaplan-meier-${generateNumber()}`; + const algorithmId = 'KAPLAN_MEIER'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['alzheimerbroadcategory'], + coVariables: ['apoe4'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'outcome_pos', + value: 'AD', + }, + { + id: 'outcome_neg', + value: 'MCI', + }, + { + id: 'max_age', + value: '100', + }, + { + id: 'total_duration', + value: '1100', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ed7f222182726313aaaafde67af362e4f748cc1 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts @@ -0,0 +1,91 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `linear-${generateNumber()}`; + const algorithmId = 'LINEAR_REGRESSION'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus'], + coVariables: ['leftpcuprecuneus'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'referencevalues', + value: '[{"name":"alzheimerbroadcategory","val":"Other"}]', + }, + { + id: 'encodingparameter', + value: 'dummycoding', + }, + { + id: 'filter', + value: + '{"condition":"AND","rules":[{"id":"subjectageyears","field":"subjectageyears","type":"integer","input":"number","operator":"greater","value":"65"}],"valid":true}', + }, + ], + }, + filter: + '{"condition":"AND","rules":[{"id":"subjectageyears","field":"subjectageyears","type":"integer","input":"number","operator":"greater","value":"65"}],"valid":true}', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data']['coefficients'][0]['estimate']).toBeCloseTo( + 0.986, + 3, + ); + expect(data.rawdata['data']['statistics'][0]['value']).toBeCloseTo( + -1.478, + 3, + ); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..abc045cd32efb98cd61908c77bd5a122719d7a41 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts @@ -0,0 +1,72 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `logistic-${generateNumber()}`; + const algorithmId = 'LOGISTIC_REGRESSION'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['gender'], + coVariables: ['lefthippocampus'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { id: 'positive_level', value: 'M' }, + { id: 'negative_level', value: 'F' }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data']['Coefficients'][0]).toBeCloseTo(-7.628, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7604dd93bd27b021f0437fd5c87304135b80957d --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts @@ -0,0 +1,65 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `histograms-${generateNumber()}`; + const algorithmId = 'MULTIPLE_HISTOGRAMS'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus'], + coVariables: ['gender', 'alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [{ id: 'bins', value: '{ "lefthippocampus" : 35 }' }], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..024e41227e8642e6896882d09213b02b7510ec25 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts @@ -0,0 +1,78 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `naivebayes-${generateNumber()}`; + const algorithmId = 'NAIVE_BAYES'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['alzheimerbroadcategory'], + coVariables: ['righthippocampus', 'lefthippocampus'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'alpha', + value: '0.1', + }, + { + id: 'k', + value: '10', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + + expect(experimentResult.results.length).toBeGreaterThanOrEqual(5); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data']['precision'][0]).toBeCloseTo(0.517, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8361797cd5805a1f84e4e8601d110e8ef344a9e --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts @@ -0,0 +1,65 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `anova-1way-${generateNumber()}`; + const algorithmId = 'ANOVA_ONEWAY'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus'], + coVariables: ['ppmicategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code === 'ppmi') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(4); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..79f65b81638c405455cdcf2288ce4fd46a6b3a59 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts @@ -0,0 +1,71 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `pca-${generateNumber()}`; + const algorithmId = 'PCA'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus', 'rightthalamusproper', 'leftthalamusproper'], + coVariables: ['alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { id: 'standardize', value: 'false' }, + { id: 'coding', value: 'null' }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(4); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data']['eigenvalues'][1]).toBeCloseTo(0.433, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1617dd43649650676c7eb41f7c3227158a1bec2 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts @@ -0,0 +1,73 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `pearson-${generateNumber()}`; + const algorithmId = 'PEARSON_CORRELATION'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['righthippocampus'], + coVariables: ['lefthippocampus'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { id: 'standardize', value: 'false' }, + { id: 'coding', value: 'null' }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(2); + const data = experimentResult.results[0] as RawResult; + + expect( + data.rawdata['data']['Pearson correlation coefficient'][0][0], + ).toBeCloseTo(0.924, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dee1858887a161e31f114f0343660847b676215 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts @@ -0,0 +1,101 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `ttest-idp-${generateNumber()}`; + const algorithmId = 'TTEST_INDEPENDENT'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: [ + 'rightpcggposteriorcingulategyrus', + 'leftpcggposteriorcingulategyrus', + 'rightacgganteriorcingulategyrus', + 'leftacgganteriorcingulategyrus', + 'rightmcggmiddlecingulategyrus', + 'leftmcggmiddlecingulategyrus', + 'rightphgparahippocampalgyrus', + ], + coVariables: ['gender'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'xlevels', + value: 'M,F', + }, + { + id: 'testvalue', + value: '3.0', + }, + { + id: 'hypothesis', + value: 'greaterthan', + }, + { + id: 'effectsize', + value: '1', + }, + { + id: 'ci', + value: '1', + }, + { + id: 'meandiff', + value: '1', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data'][0]['t_value']).toBeCloseTo(18.477, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a08e6fb3c9a4ce8332e09fc0a4b02b7cf89f2df7 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts @@ -0,0 +1,89 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `ttest-1s-${generateNumber()}`; + const algorithmId = 'TTEST_ONESAMPLE'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['righthippocampus', 'lefthippocampus'], + coVariables: ['gender', ' alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'testvalue', + value: '3.0', + }, + { + id: 'hypothesis', + value: 'different', + }, + { + id: 'effectsize', + value: '1', + }, + { + id: 'ci', + value: '1', + }, + { + id: 'meandiff', + value: '1', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data'][0]['t_value']).toBeCloseTo(8.155, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd2b971e8dcb5668faa917f1b1886efb1104359b --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts @@ -0,0 +1,72 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `ttest-paired-${generateNumber()}`; + const algorithmId = 'TTEST_PAIRED'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus', 'righthippocampus'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'hypothesis', + value: 'different', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data'][0]['t_value']).toBeCloseTo(-63.2, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b616e3266ed5ad805c74297c512c22f4c20c734 --- /dev/null +++ b/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts @@ -0,0 +1,89 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../../../../../main/app.module'; +import { ENGINE_SERVICE } from '../../../../engine.constants'; +import { IEngineService } from '../../../../engine.interfaces'; +import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; +import { + createExperiment, + generateNumber, + TEST_PATHOLOGIES, + TIMEOUT_DURATION_SECONDS, + waitForResult, +} from '../../interfaces/test-utilities'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('ExaremeService', () => { + let exaremeService: IEngineService; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + }); + const modelSlug = `anova-2way-${generateNumber()}`; + const algorithmId = 'ANOVA'; + + const input: ExperimentCreateInput = { + name: modelSlug, + variables: ['lefthippocampus'], + coVariables: ['alzheimerbroadcategory'], + datasets: TEST_PATHOLOGIES.dementia.datasets + .filter((d) => d.code !== 'fake_longitudinal') + .map((d) => d.code), + domain: TEST_PATHOLOGIES.dementia.code, + algorithm: { + id: algorithmId, + type: 'string', + parameters: [ + { + id: 'bins', + value: '40', + }, + { + id: 'iterations_max_number', + value: '20', + }, + { + id: 'sstype', + value: '2', + }, + { + id: 'outputformat', + value: 'pfa', + }, + { + id: 'design', + value: 'additive', + }, + ], + }, + filter: '', + }; + + describe('Integration Test for experiment API', () => { + it(`create ${algorithmId}`, async () => { + const experiment = await createExperiment(input, exaremeService); + + expect(experiment).toBeTruthy(); + expect(experiment?.status).toStrictEqual('pending'); + + expect(experiment?.id).toBeTruthy(); + + const experimentResult = await waitForResult( + experiment?.id ?? '', + exaremeService, + ); + + expect(experimentResult).toBeTruthy(); + expect(experimentResult.status).toStrictEqual('success'); + expect(experimentResult.results.length).toBeGreaterThanOrEqual(1); + const data = experimentResult.results[0] as RawResult; + + expect(data.rawdata['data'][0]['sumofsquares']).toBeCloseTo(34.196, 3); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/transformations.ts b/api/src/engine/connectors/exareme/transformations.ts index a57e677bf1e341a527f2c98c105376135c6a4339..07e59d977343342a6686f1f71ce936ee350cd7cd 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -5,16 +5,16 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = export const transformToAlgorithms = jsonata(` ( - $params := ["y", "pathology", "dataset", "filter"]; + $params := ["y", "pathology", "dataset", "filter", "x"]; $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; *.{ - 'name': name, + 'id': name, 'label': label, 'description': desc, 'parameters': $toArray(parameters[$not(name in $params)].{ - 'name': name, + 'id': name, 'description': desc, 'label': label, 'type': valueType, @@ -30,30 +30,45 @@ export const transformToAlgorithms = jsonata(` export const transformToExperiment = jsonata(` ( - $params := ["y", "pathology", "dataset", "filter"]; + $params := ["y", "pathology", "dataset", "filter", "x", "formula"]; + $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; + $convDate := function($v) { $type($v) = 'string' ? $toMillis($v) : $v }; + $rp := function($v) {$replace($v, /(\\+|\\*|-)/, ',')}; + $strSafe := function($v) { $type($v) = 'string' ? $v : "" }; + $formula := $eval(algorithm.parameters[name = "formula"].value); - { + ($ ~> | algorithm.parameters | {"name": name ? name : label } |){ "name": name, - "uuid": uuid, + "id": uuid, "author": createdBy, "viewed": viewed, "status": status, - "createdAt": created, - "finishedAt": finished, + "createdAt": $convDate(created), + "finishedAt": $convDate(finished), "shared": shared, - "updateAt": updated, - "domains": algorithm.parameters[name = "pathology"].value, - "variables": $split(algorithm.parameters[name = "y"].value, ','), - "filter": algorithm.parameters[name = "filter"].value, + "updateAt": $convDate(updated), + "domain": algorithm.parameters[name = "pathology"].value, "datasets": $split(algorithm.parameters[name = "dataset"].value, ','), + "variables": $split($rp(algorithm.parameters[name = "y"].value), ','), + "coVariables": $toArray($split($rp(algorithm.parameters[name = "x"].value), ',')), + "filterVariables": (algorithm.parameters[name = "filter"].value ~> $strSafe() ~> $match(/\\"id\\":\\"(\w*)\\"/)).groups, + "filter": algorithm.parameters[name = "filter"].value, + "formula": { + "transformations": $formula.single.{ + "id": var_name, + "operation": unary_operation + }[], + "interactions" : $formula.interactions.[var1, var2][] + }, "algorithm": { - "name": algorithm.name, - "parameters" : - algorithm.parameters[$not(name in $params)].({ - "name": name, - "label": label, - "value": value - }) + "id": algorithm.name, + "parameters" : $toArray( + algorithm.parameters[$not(name in $params)].({ + "id": name, + "label": label, + "value": value + }) + ) } } ) @@ -137,3 +152,57 @@ export const descriptiveSingleToTables = jsonata(` ] ) `); + +export const dataROCToLineResult = jsonata(` +({ + "name": data.title.text, + "xAxis": { + "label": data.xAxis.title.text + }, + "yAxis": { + "label": data.yAxis.title.text + }, + "lines": [ + { + "label": "ROC curve", + "x": data.series.data.$[0], + "y": data.series.data.$[1], + "type": 0 + } + ] +}) +`); + +export const dataToHeatmap = jsonata(` +( + { + "name": data.title.text, + "xAxis": { + "categories": data.xAxis.categories, + "label": data.xAxis.label + }, + "yAxis": { + "categories": data.yAxis.categories, + "label": data.yAxis.label + }, + "matrix": $toMat(data.series.data) + } +) +`); + +dataToHeatmap.registerFunction( + 'toMat', + (a) => { + const matrix = []; + + a.forEach( + (elem: { y: number | number; x: number | number; value: number }) => { + matrix[elem.y] = matrix[elem.y] ?? []; + matrix[elem.y][elem.x] = elem.value; + }, + ); + + return matrix; + }, + '<a<o>:a<a<n>>', +); diff --git a/api/src/engine/connectors/local/main.connector.ts b/api/src/engine/connectors/local/main.connector.ts index cb472d927f571c9553c0115624a9981f2be653c6..003b3c8cc2927a84dda52f8efe97b7d44edb8ab8 100644 --- a/api/src/engine/connectors/local/main.connector.ts +++ b/api/src/engine/connectors/local/main.connector.ts @@ -33,18 +33,16 @@ export default class LocalService implements IEngineService { throw new Error('Method not implemented.'); } - getExperiment(uuid: string): Experiment | Promise<Experiment> { + getExperiment(id: string): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); } - removeExperiment( - uuid: string, - ): PartialExperiment | Promise<PartialExperiment> { + removeExperiment(id: string): PartialExperiment | Promise<PartialExperiment> { throw new Error('Method not implemented.'); } editExperient( - uuid: string, + id: string, expriment: ExperimentEditInput, ): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index e56fa8e7ef75d1b50b233fa28f548129126f790f..a750538507388330a0407e4ef7f91700dc0b575b 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -30,19 +30,19 @@ export class EngineController { return this.engineService.getExperiments(); } - @Get('/experiments/:uuid') - getExperiment(@Param('uuid') uuid: string): Observable<string> | string { - return this.engineService.getExperimentREST(uuid); + @Get('/experiments/:id') + getExperiment(@Param('id') id: string): Observable<string> | string { + return this.engineService.getExperimentREST(id); } - @Delete('/experiments/:uuid') - deleteExperiment(@Param('uuid') uuid: string): Observable<string> | string { - return this.engineService.deleteExperiment(uuid); + @Delete('/experiments/:id') + deleteExperiment(@Param('id') id: string): Observable<string> | string { + return this.engineService.deleteExperiment(id); } - @Patch('/experiments/:uuid') - editExperiment(@Param('uuid') uuid: string): Observable<string> | string { - return this.engineService.editExperimentREST(uuid); + @Patch('/experiments/:id') + editExperiment(@Param('id') id: string): Observable<string> | string { + return this.engineService.editExperimentREST(id); } @Post('experiments/transient') diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index ec4fbc7fb43c1b163e3c28eb191162b0e0cf8337..a320765819458b30f430d84934b7e992af77459b 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -28,14 +28,12 @@ export interface IEngineService { name: string, ): Promise<ListExperiments> | ListExperiments; - getExperiment(uuid: string): Promise<Experiment> | Experiment; + getExperiment(id: string): Promise<Experiment> | Experiment; - removeExperiment( - uuid: string, - ): Promise<PartialExperiment> | PartialExperiment; + removeExperiment(id: string): Promise<PartialExperiment> | PartialExperiment; editExperient( - uuid: string, + id: string, expriment: ExperimentEditInput, ): Promise<Experiment> | Experiment; @@ -46,11 +44,11 @@ export interface IEngineService { getExperiments(): Observable<string> | string; - getExperimentREST(uuid: string): Observable<string> | string; + getExperimentREST(id: string): Observable<string> | string; - deleteExperiment(uuid: string): Observable<string> | string; + deleteExperiment(id: string): Observable<string> | string; - editExperimentREST(uuid: string): Observable<string> | string; + editExperimentREST(id: string): Observable<string> | string; startExperimentTransient(): Observable<string> | string; diff --git a/api/src/engine/engine.module.ts b/api/src/engine/engine.module.ts index 48442588a8d1a2c5120e99fa15d60e7da3c29739..6f8d33c6a63b40a9ac2c07a5bf3983ae0e148775 100644 --- a/api/src/engine/engine.module.ts +++ b/api/src/engine/engine.module.ts @@ -53,10 +53,11 @@ export class EngineModule { const engine = new service.default(options, httpService, req); return engine; - } catch { + } catch (e) { this.logger.error( `There is a problem with the connector '${options.type}'`, ); + this.logger.verbose(e); process.exit(); // We can't continue without an engine, shutdown the process... } } diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index 6e23ba38ca26d1129e51abb7cbba8d311cad67b9..ffed50e6467832491d03cf51cab35d6cc2292173 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -29,7 +29,7 @@ export class EngineResolver { } @Query(() => ListExperiments) - async experiments( + async experimentList( @Args('page', { nullable: true, defaultValue: 0 }) page: number, @Args('name', { nullable: true, defaultValue: '' }) name: string, ) { @@ -37,8 +37,8 @@ export class EngineResolver { } @Query(() => Experiment) - async expriment(@Args('uuid') uuid: string) { - return this.engineService.getExperiment(uuid); + async experiment(@Args('id') id: string) { + return this.engineService.getExperiment(id); } @Query(() => [Algorithm]) @@ -60,16 +60,14 @@ export class EngineResolver { @Mutation(() => Experiment) async editExperiment( - @Args('uuid') uuid: string, + @Args('id') id: string, @Args('data') experiment: ExperimentEditInput, ) { - return this.engineService.editExperient(uuid, experiment); + return this.engineService.editExperient(id, experiment); } @Mutation(() => PartialExperiment) - async removeExperiment( - @Args('uuid') uuid: string, - ): Promise<PartialExperiment> { - return this.engineService.removeExperiment(uuid); + async removeExperiment(@Args('id') id: string): Promise<PartialExperiment> { + return this.engineService.removeExperiment(id); } } diff --git a/api/src/engine/interceptors/headers.interceptor.ts b/api/src/engine/interceptors/headers.interceptor.ts index 9b28e4953456de5d9bc66a390c9cf452cf45306f..8dddccb93815c21cb0f92c79bd97d4fb293bfd35 100644 --- a/api/src/engine/interceptors/headers.interceptor.ts +++ b/api/src/engine/interceptors/headers.interceptor.ts @@ -1,17 +1,35 @@ import { HttpService } from '@nestjs/axios'; -import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common'; +import { + CallHandler, + HttpException, + Inject, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { IncomingMessage } from 'http'; -import { Observable, tap } from 'rxjs'; +import { catchError, Observable, tap } from 'rxjs'; +import { ENGINE_MODULE_OPTIONS } from '../engine.constants'; +import { IEngineOptions } from '../engine.interfaces'; @Injectable() export class HeadersInterceptor implements NestInterceptor { - constructor(private httpService: HttpService) {} + private readonly logger: Logger; + + constructor( + private httpService: HttpService, + @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, + ) { + // Logger name is the engine name + // HttpService will be used mostly by the engine (but it's not always true) + this.logger = new Logger(options.type); + } intercept(context: GqlExecutionContext, next: CallHandler): Observable<any> { // cleaner : add only the auth header (should find the name) - const keys = ['cookie', 'x-xsrf-token']; + const keys = ['cookie', 'x-xsrf-token']; // should be a module parameter let headers = {}; switch (context.getType()) { @@ -29,7 +47,7 @@ export class HeadersInterceptor implements NestInterceptor { } } - Object.keys(headers) + Object.keys(headers) // copy needed keys .filter((key) => keys.includes(key)) .map((key) => key.toLowerCase()) .forEach((key) => { @@ -37,8 +55,17 @@ export class HeadersInterceptor implements NestInterceptor { }); return next.handle().pipe( + catchError((e) => { + if (!e.response.data || !e.response.status) return e; + + this.logger.log(e.message); + this.logger.verbose( + `[Error ${e.response.status}] ${e.response.data.message}`, + ); + throw new HttpException(e.response.data, e.response.status); // catch errors, maybe make it optional (module parameter) + }), tap(() => { - this.httpService.axiosRef.defaults.headers.common = {}; //cleaning request + this.httpService.axiosRef.defaults.headers.common = {}; // cleaning request }), ); } diff --git a/api/src/engine/models/dataset.model.ts b/api/src/engine/models/dataset.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..a15340b0281906705dfc12e32a8af5db4165a27f --- /dev/null +++ b/api/src/engine/models/dataset.model.ts @@ -0,0 +1,8 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { Entity } from './entity.model'; + +@ObjectType() +export class Dataset extends Entity { + @Field({ nullable: true, defaultValue: false }) + isLongitudinal?: boolean; +} diff --git a/api/src/engine/models/domain.model.ts b/api/src/engine/models/domain.model.ts index f5b8def42a318613b348040b2eccb3ea67a7aa29..1cfef22fb86343d0c521ada3e1e7c7ebc5e7e001 100644 --- a/api/src/engine/models/domain.model.ts +++ b/api/src/engine/models/domain.model.ts @@ -1,5 +1,5 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { Category } from './category.model'; +import { Dataset } from './dataset.model'; import { Entity } from './entity.model'; import { Group } from './group.model'; import { Variable } from './variable.model'; @@ -15,8 +15,8 @@ export class Domain extends Entity { @Field(() => [Variable]) variables: Variable[]; - @Field(() => [Category]) - datasets: Category[]; + @Field(() => [Dataset]) + datasets: Dataset[]; @Field(() => Group) rootGroup: Group; diff --git a/api/src/engine/models/experiment/algorithm-parameter.model.ts b/api/src/engine/models/experiment/algorithm-parameter.model.ts index d5db6bfc11634ae3ec054c4c6eef9a5441f74931..d934ebd9a19d08ab2a8b2d0ea1d84f4fa2f369eb 100644 --- a/api/src/engine/models/experiment/algorithm-parameter.model.ts +++ b/api/src/engine/models/experiment/algorithm-parameter.model.ts @@ -3,10 +3,10 @@ import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class AlgorithmParameter { @Field() - name: string; + id: string; - @Field(() => [String], { nullable: true }) - value?: string[]; + @Field({ nullable: true }) + value?: string; @Field({ nullable: true }) label?: string; diff --git a/api/src/engine/models/experiment/algorithm.model.ts b/api/src/engine/models/experiment/algorithm.model.ts index ec26d747fc8019a29822bf673aa82b29162f5757..818b3c9d5c131a58625b3d724185cb6d87bc7827 100644 --- a/api/src/engine/models/experiment/algorithm.model.ts +++ b/api/src/engine/models/experiment/algorithm.model.ts @@ -4,7 +4,7 @@ import { AlgorithmParameter } from './algorithm-parameter.model'; @ObjectType() export class Algorithm { @Field() - name: string; + id: string; @Field(() => [AlgorithmParameter], { nullable: true, defaultValue: [] }) parameters?: AlgorithmParameter[]; diff --git a/api/src/engine/models/experiment/author.model.ts b/api/src/engine/models/experiment/author.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..0536a2e5a337e35f623dc9d649e817fa988dc13d --- /dev/null +++ b/api/src/engine/models/experiment/author.model.ts @@ -0,0 +1,10 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class Author { + @Field({ nullable: true, defaultValue: '' }) + username?: string; + + @Field({ nullable: true, defaultValue: '' }) + fullname?: string; +} diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index 06e28b4b2e1c93e8dac7c6e7dc828c706f370c8a..eb757d64d9444f4c505705d3d6faa2b7e32c4d72 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -1,14 +1,36 @@ import { Field, ObjectType, PartialType } from '@nestjs/graphql'; import { ResultUnion } from '../result/common/result-union.model'; import { Algorithm } from './algorithm.model'; +import { Author } from './author.model'; + +@ObjectType() +export class Transformation { + @Field({ description: "Variable's id on which to apply the transformation" }) + id: string; + + @Field({ description: 'Transformation to apply' }) + operation: string; +} + +@ObjectType() +export class Formula { + @Field(() => [Transformation], { nullable: true, defaultValue: [] }) + transformations: Transformation[]; + + @Field(() => [[String]], { nullable: true, defaultValue: [] }) + interactions: string[][]; +} @ObjectType() export class Experiment { - @Field({ nullable: true }) - uuid?: string; + @Field() + id: string; - @Field({ nullable: true, defaultValue: '' }) - author?: string; + @Field() + name: string; + + @Field(() => Author, { nullable: true, defaultValue: '' }) + author?: Author; @Field({ nullable: true }) createdAt?: number; @@ -43,11 +65,17 @@ export class Experiment { @Field(() => [String]) variables: string[]; - @Field() - algorithm: Algorithm; + @Field(() => [String], { nullable: true, defaultValue: [] }) + coVariables?: string[]; + + @Field(() => [String], { nullable: true, defaultValue: [] }) + filterVariables?: string[]; + + @Field(() => Formula, { nullable: true }) + formula?: Formula; @Field() - name: string; + algorithm: Algorithm; } @ObjectType() diff --git a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts index 8eb4cb26b5dade932440b6fef169275988d33b03..8cd3ec29ddfecb22c1574ea339cd96b4a010f79a 100644 --- a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts +++ b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts @@ -1,10 +1,25 @@ -import { Field, InputType } from '@nestjs/graphql'; +import { Field, InputType, registerEnumType } from '@nestjs/graphql'; + +export enum ParamType { + STRING, + NUMBER, +} + +registerEnumType(ParamType, { + name: 'ParamType', +}); @InputType() export class AlgorithmParamInput { @Field() - name: string; + id: string; + + @Field(() => ParamType, { + nullable: true, + defaultValue: ParamType.STRING, + }) + type?: ParamType; - @Field(() => [String]) - value: string[]; + @Field(() => String) + value: string; } diff --git a/api/src/engine/models/experiment/input/algorithm.input.ts b/api/src/engine/models/experiment/input/algorithm.input.ts index 54d26b7686559c39b311cee5e5496bce530b0cf5..d87abd4561f975dbb9dfc00964f658265a340085 100644 --- a/api/src/engine/models/experiment/input/algorithm.input.ts +++ b/api/src/engine/models/experiment/input/algorithm.input.ts @@ -4,7 +4,7 @@ import { AlgorithmParamInput } from './algorithm-parameter.input'; @InputType() export class AlgorithmInput { @Field() - name: string; + id: string; @Field(() => [AlgorithmParamInput], { nullable: true, defaultValue: [] }) parameters: AlgorithmParamInput[]; diff --git a/api/src/engine/models/experiment/input/experiment-create.input.ts b/api/src/engine/models/experiment/input/experiment-create.input.ts index b0b57cea85b17b0b49d4cbdf7b6cc12f21b71688..a34bb4c8a28c2ee456a6e88f44cdc6a87f4cd059 100644 --- a/api/src/engine/models/experiment/input/experiment-create.input.ts +++ b/api/src/engine/models/experiment/input/experiment-create.input.ts @@ -4,7 +4,7 @@ import { AlgorithmInput } from './algorithm.input'; @InputType() export class FormulaTransformation { @Field() - name: string; + id: string; @Field() operation: string; @@ -24,6 +24,9 @@ export class ExperimentCreateInput { @Field(() => [String]) variables: string[]; + @Field(() => [String], { nullable: true, defaultValue: [] }) + coVariables?: string[]; + @Field() algorithm: AlgorithmInput; @@ -31,8 +34,8 @@ export class ExperimentCreateInput { name: string; @Field(() => [FormulaTransformation], { nullable: true }) - transformations: FormulaTransformation[]; + transformations?: FormulaTransformation[]; @Field(() => [[String]], { nullable: true }) - interactions: string[][]; + interactions?: string[][]; } diff --git a/api/src/engine/models/experiment/input/experiment-edit.input.ts b/api/src/engine/models/experiment/input/experiment-edit.input.ts index d37deb1e8d008d746fda7214bb6890142b74a12a..30812416dea4bdb499226d44763790f9578f21a5 100644 --- a/api/src/engine/models/experiment/input/experiment-edit.input.ts +++ b/api/src/engine/models/experiment/input/experiment-edit.input.ts @@ -5,6 +5,9 @@ export class ExperimentEditInput { @Field({ nullable: true }) name?: string; + @Field({ nullable: true }) + shared?: boolean; + @Field({ nullable: true }) viewed?: boolean; } diff --git a/api/src/engine/models/result/common/result-union.model.ts b/api/src/engine/models/result/common/result-union.model.ts index 2f56bb4d1f6ef577e9e836972f45dd59ede7fc28..bc4bb4a1472ea131fd8696da73d5c3d227c44a8f 100644 --- a/api/src/engine/models/result/common/result-union.model.ts +++ b/api/src/engine/models/result/common/result-union.model.ts @@ -19,10 +19,6 @@ export const ResultUnion = createUnionType({ return TableResult; } - if (value.rawdata) { - return RawResult; - } - if (value.groups) { return GroupsResult; } @@ -31,10 +27,10 @@ export const ResultUnion = createUnionType({ return HeatMapResult; } - if (value.x) { + if (value.lines) { return LineChartResult; } - return null; + return RawResult; }, }); diff --git a/api/src/engine/models/result/heat-map-result.model.ts b/api/src/engine/models/result/heat-map-result.model.ts index 267dc3c20201236e5d11ee1cbcbb6d6d89af15a7..8dd65ada27662536bffa7aeea6c8b78b0759c2fa 100644 --- a/api/src/engine/models/result/heat-map-result.model.ts +++ b/api/src/engine/models/result/heat-map-result.model.ts @@ -7,12 +7,12 @@ export class HeatMapResult extends Result { @Field() name: string; - @Field(() => [[Number]]) - matrix: number[][]; + @Field(() => ChartAxis, { nullable: true }) + xAxis?: ChartAxis; - @Field(() => ChartAxis) - xAxis: ChartAxis; + @Field(() => ChartAxis, { nullable: true }) + yAxis?: ChartAxis; - @Field(() => ChartAxis) - yAxis: ChartAxis; + @Field(() => [[Number]]) + matrix: number[][]; } diff --git a/api/src/engine/models/result/raw-result.model.ts b/api/src/engine/models/result/raw-result.model.ts index 853362293bbfe6da230e487f8ea139ba36a16f3b..01528a5fd6dcbffe09d646bf7356ef87f18c200b 100644 --- a/api/src/engine/models/result/raw-result.model.ts +++ b/api/src/engine/models/result/raw-result.model.ts @@ -7,6 +7,6 @@ import { Result } from './common/result.model'; @ObjectType() export class RawResult extends Result { - @Field(() => GraphQLJSON) - rawdata: unknown; + @Field(() => GraphQLJSON, { nullable: true, defaultValue: '' }) + rawdata?: unknown; } diff --git a/api/src/engine/test/core.e2e-spec.ts b/api/src/engine/test/core.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb35467dd1911ea2f88c8af9f0268218ddfdb8a5 --- /dev/null +++ b/api/src/engine/test/core.e2e-spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Domain } from 'src/engine/models/domain.model'; +import { AppModule } from '../../main/app.module'; +import { TIMEOUT_DURATION_SECONDS } from '../connectors/exareme/interfaces/test-utilities'; +import { ENGINE_SERVICE } from '../engine.constants'; +import { IEngineService } from '../engine.interfaces'; + +jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); + +describe('Engine service', () => { + let engineService: IEngineService; + let domains: Domain[]; + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + engineService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + + domains = await engineService.getDomains([]); + }); + + it('Get domains', async () => { + expect(domains).toBeTruthy(); + expect(domains.length).toBeGreaterThanOrEqual(1); + }); + + it('Get datasets', async () => { + domains.forEach((domain) => { + expect(domain.datasets).toBeTruthy(); + }); + }); + + it('Get algorithms', async () => { + const algorithms = await engineService.getAlgorithms(); + expect(algorithms).toBeTruthy(); + expect(algorithms.length).toBeGreaterThanOrEqual(1); + }); + + it('Get groups', async () => { + domains.forEach((domain) => { + expect(domain.groups).toBeTruthy(); + expect(domain.groups.length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/api/src/schema.gql b/api/src/schema.gql index 1d30677261172d7d7fdc830d938d329d9b5f2d5d..854bd5ec152a379d4a5fcb3b94f6f6dbcf925d23 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -2,9 +2,10 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ -type Category { +type Dataset { id: String! label: String + isLongitudinal: Boolean } type Group { @@ -17,6 +18,11 @@ type Group { variables: [String!] } +type Category { + id: String! + label: String +} + type Variable { id: String! label: String @@ -32,13 +38,13 @@ type Domain { description: String groups: [Group!]! variables: [Variable!]! - datasets: [Category!]! + datasets: [Dataset!]! rootGroup: Group! } type AlgorithmParameter { - name: String! - value: [String!] + id: String! + value: String label: String description: String defaultValue: String @@ -50,7 +56,7 @@ type AlgorithmParameter { } type Algorithm { - name: String! + id: String! parameters: [AlgorithmParameter!] label: String type: String @@ -72,7 +78,7 @@ type TableResult { } type RawResult { - rawdata: JSON! + rawdata: JSON } """ @@ -86,9 +92,9 @@ type GroupsResult { type HeatMapResult { name: String! + xAxis: ChartAxis + yAxis: ChartAxis matrix: [[Float!]!]! - xAxis: ChartAxis! - yAxis: ChartAxis! } type LineChartResult { @@ -126,9 +132,28 @@ type Header { type: String! } +type Author { + username: String + fullname: String +} + +type Transformation { + """Variable's id on which to apply the transformation""" + id: String! + + """Transformation to apply""" + operation: String! +} + +type Formula { + transformations: [Transformation!] + interactions: [[String!]!] +} + type Experiment { - uuid: String - author: String + id: String! + name: String! + author: Author createdAt: Float updateAt: Float finishedAt: Float @@ -140,13 +165,16 @@ type Experiment { filter: String domain: String! variables: [String!]! + coVariables: [String!] + filterVariables: [String!] + formula: Formula algorithm: Algorithm! - name: String! } type PartialExperiment { - uuid: String - author: String + id: String + name: String + author: Author createdAt: Float updateAt: Float finishedAt: Float @@ -158,8 +186,10 @@ type PartialExperiment { filter: String domain: String variables: [String!] + coVariables: [String!] + filterVariables: [String!] + formula: Formula algorithm: Algorithm - name: String } type ListExperiments { @@ -171,15 +201,15 @@ type ListExperiments { type Query { domains(ids: [String!] = []): [Domain!]! - experiments(name: String = "", page: Float = 0): ListExperiments! - expriment(uuid: String!): Experiment! + experimentList(name: String = "", page: Float = 0): ListExperiments! + experiment(id: String!): Experiment! algorithms: [Algorithm!]! } type Mutation { createExperiment(isTransient: Boolean = false, data: ExperimentCreateInput!): Experiment! - editExperiment(data: ExperimentEditInput!, uuid: String!): Experiment! - removeExperiment(uuid: String!): PartialExperiment! + editExperiment(data: ExperimentEditInput!, id: String!): Experiment! + removeExperiment(id: String!): PartialExperiment! } input ExperimentCreateInput { @@ -187,6 +217,7 @@ input ExperimentCreateInput { filter: String domain: String! variables: [String!]! + coVariables: [String!] = [] algorithm: AlgorithmInput! name: String! transformations: [FormulaTransformation!] @@ -194,22 +225,29 @@ input ExperimentCreateInput { } input AlgorithmInput { - name: String! + id: String! parameters: [AlgorithmParamInput!] = [] type: String! } input AlgorithmParamInput { - name: String! - value: [String!]! + id: String! + type: ParamType = STRING + value: String! +} + +enum ParamType { + STRING + NUMBER } input FormulaTransformation { - name: String! + id: String! operation: String! } input ExperimentEditInput { name: String + shared: Boolean viewed: Boolean } diff --git a/api/test/app.e2e-spec.ts b/api/test/app.e2e-spec.ts index 50cda62332e9474925e819ff946358a9c40d1bf2..92622c06ac54f7020c4be59cc4f3751feffeef0b 100644 --- a/api/test/app.e2e-spec.ts +++ b/api/test/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { AppModule } from 'src/main/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; diff --git a/api/test/jest-e2e.json b/api/test/jest-e2e.json deleted file mode 100644 index e9d912f3e3cefc18505d3cd19b3a5a9f567f5de0..0000000000000000000000000000000000000000 --- a/api/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/api/test/jest.e2e-config.ts b/api/test/jest.e2e-config.ts new file mode 100644 index 0000000000000000000000000000000000000000..cad149913075e7aa049a6a11ea40b7e96b92e476 --- /dev/null +++ b/api/test/jest.e2e-config.ts @@ -0,0 +1,31 @@ +import type { Config } from '@jest/types'; +import * as fs from 'fs'; +import * as dotenv from 'dotenv'; + +['.env.defaults', '.env'].forEach((f) => dotenv.config({ path: f })); + +const srcPath = 'src/engine/connectors'; +const engine_type = process.env.ENGINE_TYPE; // if there no engine all tests will run + +export default async (): Promise<Config.InitialOptions> => { + const dirs = (await fs.promises.readdir(srcPath)) + .filter((dir) => dir !== engine_type) + .map((dir) => `${srcPath}/${dir}`); + + return { + moduleFileExtensions: ['js', 'json', 'ts'], + testPathIgnorePatterns: dirs, + rootDir: '../src', + testRegex: '.e2e-spec.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + setupFiles: ['dotenv/config'], + moduleNameMapper: { + '^src/(.*)$': '<rootDir>/$1', + }, + }; +};