From 043e1a18a904798a8a92ccc82173f6239dfd0332 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Wed, 29 Jun 2022 12:50:02 -0500 Subject: [PATCH 01/13] broken initial stab at summary object. --- bids-validator/src/main.ts | 1 + bids-validator/src/summary.ts | 72 +++++++++++++++++++ bids-validator/src/types/validation-result.ts | 29 +++++++- bids-validator/src/validators/bids.ts | 4 +- 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 bids-validator/src/summary.ts diff --git a/bids-validator/src/main.ts b/bids-validator/src/main.ts index 6e7748884..981ac0462 100644 --- a/bids-validator/src/main.ts +++ b/bids-validator/src/main.ts @@ -3,6 +3,7 @@ import { readFileTree } from './files/deno.ts' import { resolve } from './deps/path.ts' import { fullTestAdapter } from './compat/fulltest.ts' import { validate } from './validators/bids.ts' +import { summary } from './summary.ts' function inspect(obj: any) { console.log( diff --git a/bids-validator/src/summary.ts b/bids-validator/src/summary.ts new file mode 100644 index 000000000..388d23149 --- /dev/null +++ b/bids-validator/src/summary.ts @@ -0,0 +1,72 @@ +// import { Summary } from '../types/summary.ts' + +export const summary = { + dataProcessed: false, + totalFiles: -1, + size: 0, + sessions: new Set(), + subjects: new Set(), + subjectMetadata: {}, + tasks: new Set(), + modalities: { + mri: 0, + pet: 0, + meg: 0, + eeg: 0, + ieeg: 0, + microscopy: 0, + }, + secondaryModalities: { + MRI_Diffusion: 0, + MRI_Structural: 0, + MRI_Functional: 0, + MRI_Perfusion: 0, + PET_Static: 0, + PET_Dynamic: 0, + iEEG_ECoG: 0, + iEEG_SEEG: 0, + }, + pet: null, +} + +const modalityLookup = {} + +const secondaryLookup = { + dwi: 'MRI_Diffusion', + anat: 'MRI_Structural', + bold: 'MRI_Functional', + perf: 'MRI_Perfusion', +} + +export function updateSummary(context: Context): void { + if (context.file.path.startsWith('/derivatives')) { + return + } + + summary.totalFiles++ + summary.size += context.file.size + + if ('sub' in context.entities) { + summary.subjects.add(context.entities.sub) + } + if ('ses' in context.entities) { + summary.sesssions.add(context.entities.ses) + } + if ('task' in context.entities) { + summary.tasks.add(context.entities.task) + } + if (context.modality) { + summary.modalities[context.modality]++ + } + + if (context.datatype in secondaryLookup) { + const key = secondaryLookup[context.datatype] + secondary[key]++ + } else if (context.datatype === 'pet' && 'rec' in context.entities) { + if (['acstat', 'nacstat'].includes(context.entities.rec)) { + secondaty.PET_Static++ + } else if (['acdyn', 'nacdyn'].includes(context.entities.rec)) { + secondaty.PET_Dynamic++ + } + } +} diff --git a/bids-validator/src/types/validation-result.ts b/bids-validator/src/types/validation-result.ts index 82f0f44d7..4c5d8a2a0 100644 --- a/bids-validator/src/types/validation-result.ts +++ b/bids-validator/src/types/validation-result.ts @@ -1,9 +1,36 @@ import { DatasetIssues } from '../issues/datasetIssues.ts' +interface SubjectMetadata { + PARTICIPANT_ID: string + age: string + sex: string + group: string +} +/* + BodyPart: {}, + ScannerManufacturer: {}, + ScannerManufacturersModelName: {}, + TracerName: {}, + TracerRadionuclide: {}, +*/ + +export interface Summary { + sessions: string[] + subjects: string[] + subjectMetadata: SubjectMetaData[] + tasks: string[] + modalities: string[] + secondaryModalities: string[] + totalFiles: number + size: number + dataProcessed: boolean + pet: Record +} + /** * The output of a validation run */ export interface ValidationResult { issues: DatasetIssues - summary: Record + summary: Summary } diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index ad1510cf2..194680d88 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -11,6 +11,7 @@ import { } from './filenames.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { ValidationResult } from '../types/validation-result.ts' +import { updateSummary } from '../summary.ts' /** * Full BIDS schema validation entrypoint @@ -18,7 +19,6 @@ import { ValidationResult } from '../types/validation-result.ts' export async function validate(fileTree: FileTree): Promise { const issues = new DatasetIssues() // TODO - summary should be implemented in pure schema mode - const summary = {} const schema = await loadSchema() for await (const context of walkFileTree(fileTree, issues)) { // TODO - Skip ignored files for now (some tests may reference ignored files) @@ -33,9 +33,9 @@ export async function validate(fileTree: FileTree): Promise { checkLabelFormat(schema, context) } applyRules(schema, context) + updateSummary(context) } return { issues, - summary, } } From 3c60642464d1676159118cf4afab10c513f00462 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Wed, 29 Jun 2022 15:23:07 -0500 Subject: [PATCH 02/13] move over collectSubjectMetadata, get typescript to play well with new summary code. --- bids-validator/src/main.ts | 1 - bids-validator/src/schema/context.ts | 4 +- bids-validator/src/summary.ts | 72 ---------- .../src/summary/collectSubjectMetadata.ts | 71 ++++++++++ bids-validator/src/summary/summary.ts | 132 ++++++++++++++++++ bids-validator/src/tests/local/common.ts | 6 +- .../src/tests/local/valid_headers.test.ts | 2 +- bids-validator/src/types/validation-result.ts | 12 +- bids-validator/src/validators/bids.ts | 3 +- 9 files changed, 219 insertions(+), 84 deletions(-) delete mode 100644 bids-validator/src/summary.ts create mode 100644 bids-validator/src/summary/collectSubjectMetadata.ts create mode 100644 bids-validator/src/summary/summary.ts diff --git a/bids-validator/src/main.ts b/bids-validator/src/main.ts index 981ac0462..6e7748884 100644 --- a/bids-validator/src/main.ts +++ b/bids-validator/src/main.ts @@ -3,7 +3,6 @@ import { readFileTree } from './files/deno.ts' import { resolve } from './deps/path.ts' import { fullTestAdapter } from './compat/fulltest.ts' import { validate } from './validators/bids.ts' -import { summary } from './summary.ts' function inspect(obj: any) { console.log( diff --git a/bids-validator/src/schema/context.ts b/bids-validator/src/schema/context.ts index 7102af6a4..77ec8de22 100644 --- a/bids-validator/src/schema/context.ts +++ b/bids-validator/src/schema/context.ts @@ -17,7 +17,7 @@ export class BIDSContext implements Context { file: BIDSFile suffix: string extension: string - entities: object + entities: Record dataset: ContextDataset subject: ContextSubject datatype: string @@ -25,7 +25,7 @@ export class BIDSContext implements Context { sidecar: object associations: ContextAssociations columns: object - json: object + json: Record nifti_header: ContextNiftiHeader constructor(fileTree: FileTree, file: BIDSFile, issues: DatasetIssues) { diff --git a/bids-validator/src/summary.ts b/bids-validator/src/summary.ts deleted file mode 100644 index 388d23149..000000000 --- a/bids-validator/src/summary.ts +++ /dev/null @@ -1,72 +0,0 @@ -// import { Summary } from '../types/summary.ts' - -export const summary = { - dataProcessed: false, - totalFiles: -1, - size: 0, - sessions: new Set(), - subjects: new Set(), - subjectMetadata: {}, - tasks: new Set(), - modalities: { - mri: 0, - pet: 0, - meg: 0, - eeg: 0, - ieeg: 0, - microscopy: 0, - }, - secondaryModalities: { - MRI_Diffusion: 0, - MRI_Structural: 0, - MRI_Functional: 0, - MRI_Perfusion: 0, - PET_Static: 0, - PET_Dynamic: 0, - iEEG_ECoG: 0, - iEEG_SEEG: 0, - }, - pet: null, -} - -const modalityLookup = {} - -const secondaryLookup = { - dwi: 'MRI_Diffusion', - anat: 'MRI_Structural', - bold: 'MRI_Functional', - perf: 'MRI_Perfusion', -} - -export function updateSummary(context: Context): void { - if (context.file.path.startsWith('/derivatives')) { - return - } - - summary.totalFiles++ - summary.size += context.file.size - - if ('sub' in context.entities) { - summary.subjects.add(context.entities.sub) - } - if ('ses' in context.entities) { - summary.sesssions.add(context.entities.ses) - } - if ('task' in context.entities) { - summary.tasks.add(context.entities.task) - } - if (context.modality) { - summary.modalities[context.modality]++ - } - - if (context.datatype in secondaryLookup) { - const key = secondaryLookup[context.datatype] - secondary[key]++ - } else if (context.datatype === 'pet' && 'rec' in context.entities) { - if (['acstat', 'nacstat'].includes(context.entities.rec)) { - secondaty.PET_Static++ - } else if (['acdyn', 'nacdyn'].includes(context.entities.rec)) { - secondaty.PET_Dynamic++ - } - } -} diff --git a/bids-validator/src/summary/collectSubjectMetadata.ts b/bids-validator/src/summary/collectSubjectMetadata.ts new file mode 100644 index 000000000..41e728200 --- /dev/null +++ b/bids-validator/src/summary/collectSubjectMetadata.ts @@ -0,0 +1,71 @@ +import { SubjectMetadata } from '../types/validation-result.ts' +const PARTICIPANT_ID = 'participantId' +/** + * Go from tsv format string with participant_id as a required header to array of form + * [ + * { + * participantId: 'participant_id_1' + * foo: 'x', + * ... + * }, + * { + * participantId: 'participant_id_2' + * foo: 'y', + * ... + * } + * ... + * ] + * + * returns null if participant_id is not a header or file contents do not exist + * @param {string} participantsTsvContent + */ +export const collectSubjectMetadata = ( + participantsTsvContent: Uint8Array, +): SubjectMetadata[] => { + if (!participantsTsvContent) { + return [] + } + + const contentTable = new TextDecoder() + .decode(participantsTsvContent) + .split(/\r?\n/) + .filter((row) => row !== '') + .map((row) => row.split('\t')) + const [snakeCaseHeaders, ...subjectData] = contentTable + const headers = snakeCaseHeaders.map((header) => + header === 'participant_id' ? PARTICIPANT_ID : header, + ) + const targetKeys = [PARTICIPANT_ID, 'age', 'sex', 'group'] + .map((key) => ({ + key, + index: headers.findIndex((targetKey) => targetKey === key), + })) + .filter(({ index }) => index !== -1) + const participantIdKey = targetKeys.find(({ key }) => key === PARTICIPANT_ID) + const ageKey = targetKeys.find(({ key }) => key === 'age') + if (participantIdKey === undefined) return [] as SubjectMetadata[] + else + return subjectData + .map((data) => { + // this first map is for transforming any data coming out of participants.tsv: + // strip subject ids to match metadata.subjects: 'sub-01' -> '01' + data[participantIdKey.index] = data[participantIdKey.index].replace( + /^sub-/, + '', + ) + // make age an integer + // @ts-expect-error + if (ageKey) data[ageKey.index] = parseInt(data[ageKey.index]) + return data + }) + .map((data) => + //extract all target metadata for each subject + targetKeys.reduce( + (subject, { key, index }) => ({ + ...subject, + [key]: data[index], + }), + {}, + ), + ) as SubjectMetadata[] +} diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts new file mode 100644 index 000000000..d12a82fba --- /dev/null +++ b/bids-validator/src/summary/summary.ts @@ -0,0 +1,132 @@ +import { collectSubjectMetadata } from './collectSubjectMetadata.ts' +import { readAll, readerFromStreamReader } from '../deps/stream.ts' +import { Summary } from '../types/validation-result.ts' +import { BIDSContext } from '../schema/context.ts' + +const modalitiesCount: Record = { + mri: 0, + pet: 0, + meg: 0, + eeg: 0, + ieeg: 0, + microscopy: 0, +} + +const secondaryModalitiesCount: Record = { + MRI_Diffusion: 0, + MRI_Structural: 0, + MRI_Functional: 0, + MRI_Perfusion: 0, + PET_Static: 0, + PET_Dynamic: 0, + iEEG_ECoG: 0, + iEEG_SEEG: 0, +} + +const modalityPrettyLookup: Record = { + mri: 'MRI', + pet: 'PET', + meg: 'MEG', + eeg: 'EEG', + ieeg: 'iEEG', + micro: 'Microscopy', +} + +const secondaryLookup: Record = { + dwi: 'MRI_Diffusion', + anat: 'MRI_Structural', + bold: 'MRI_Functional', + perf: 'MRI_Perfusion', +} + +function computeModalities(modalities: Record): string[] { + // Order by matching file count + const nonZero = Object.keys(modalities).filter((a) => modalities[a] !== 0) + if (nonZero.length === 0) { + return [] + } + const sortedModalities = nonZero.sort((a, b) => { + if (modalities[b] === modalities[a]) { + // On a tie, hand it to the non-MRI modality + if (b === 'MRI') { + return -1 + } else { + return 0 + } + } + return modalities[b] - modalities[a] + }) + return sortedModalities.map((mod) => + mod in modalityPrettyLookup ? modalityPrettyLookup[mod] : mod, + ) +} + +function computeSecondaryModalities( + secondary: Record, +): string[] { + const nonZeroSecondary = Object.keys(secondary).filter( + (a) => secondary[a] !== 0, + ) + const sortedSecondary = nonZeroSecondary.sort( + (a, b) => secondary[b] - secondary[a], + ) + return sortedSecondary +} + +export const summary: Summary = { + dataProcessed: false, + totalFiles: -1, + size: 0, + sessions: new Set(), + subjects: new Set(), + subjectMetadata: [], + tasks: new Set(), + pet: {}, + get modalities() { + return computeModalities(modalitiesCount) + }, + get secondaryModalities() { + return computeSecondaryModalities(secondaryModalitiesCount) + }, +} + +export async function updateSummary(context: BIDSContext): Promise { + if (context.file.path.startsWith('/derivatives')) { + return + } + + summary.totalFiles++ + summary.size += await context.file.size + + if ('sub' in context.entities) { + summary.subjects.add(context.entities.sub) + } + if ('ses' in context.entities) { + summary.sessions.add(context.entities.ses) + } + if ('TaskName' in context.json) { + summary.tasks.add(context.json.TaskName) + } + if (context.modality) { + modalitiesCount[context.modality]++ + } + + if (context.datatype in secondaryLookup) { + const key = secondaryLookup[context.datatype] + secondaryModalitiesCount[key]++ + } else if (context.datatype === 'pet' && 'rec' in context.entities) { + if (['acstat', 'nacstat'].includes(context.entities.rec)) { + secondaryModalitiesCount.PET_Static++ + } else if (['acdyn', 'nacdyn'].includes(context.entities.rec)) { + secondaryModalitiesCount.PET_Dynamic++ + } + } + + if (context.file.path.includes('participants.tsv')) { + const stream = await context.file.stream + const streamReader = stream.getReader() + const denoReader = readerFromStreamReader(streamReader) + const fileBuffer = await readAll(denoReader) + summary.subjectMetadata = collectSubjectMetadata(fileBuffer) + } +} diff --git a/bids-validator/src/tests/local/common.ts b/bids-validator/src/tests/local/common.ts index 464508260..9f17f51aa 100644 --- a/bids-validator/src/tests/local/common.ts +++ b/bids-validator/src/tests/local/common.ts @@ -3,13 +3,17 @@ import { FileTree } from '../../types/filetree.ts' import { validate } from '../../validators/bids.ts' import { ValidationResult } from '../../types/validation-result.ts' import { DatasetIssues } from '../../issues/datasetIssues.ts' +import { summary } from '../../summary/summary.ts' export async function validatePath( t: Deno.TestContext, path: string, ): Promise<{ tree: FileTree; result: ValidationResult }> { let tree: FileTree = new FileTree('', '') - let result: ValidationResult = { issues: new DatasetIssues(), summary: {} } + let result: ValidationResult = { + issues: new DatasetIssues(), + summary: summary, + } await t.step('file tree is read', async () => { tree = await readFileTree(path) diff --git a/bids-validator/src/tests/local/valid_headers.test.ts b/bids-validator/src/tests/local/valid_headers.test.ts index d038ba6a8..bb86d5a22 100644 --- a/bids-validator/src/tests/local/valid_headers.test.ts +++ b/bids-validator/src/tests/local/valid_headers.test.ts @@ -19,7 +19,7 @@ Deno.test('valid_headers dataset', async (t) => { }) await t.step('summary has correct dataProcessed', () => { - assertEquals(result.summary.dataProcessed, ['rhyme judgment']) + assertEquals(result.summary.dataProcessed, false) }) await t.step('summary has correct dataProcessed', () => { diff --git a/bids-validator/src/types/validation-result.ts b/bids-validator/src/types/validation-result.ts index 4c5d8a2a0..8ce6e70f1 100644 --- a/bids-validator/src/types/validation-result.ts +++ b/bids-validator/src/types/validation-result.ts @@ -1,8 +1,8 @@ import { DatasetIssues } from '../issues/datasetIssues.ts' -interface SubjectMetadata { +export interface SubjectMetadata { PARTICIPANT_ID: string - age: string + age: number sex: string group: string } @@ -15,10 +15,10 @@ interface SubjectMetadata { */ export interface Summary { - sessions: string[] - subjects: string[] - subjectMetadata: SubjectMetaData[] - tasks: string[] + sessions: Set + subjects: Set + subjectMetadata: SubjectMetadata[] + tasks: Set modalities: string[] secondaryModalities: string[] totalFiles: number diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index 194680d88..69e1f5821 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -11,7 +11,7 @@ import { } from './filenames.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { ValidationResult } from '../types/validation-result.ts' -import { updateSummary } from '../summary.ts' +import { summary, updateSummary } from '../summary/summary.ts' /** * Full BIDS schema validation entrypoint @@ -37,5 +37,6 @@ export async function validate(fileTree: FileTree): Promise { } return { issues, + summary, } } From dd88ba1c176798ec470653abb79a69dcd1c96055 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Wed, 29 Jun 2022 16:20:25 -0500 Subject: [PATCH 03/13] await update to summary. --- bids-validator/src/validators/bids.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index 69e1f5821..80e1ded39 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -33,7 +33,7 @@ export async function validate(fileTree: FileTree): Promise { checkLabelFormat(schema, context) } applyRules(schema, context) - updateSummary(context) + await updateSummary(context) } return { issues, From 71bde4688dedd73e284b01b4006cc876e9b4031d Mon Sep 17 00:00:00 2001 From: Nell Hardcastle Date: Wed, 29 Jun 2022 13:00:14 -0700 Subject: [PATCH 04/13] fix: Fix minor issues with valid_headers tests --- bids-validator/src/tests/local/valid_headers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bids-validator/src/tests/local/valid_headers.test.ts b/bids-validator/src/tests/local/valid_headers.test.ts index bb86d5a22..cf05feadf 100644 --- a/bids-validator/src/tests/local/valid_headers.test.ts +++ b/bids-validator/src/tests/local/valid_headers.test.ts @@ -22,7 +22,7 @@ Deno.test('valid_headers dataset', async (t) => { assertEquals(result.summary.dataProcessed, false) }) - await t.step('summary has correct dataProcessed', () => { + await t.step('summary has correct modalities', () => { assertEquals(result.summary.modalities, ['MRI']) }) From c4025dac316afea63ba80d4cb39a551bbd398b3d Mon Sep 17 00:00:00 2001 From: Nell Hardcastle Date: Wed, 29 Jun 2022 14:38:04 -0700 Subject: [PATCH 05/13] feat: Add .text() method to BIDSFile for quickly accessing full text --- bids-validator/src/files/deno.test.ts | 7 ++- bids-validator/src/files/deno.ts | 46 ++++++++++++------- bids-validator/src/files/ignore.ts | 6 +-- .../src/issues/datasetIssues.test.ts | 11 +++-- bids-validator/src/schema/entities.test.ts | 5 +- bids-validator/src/tests/simple-dataset.ts | 27 +++++++---- bids-validator/src/types/file.ts | 6 ++- 7 files changed, 68 insertions(+), 40 deletions(-) diff --git a/bids-validator/src/files/deno.test.ts b/bids-validator/src/files/deno.test.ts index 5fb949a3d..3e6d71295 100644 --- a/bids-validator/src/files/deno.test.ts +++ b/bids-validator/src/files/deno.test.ts @@ -26,10 +26,15 @@ Deno.test('Deno implementation of BIDSFile', async (t) => { }) await t.step('can be read as ReadableStream', async () => { const file = new BIDSFileDeno(testDir, testFilename, ignore) - const stream = await file.stream + const stream = file.stream const streamReader = stream.getReader() const denoReader = readerFromStreamReader(streamReader) const fileBuffer = await readAll(denoReader) assertEquals(await file.size, fileBuffer.length) }) + await t.step('can be read with .text() method', async () => { + const file = new BIDSFileDeno(testDir, testFilename, ignore) + const text = await file.text() + assertEquals(await file.size, text.length) + }) }) diff --git a/bids-validator/src/files/deno.ts b/bids-validator/src/files/deno.ts index 3375e3cf4..de13285f2 100644 --- a/bids-validator/src/files/deno.ts +++ b/bids-validator/src/files/deno.ts @@ -14,7 +14,7 @@ export class BIDSFileDeno implements BIDSFile { #ignore: FileIgnoreRulesDeno name: string path: string - private _fileInfo: Deno.FileInfo | undefined + #fileInfo: Deno.FileInfo | undefined private _datasetAbsPath: string constructor(datasetPath: string, path: string, ignore: FileIgnoreRulesDeno) { @@ -28,30 +28,44 @@ export class BIDSFileDeno implements BIDSFile { return join(this._datasetAbsPath, this.path) } - private async _getSize(): Promise { - if (!this._fileInfo) { - this._fileInfo = await Deno.stat(this._getPath()) - } - return this._fileInfo.size - } - - get size(): Promise { - return this._getSize() + /** + * Deferred stat to get size + */ + get size(): number { + return ( + this.#fileInfo?.size || + (this.#fileInfo = Deno.statSync(this._getPath())).size + ) } - private async _getStream(): Promise> { + get stream(): ReadableStream { // Avoid asking for write access const openOptions = { read: true, write: false } - return (await Deno.open(this._getPath(), openOptions)).readable - } - - get stream(): Promise> { - return this._getStream() + return Deno.openSync(this._getPath(), openOptions).readable } get ignored(): boolean { return this.#ignore.test(this.path) } + + /** + * Read the entire file and decode as utf-8 text + */ + async text(): Promise { + const streamReader = this.stream + .pipeThrough(new TextDecoderStream('utf-8')) + .getReader() + let data = '' + try { + while (true) { + const { done, value } = await streamReader.read() + if (done) return data + data += value + } + } finally { + streamReader.releaseLock() + } + } } export class FileTreeDeno extends FileTree { diff --git a/bids-validator/src/files/ignore.ts b/bids-validator/src/files/ignore.ts index 96f49b33b..aa2ad2c20 100644 --- a/bids-validator/src/files/ignore.ts +++ b/bids-validator/src/files/ignore.ts @@ -3,11 +3,7 @@ import { ignore, Ignore } from '../deps/ignore.ts' import { FileIgnoreRules } from '../types/ignore.ts' export async function readBidsIgnore(file: BIDSFile) { - const fileStream = await file.stream - const reader = fileStream - .pipeThrough(new TextDecoderStream('utf-8')) - .getReader() - const { value } = await reader.read() + const value = await file.text() if (value) { const lines = value.split('\n') return lines diff --git a/bids-validator/src/issues/datasetIssues.test.ts b/bids-validator/src/issues/datasetIssues.test.ts index 94452952a..10f99c650 100644 --- a/bids-validator/src/issues/datasetIssues.test.ts +++ b/bids-validator/src/issues/datasetIssues.test.ts @@ -16,20 +16,23 @@ Deno.test('DatasetIssues management class', async (t) => { // This mostly tests the issueFile mapping function const issues = new DatasetIssues() const testStream = new ReadableStream() + const text = () => Promise.resolve('') const files = [ { + text, name: 'dataset_description.json', path: '/dataset_description.json', - size: Promise.resolve(500), + size: 500, ignored: false, - stream: Promise.resolve(testStream), + stream: testStream, } as BIDSFile, { + text, name: 'README', path: '/README', - size: Promise.resolve(500), + size: 500, ignored: false, - stream: Promise.resolve(testStream), + stream: testStream, line: 1, character: 5, severity: 'warning', diff --git a/bids-validator/src/schema/entities.test.ts b/bids-validator/src/schema/entities.test.ts index d5da7cee6..73516e0c7 100644 --- a/bids-validator/src/schema/entities.test.ts +++ b/bids-validator/src/schema/entities.test.ts @@ -6,9 +6,10 @@ Deno.test('test readEntities', async (t) => { const testFile = { name: 'task-rhymejudgment_bold.json', path: '/task-rhymejudgment_bold.json', - size: null as unknown as Promise, + size: null as unknown as number, ignored: false, - stream: null as unknown as Promise>, + stream: null as unknown as ReadableStream, + text: () => Promise.resolve(''), } const context = readEntities(testFile) assert(context.suffix === 'bold', 'failed to match suffix') diff --git a/bids-validator/src/tests/simple-dataset.ts b/bids-validator/src/tests/simple-dataset.ts index 659b2e011..0e10fdb09 100644 --- a/bids-validator/src/tests/simple-dataset.ts +++ b/bids-validator/src/tests/simple-dataset.ts @@ -1,48 +1,55 @@ import { FileTree } from '../types/filetree.ts' +const text = () => Promise.resolve('') + // Very basic dataset modeled for tests const rootFileTree = new FileTree('/', '') const subjectFileTree = new FileTree('/sub-01', 'sub-01', rootFileTree) const anatFileTree = new FileTree('/sub-01/anat', 'anat', subjectFileTree) anatFileTree.files = [ { + text, path: '/sub-01/anat/sub-01_T1w.nii.gz', name: 'sub-01_T1w.nii.gz', - size: Promise.resolve(311112), + size: 311112, ignored: false, - stream: Promise.resolve(new ReadableStream()), + stream: new ReadableStream(), }, ] subjectFileTree.files = [] subjectFileTree.directories = [anatFileTree] rootFileTree.files = [ { + text, path: '/dataset_description.json', name: 'dataset_description.json', - size: Promise.resolve(240), + size: 240, ignored: false, - stream: Promise.resolve(new ReadableStream()), + stream: new ReadableStream(), }, { + text, path: '/README', name: 'README', - size: Promise.resolve(709), + size: 709, ignored: false, - stream: Promise.resolve(new ReadableStream()), + stream: new ReadableStream(), }, { + text, path: '/CHANGES', name: 'CHANGES', - size: Promise.resolve(39), + size: 39, ignored: false, - stream: Promise.resolve(new ReadableStream()), + stream: new ReadableStream(), }, { + text, path: '/participants.tsv', name: 'participants.tsv', - size: Promise.resolve(36), + size: 36, ignored: false, - stream: Promise.resolve(new ReadableStream()), + stream: new ReadableStream(), }, ] rootFileTree.directories = [subjectFileTree] diff --git a/bids-validator/src/types/file.ts b/bids-validator/src/types/file.ts index 886ded9e3..8dee16392 100644 --- a/bids-validator/src/types/file.ts +++ b/bids-validator/src/types/file.ts @@ -9,9 +9,11 @@ export interface BIDSFile { // Dataset relative path for the file path: string // File size in bytes - size: Promise + size: number // BIDS ignore status of the file ignored: boolean // ReadableStream to file raw contents - stream: Promise> + stream: ReadableStream + // Resolve stream to decoded utf-8 text + text: () => Promise } From 7e5ee73dd2a331720c17c21072b4dd237b475a36 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Thu, 30 Jun 2022 13:27:25 -0500 Subject: [PATCH 06/13] use text() function for taskname and participants entries in summary --- bids-validator/src/schema/context.ts | 2 +- .../src/summary/collectSubjectMetadata.ts | 5 ++--- bids-validator/src/summary/summary.ts | 14 +++++++------- .../src/tests/local/valid_headers.test.ts | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bids-validator/src/schema/context.ts b/bids-validator/src/schema/context.ts index 77ec8de22..bca20d67b 100644 --- a/bids-validator/src/schema/context.ts +++ b/bids-validator/src/schema/context.ts @@ -42,8 +42,8 @@ export class BIDSContext implements Context { this.modality = '' this.sidecar = {} this.associations = {} as ContextAssociations - this.columns = {} this.json = {} + this.columns = {} this.nifti_header = {} as ContextNiftiHeader } diff --git a/bids-validator/src/summary/collectSubjectMetadata.ts b/bids-validator/src/summary/collectSubjectMetadata.ts index 41e728200..b5e4db934 100644 --- a/bids-validator/src/summary/collectSubjectMetadata.ts +++ b/bids-validator/src/summary/collectSubjectMetadata.ts @@ -20,14 +20,13 @@ const PARTICIPANT_ID = 'participantId' * @param {string} participantsTsvContent */ export const collectSubjectMetadata = ( - participantsTsvContent: Uint8Array, + participantsTsvContent: string, ): SubjectMetadata[] => { if (!participantsTsvContent) { return [] } - const contentTable = new TextDecoder() - .decode(participantsTsvContent) + const contentTable = participantsTsvContent .split(/\r?\n/) .filter((row) => row !== '') .map((row) => row.split('\t')) diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index d12a82fba..0a2a3a2ac 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -104,8 +104,11 @@ export async function updateSummary(context: BIDSContext): Promise { if ('ses' in context.entities) { summary.sessions.add(context.entities.ses) } - if ('TaskName' in context.json) { - summary.tasks.add(context.json.TaskName) + if (context.extension === '.json') { + const parsedJson = JSON.parse(await context.file.text()) + if ('TaskName' in parsedJson) { + summary.tasks.add(parsedJson.TaskName) + } } if (context.modality) { modalitiesCount[context.modality]++ @@ -123,10 +126,7 @@ export async function updateSummary(context: BIDSContext): Promise { } if (context.file.path.includes('participants.tsv')) { - const stream = await context.file.stream - const streamReader = stream.getReader() - const denoReader = readerFromStreamReader(streamReader) - const fileBuffer = await readAll(denoReader) - summary.subjectMetadata = collectSubjectMetadata(fileBuffer) + let tsvContents = await context.file.text() + summary.subjectMetadata = collectSubjectMetadata(tsvContents) } } diff --git a/bids-validator/src/tests/local/valid_headers.test.ts b/bids-validator/src/tests/local/valid_headers.test.ts index cf05feadf..de863c9ca 100644 --- a/bids-validator/src/tests/local/valid_headers.test.ts +++ b/bids-validator/src/tests/local/valid_headers.test.ts @@ -15,7 +15,7 @@ Deno.test('valid_headers dataset', async (t) => { }) await t.step('summary has correct tasks', () => { - assertEquals(result.summary.tasks, ['rhyme judgment']) + assertEquals(Array.from(result.summary.tasks), ['rhyme judgment']) }) await t.step('summary has correct dataProcessed', () => { From be14d463a401db536ddf98da95be48de5748bf0c Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Thu, 30 Jun 2022 13:44:43 -0500 Subject: [PATCH 07/13] add nellh's getter to parse json for context class --- bids-validator/src/schema/context.ts | 9 ++++++--- bids-validator/src/summary/summary.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bids-validator/src/schema/context.ts b/bids-validator/src/schema/context.ts index bca20d67b..1e80c4063 100644 --- a/bids-validator/src/schema/context.ts +++ b/bids-validator/src/schema/context.ts @@ -25,7 +25,6 @@ export class BIDSContext implements Context { sidecar: object associations: ContextAssociations columns: object - json: Record nifti_header: ContextNiftiHeader constructor(fileTree: FileTree, file: BIDSFile, issues: DatasetIssues) { @@ -42,11 +41,15 @@ export class BIDSContext implements Context { this.modality = '' this.sidecar = {} this.associations = {} as ContextAssociations - this.json = {} this.columns = {} this.nifti_header = {} as ContextNiftiHeader } - + get json(): Promise> { + return this.file + .text() + .then((text) => JSON.parse(text)) + .catch((error) => {}) + } get path(): string { return this.datasetPath } diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index 0a2a3a2ac..0f5a8464a 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -105,7 +105,7 @@ export async function updateSummary(context: BIDSContext): Promise { summary.sessions.add(context.entities.ses) } if (context.extension === '.json') { - const parsedJson = JSON.parse(await context.file.text()) + const parsedJson = await context.json if ('TaskName' in parsedJson) { summary.tasks.add(parsedJson.TaskName) } From 362ca76eb557d0f20047a55fe206462916e95295 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 1 Jul 2022 09:39:55 -0500 Subject: [PATCH 08/13] get summary modalities and secondary modalites going --- bids-validator/src/files/deno.ts | 4 ++-- bids-validator/src/main.ts | 2 +- bids-validator/src/summary/summary.ts | 15 +++++++++++++-- bids-validator/src/tests/local/common.ts | 4 ++-- bids-validator/src/types/validation-result.ts | 15 ++++++++++++++- bids-validator/src/validators/bids.ts | 4 ++-- bids-validator/src/validators/filenames.ts | 2 ++ 7 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bids-validator/src/files/deno.ts b/bids-validator/src/files/deno.ts index de13285f2..6f0bc35fa 100644 --- a/bids-validator/src/files/deno.ts +++ b/bids-validator/src/files/deno.ts @@ -93,7 +93,7 @@ export async function _readFileTree( const tree = new FileTreeDeno(relativePath, name, parent, rootPath) for await (const dirEntry of Deno.readDir(join(rootPath, relativePath))) { - if (dirEntry.isFile) { + if (dirEntry.isFile || dirEntry.isSymlink) { const file = new BIDSFileDeno( rootPath, join(relativePath, dirEntry.name), @@ -122,6 +122,6 @@ export async function _readFileTree( * Read in the target directory structure and return a FileTree */ export function readFileTree(rootPath: string): Promise { - const ignore = new FileIgnoreRulesDeno([]) + const ignore = new FileIgnoreRulesDeno(['.git**']) return _readFileTree(rootPath, '/', ignore) } diff --git a/bids-validator/src/main.ts b/bids-validator/src/main.ts index 6e7748884..ae407fa55 100644 --- a/bids-validator/src/main.ts +++ b/bids-validator/src/main.ts @@ -22,7 +22,7 @@ async function main() { const schemaResult = await validate(tree) if (options.schemaOnly) { - inspect(schemaResult.issues.issues) + inspect(schemaResult) // TODO - generate a summary without the old validator } else { const output = schemaResult.issues.formatOutput() diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index 0f5a8464a..52cb44bfd 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -1,6 +1,6 @@ import { collectSubjectMetadata } from './collectSubjectMetadata.ts' import { readAll, readerFromStreamReader } from '../deps/stream.ts' -import { Summary } from '../types/validation-result.ts' +import { Summary, SummaryOutput } from '../types/validation-result.ts' import { BIDSContext } from '../schema/context.ts' const modalitiesCount: Record = { @@ -35,7 +35,7 @@ const modalityPrettyLookup: Record = { const secondaryLookup: Record = { dwi: 'MRI_Diffusion', anat: 'MRI_Structural', - bold: 'MRI_Functional', + func: 'MRI_Functional', perf: 'MRI_Perfusion', } @@ -130,3 +130,14 @@ export async function updateSummary(context: BIDSContext): Promise { summary.subjectMetadata = collectSubjectMetadata(tsvContents) } } + +export function formatSummary(summary: Summary): SummaryOutput { + return { + ...summary, + sessions: Array.from(summary.sessions), + subjects: Array.from(summary.subjects), + tasks: Array.from(summary.tasks), + modalities: summary.modalities, + secondaryModalities: summary.secondaryModalities, + } +} diff --git a/bids-validator/src/tests/local/common.ts b/bids-validator/src/tests/local/common.ts index 9f17f51aa..ebe7d168a 100644 --- a/bids-validator/src/tests/local/common.ts +++ b/bids-validator/src/tests/local/common.ts @@ -3,7 +3,7 @@ import { FileTree } from '../../types/filetree.ts' import { validate } from '../../validators/bids.ts' import { ValidationResult } from '../../types/validation-result.ts' import { DatasetIssues } from '../../issues/datasetIssues.ts' -import { summary } from '../../summary/summary.ts' +import { summary, formatSummary } from '../../summary/summary.ts' export async function validatePath( t: Deno.TestContext, @@ -12,7 +12,7 @@ export async function validatePath( let tree: FileTree = new FileTree('', '') let result: ValidationResult = { issues: new DatasetIssues(), - summary: summary, + summary: formatSummary(summary), } await t.step('file tree is read', async () => { diff --git a/bids-validator/src/types/validation-result.ts b/bids-validator/src/types/validation-result.ts index 8ce6e70f1..d229bf40c 100644 --- a/bids-validator/src/types/validation-result.ts +++ b/bids-validator/src/types/validation-result.ts @@ -27,10 +27,23 @@ export interface Summary { pet: Record } +export interface SummaryOutput { + sessions: string[] + subjects: string[] + subjectMetadata: SubjectMetadata[] + tasks: string[] + modalities: string[] + secondaryModalities: string[] + totalFiles: number + size: number + dataProcessed: boolean + pet: Record +} + /** * The output of a validation run */ export interface ValidationResult { issues: DatasetIssues - summary: Summary + summary: SummaryOutput } diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index 80e1ded39..8e178dbd8 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -11,7 +11,7 @@ import { } from './filenames.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { ValidationResult } from '../types/validation-result.ts' -import { summary, updateSummary } from '../summary/summary.ts' +import { summary, formatSummary, updateSummary } from '../summary/summary.ts' /** * Full BIDS schema validation entrypoint @@ -37,6 +37,6 @@ export async function validate(fileTree: FileTree): Promise { } return { issues, - summary, + summary: formatSummary(summary), } } diff --git a/bids-validator/src/validators/filenames.ts b/bids-validator/src/validators/filenames.ts index 1880dc415..65a748022 100644 --- a/bids-validator/src/validators/filenames.ts +++ b/bids-validator/src/validators/filenames.ts @@ -13,6 +13,7 @@ export function checkDatatypes(schema: Schema, context: BIDSContext) { datatypeFromDirectory(schema, context) if (schema.rules.datatypes.hasOwnProperty(context.datatype)) { const rules = schema.rules.datatypes[context.datatype] + for (const key of Object.keys(rules)) { if (validateFilenameAgainstRule(rules[key], schema, context)) { matchedRule = key @@ -180,6 +181,7 @@ export function datatypeFromDirectory(schema: Schema, context: BIDSContext) { if (schema.rules.modalities[key].datatypes.includes(dirDatatype)) { context.modality = key context.datatype = dirDatatype + return } } From cb91bb50b71df99007109105beaf35fb8b3a7010 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 1 Jul 2022 10:42:59 -0500 Subject: [PATCH 09/13] convert summary and related functions to a class --- bids-validator/src/summary/summary.ts | 174 ++++++++++-------- bids-validator/src/tests/local/common.ts | 4 +- bids-validator/src/types/validation-result.ts | 14 +- bids-validator/src/validators/bids.ts | 6 +- 4 files changed, 104 insertions(+), 94 deletions(-) diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index 52cb44bfd..e6bc390d2 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -1,28 +1,8 @@ import { collectSubjectMetadata } from './collectSubjectMetadata.ts' import { readAll, readerFromStreamReader } from '../deps/stream.ts' -import { Summary, SummaryOutput } from '../types/validation-result.ts' +import { SummaryOutput, SubjectMetadata } from '../types/validation-result.ts' import { BIDSContext } from '../schema/context.ts' -const modalitiesCount: Record = { - mri: 0, - pet: 0, - meg: 0, - eeg: 0, - ieeg: 0, - microscopy: 0, -} - -const secondaryModalitiesCount: Record = { - MRI_Diffusion: 0, - MRI_Structural: 0, - MRI_Functional: 0, - MRI_Perfusion: 0, - PET_Static: 0, - PET_Dynamic: 0, - iEEG_ECoG: 0, - iEEG_SEEG: 0, -} - const modalityPrettyLookup: Record = { mri: 'MRI', pet: 'PET', @@ -73,71 +53,113 @@ function computeSecondaryModalities( return sortedSecondary } -export const summary: Summary = { - dataProcessed: false, - totalFiles: -1, - size: 0, - sessions: new Set(), - subjects: new Set(), - subjectMetadata: [], - tasks: new Set(), - pet: {}, +class Summary { + sessions: Set + subjects: Set + subjectMetadata: SubjectMetadata[] + tasks: Set + totalFiles: number + size: number + dataProcessed: boolean + pet: Record + modalitiesCount: Record + secondaryModalitiesCount: Record + datatypes: Set + constructor() { + this.dataProcessed = false + this.totalFiles = -1 + this.size = 0 + this.sessions = new Set() + this.subjects = new Set() + this.subjectMetadata = [] + this.tasks = new Set() + this.pet = {} + this.datatypes = new Set() + this.modalitiesCount = { + mri: 0, + pet: 0, + meg: 0, + eeg: 0, + ieeg: 0, + microscopy: 0, + } + this.secondaryModalitiesCount = { + MRI_Diffusion: 0, + MRI_Structural: 0, + MRI_Functional: 0, + MRI_Perfusion: 0, + PET_Static: 0, + PET_Dynamic: 0, + iEEG_ECoG: 0, + iEEG_SEEG: 0, + } + } get modalities() { - return computeModalities(modalitiesCount) - }, + return computeModalities(this.modalitiesCount) + } get secondaryModalities() { - return computeSecondaryModalities(secondaryModalitiesCount) - }, -} - -export async function updateSummary(context: BIDSContext): Promise { - if (context.file.path.startsWith('/derivatives')) { - return + return computeSecondaryModalities(this.secondaryModalitiesCount) } + async update(context: BIDSContext): Promise { + if (context.file.path.startsWith('/derivatives')) { + return + } - summary.totalFiles++ - summary.size += await context.file.size + this.totalFiles++ + this.size += await context.file.size - if ('sub' in context.entities) { - summary.subjects.add(context.entities.sub) - } - if ('ses' in context.entities) { - summary.sessions.add(context.entities.ses) - } - if (context.extension === '.json') { - const parsedJson = await context.json - if ('TaskName' in parsedJson) { - summary.tasks.add(parsedJson.TaskName) + if ('sub' in context.entities) { + this.subjects.add(context.entities.sub) + } + if ('ses' in context.entities) { + this.sessions.add(context.entities.ses) } - } - if (context.modality) { - modalitiesCount[context.modality]++ - } - if (context.datatype in secondaryLookup) { - const key = secondaryLookup[context.datatype] - secondaryModalitiesCount[key]++ - } else if (context.datatype === 'pet' && 'rec' in context.entities) { - if (['acstat', 'nacstat'].includes(context.entities.rec)) { - secondaryModalitiesCount.PET_Static++ - } else if (['acdyn', 'nacdyn'].includes(context.entities.rec)) { - secondaryModalitiesCount.PET_Dynamic++ + if (context.datatype) { + this.datatypes.add(context.datatype) + } + if (context.extension === '.json') { + const parsedJson = await context.json + if ('TaskName' in parsedJson) { + this.tasks.add(parsedJson.TaskName) + } + } + if (context.modality) { + this.modalitiesCount[context.modality]++ } - } - if (context.file.path.includes('participants.tsv')) { - let tsvContents = await context.file.text() - summary.subjectMetadata = collectSubjectMetadata(tsvContents) + if (context.datatype in secondaryLookup) { + const key = secondaryLookup[context.datatype] + this.secondaryModalitiesCount[key]++ + } else if (context.datatype === 'pet' && 'rec' in context.entities) { + if (['acstat', 'nacstat'].includes(context.entities.rec)) { + this.secondaryModalitiesCount.PET_Static++ + } else if (['acdyn', 'nacdyn'].includes(context.entities.rec)) { + this.secondaryModalitiesCount.PET_Dynamic++ + } + } + + if (context.file.path.includes('participants.tsv')) { + let tsvContents = await context.file.text() + this.subjectMetadata = collectSubjectMetadata(tsvContents) + } } -} -export function formatSummary(summary: Summary): SummaryOutput { - return { - ...summary, - sessions: Array.from(summary.sessions), - subjects: Array.from(summary.subjects), - tasks: Array.from(summary.tasks), - modalities: summary.modalities, - secondaryModalities: summary.secondaryModalities, + formatOutput(): SummaryOutput { + return { + sessions: Array.from(this.sessions), + subjects: Array.from(this.subjects), + subjectMetadata: this.subjectMetadata, + tasks: Array.from(this.tasks), + modalities: this.modalities, + secondaryModalities: this.secondaryModalities, + totalFiles: this.totalFiles, + size: this.size, + dataProcessed: this.dataProcessed, + pet: this.pet, + datatypes: Array.from(this.datatypes), + } } } + +export const summary = new Summary() diff --git a/bids-validator/src/tests/local/common.ts b/bids-validator/src/tests/local/common.ts index ebe7d168a..b6aa65317 100644 --- a/bids-validator/src/tests/local/common.ts +++ b/bids-validator/src/tests/local/common.ts @@ -3,7 +3,7 @@ import { FileTree } from '../../types/filetree.ts' import { validate } from '../../validators/bids.ts' import { ValidationResult } from '../../types/validation-result.ts' import { DatasetIssues } from '../../issues/datasetIssues.ts' -import { summary, formatSummary } from '../../summary/summary.ts' +import { summary } from '../../summary/summary.ts' export async function validatePath( t: Deno.TestContext, @@ -12,7 +12,7 @@ export async function validatePath( let tree: FileTree = new FileTree('', '') let result: ValidationResult = { issues: new DatasetIssues(), - summary: formatSummary(summary), + summary: summary.formatOutput(), } await t.step('file tree is read', async () => { diff --git a/bids-validator/src/types/validation-result.ts b/bids-validator/src/types/validation-result.ts index d229bf40c..0c7a113c0 100644 --- a/bids-validator/src/types/validation-result.ts +++ b/bids-validator/src/types/validation-result.ts @@ -14,19 +14,6 @@ export interface SubjectMetadata { TracerRadionuclide: {}, */ -export interface Summary { - sessions: Set - subjects: Set - subjectMetadata: SubjectMetadata[] - tasks: Set - modalities: string[] - secondaryModalities: string[] - totalFiles: number - size: number - dataProcessed: boolean - pet: Record -} - export interface SummaryOutput { sessions: string[] subjects: string[] @@ -38,6 +25,7 @@ export interface SummaryOutput { size: number dataProcessed: boolean pet: Record + datatypes: string[] } /** diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index 8e178dbd8..fc869eab5 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -11,7 +11,7 @@ import { } from './filenames.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { ValidationResult } from '../types/validation-result.ts' -import { summary, formatSummary, updateSummary } from '../summary/summary.ts' +import { summary } from '../summary/summary.ts' /** * Full BIDS schema validation entrypoint @@ -33,10 +33,10 @@ export async function validate(fileTree: FileTree): Promise { checkLabelFormat(schema, context) } applyRules(schema, context) - await updateSummary(context) + await summary.update(context) } return { issues, - summary: formatSummary(summary), + summary: summary.formatOutput(), } } From e01105c854b1be05615803d845beee04b6fb769a Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 1 Jul 2022 11:09:47 -0500 Subject: [PATCH 10/13] add test to check summary modality sorting works --- bids-validator/src/summary/summary.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 bids-validator/src/summary/summary.test.ts diff --git a/bids-validator/src/summary/summary.test.ts b/bids-validator/src/summary/summary.test.ts new file mode 100644 index 000000000..aacf91d3c --- /dev/null +++ b/bids-validator/src/summary/summary.test.ts @@ -0,0 +1,15 @@ +import { computeModalities, modalityPrettyLookup, Summary } from './summary.ts' +import { assertEquals, assertObjectMatch } from '../deps/asserts.ts' + +Deno.test('Summary class and helper functions', async (t) => { + await t.step('Constructor succeeds', () => { + new Summary() + }) + await t.step('computeModalities properly sorts modality counts', () => { + const modalitiesIn = { eeg: 5, mri: 6, pet: 6 } + const modalitiesOut = ['pet', 'mri', 'eeg'].map( + (x) => modalityPrettyLookup[x], + ) + assertEquals(computeModalities(modalitiesIn), modalitiesOut) + }) +}) From 28078bf9f58c76e8cc9a6c196d824ac743f208c6 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 1 Jul 2022 11:16:35 -0500 Subject: [PATCH 11/13] forgot to add changes to summary for tests to work --- bids-validator/src/summary/summary.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index e6bc390d2..530e561f2 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -3,7 +3,7 @@ import { readAll, readerFromStreamReader } from '../deps/stream.ts' import { SummaryOutput, SubjectMetadata } from '../types/validation-result.ts' import { BIDSContext } from '../schema/context.ts' -const modalityPrettyLookup: Record = { +export const modalityPrettyLookup: Record = { mri: 'MRI', pet: 'PET', meg: 'MEG', @@ -19,7 +19,9 @@ const secondaryLookup: Record = { perf: 'MRI_Perfusion', } -function computeModalities(modalities: Record): string[] { +export function computeModalities( + modalities: Record, +): string[] { // Order by matching file count const nonZero = Object.keys(modalities).filter((a) => modalities[a] !== 0) if (nonZero.length === 0) { @@ -28,7 +30,7 @@ function computeModalities(modalities: Record): string[] { const sortedModalities = nonZero.sort((a, b) => { if (modalities[b] === modalities[a]) { // On a tie, hand it to the non-MRI modality - if (b === 'MRI') { + if (b === 'mri') { return -1 } else { return 0 @@ -41,7 +43,7 @@ function computeModalities(modalities: Record): string[] { ) } -function computeSecondaryModalities( +export function computeSecondaryModalities( secondary: Record, ): string[] { const nonZeroSecondary = Object.keys(secondary).filter( @@ -53,7 +55,7 @@ function computeSecondaryModalities( return sortedSecondary } -class Summary { +export class Summary { sessions: Set subjects: Set subjectMetadata: SubjectMetadata[] From 0ce659ff8388950f0efcb223488b351be08a728a Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 1 Jul 2022 13:28:10 -0500 Subject: [PATCH 12/13] Set default ignore rules in ignores constructor. Use exported summary class instead of singleton. Add additional filenames test --- bids-validator/src/files/deno.ts | 2 +- bids-validator/src/files/ignore.ts | 3 ++ bids-validator/src/summary/summary.ts | 9 +++-- bids-validator/src/tests/local/common.ts | 3 +- bids-validator/src/validators/bids.ts | 3 +- .../src/validators/filenames.test.ts | 36 +++++++++++++++---- 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/bids-validator/src/files/deno.ts b/bids-validator/src/files/deno.ts index 6f0bc35fa..ee6509d40 100644 --- a/bids-validator/src/files/deno.ts +++ b/bids-validator/src/files/deno.ts @@ -122,6 +122,6 @@ export async function _readFileTree( * Read in the target directory structure and return a FileTree */ export function readFileTree(rootPath: string): Promise { - const ignore = new FileIgnoreRulesDeno(['.git**']) + const ignore = new FileIgnoreRulesDeno([]) return _readFileTree(rootPath, '/', ignore) } diff --git a/bids-validator/src/files/ignore.ts b/bids-validator/src/files/ignore.ts index aa2ad2c20..c02fce640 100644 --- a/bids-validator/src/files/ignore.ts +++ b/bids-validator/src/files/ignore.ts @@ -12,6 +12,8 @@ export async function readBidsIgnore(file: BIDSFile) { } } +const defaultIgnores = ['.git**', '.datalad'] + /** * Deno implementation of .bidsignore style rules */ @@ -20,6 +22,7 @@ export class FileIgnoreRulesDeno implements FileIgnoreRules { constructor(config: string[]) { this.#ignore = ignore({ allowRelativePaths: true }) + this.#ignore.add(defaultIgnores) this.#ignore.add(config) } diff --git a/bids-validator/src/summary/summary.ts b/bids-validator/src/summary/summary.ts index 530e561f2..4312ad769 100644 --- a/bids-validator/src/summary/summary.ts +++ b/bids-validator/src/summary/summary.ts @@ -117,9 +117,10 @@ export class Summary { this.sessions.add(context.entities.ses) } - if (context.datatype) { + if (context.datatype.length) { this.datatypes.add(context.datatype) } + if (context.extension === '.json') { const parsedJson = await context.json if ('TaskName' in parsedJson) { @@ -141,8 +142,8 @@ export class Summary { } } - if (context.file.path.includes('participants.tsv')) { - let tsvContents = await context.file.text() + if (context.file.path.endsWith('participants.tsv')) { + const tsvContents = await context.file.text() this.subjectMetadata = collectSubjectMetadata(tsvContents) } } @@ -163,5 +164,3 @@ export class Summary { } } } - -export const summary = new Summary() diff --git a/bids-validator/src/tests/local/common.ts b/bids-validator/src/tests/local/common.ts index b6aa65317..dc4baaa8e 100644 --- a/bids-validator/src/tests/local/common.ts +++ b/bids-validator/src/tests/local/common.ts @@ -3,13 +3,14 @@ import { FileTree } from '../../types/filetree.ts' import { validate } from '../../validators/bids.ts' import { ValidationResult } from '../../types/validation-result.ts' import { DatasetIssues } from '../../issues/datasetIssues.ts' -import { summary } from '../../summary/summary.ts' +import { Summary } from '../../summary/summary.ts' export async function validatePath( t: Deno.TestContext, path: string, ): Promise<{ tree: FileTree; result: ValidationResult }> { let tree: FileTree = new FileTree('', '') + let summary = new Summary() let result: ValidationResult = { issues: new DatasetIssues(), summary: summary.formatOutput(), diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index fc869eab5..54d89fa4e 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -11,13 +11,14 @@ import { } from './filenames.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { ValidationResult } from '../types/validation-result.ts' -import { summary } from '../summary/summary.ts' +import { Summary } from '../summary/summary.ts' /** * Full BIDS schema validation entrypoint */ export async function validate(fileTree: FileTree): Promise { const issues = new DatasetIssues() + const summary = new Summary() // TODO - summary should be implemented in pure schema mode const schema = await loadSchema() for await (const context of walkFileTree(fileTree, issues)) { diff --git a/bids-validator/src/validators/filenames.test.ts b/bids-validator/src/validators/filenames.test.ts index 12771b666..51c50349c 100644 --- a/bids-validator/src/validators/filenames.test.ts +++ b/bids-validator/src/validators/filenames.test.ts @@ -42,13 +42,35 @@ Deno.test('test datatypeFromDirectory', (t) => { }) }) -Deno.test('test checkDatatype', (t) => { - const filesToTest = [['/sub-01/func/sub-01_task-taskname_bold.json', []]] - filesToTest.map((test) => { - let context = newContext(test[0]) - context = { ...context, ...readEntities(context.file) } - checkDatatypes(schema, context) - assertEquals(context.issues.fileInIssues(test[0]), test[1]) +Deno.test('test checkDatatype', async (t) => { + await t.step('Check no errors on good file', () => { + const filesToTest = [['/sub-01/func/sub-01_task-taskname_bold.json', []]] + filesToTest.map((test) => { + let context = newContext(test[0]) + context = { ...context, ...readEntities(context.file) } + checkDatatypes(schema, context) + assertEquals(context.issues.fileInIssues(test[0]), test[1]) + }) + }) + + await t.step('Check for correct issues generated', () => { + const filesToTest = [ + ['/sub-01/anat/sub-01_task-taskname_bold.json', 'DATATYPE_MISMATCH'], + ['/sub-01/func/task-taskname_bold.json', 'MISSING_REQUIRED_ENTITY'], + [ + '/sub-01/func/sub-01_task-taskname_bad-ent_bold.json', + 'ENTITY_NOT_IN_RULE', + ], + ] + filesToTest.map((test) => { + let context = newContext(test[0]) + context = { ...context, ...readEntities(context.file) } + checkDatatypes(schema, context) + assertEquals( + context.issues.getFileIssueKeys(test[0]).includes(test[1]), + true, + ) + }) }) }) From ac2aa22a667e093c8fcd0560cf99a2c1e64da5a1 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 8 Jul 2022 12:16:07 -0500 Subject: [PATCH 13/13] pluralize datatype in checkdatatypes function. Reorder modalities in testing modality ranking in summary --- bids-validator/src/summary/summary.test.ts | 4 ++-- bids-validator/src/validators/filenames.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bids-validator/src/summary/summary.test.ts b/bids-validator/src/summary/summary.test.ts index aacf91d3c..9e0fbe886 100644 --- a/bids-validator/src/summary/summary.test.ts +++ b/bids-validator/src/summary/summary.test.ts @@ -6,8 +6,8 @@ Deno.test('Summary class and helper functions', async (t) => { new Summary() }) await t.step('computeModalities properly sorts modality counts', () => { - const modalitiesIn = { eeg: 5, mri: 6, pet: 6 } - const modalitiesOut = ['pet', 'mri', 'eeg'].map( + const modalitiesIn = { eeg: 5, pet: 6, mri: 6, ieeg: 6 } + const modalitiesOut = ['pet', 'ieeg', 'mri', 'eeg'].map( (x) => modalityPrettyLookup[x], ) assertEquals(computeModalities(modalitiesIn), modalitiesOut) diff --git a/bids-validator/src/validators/filenames.ts b/bids-validator/src/validators/filenames.ts index 65a748022..93496df25 100644 --- a/bids-validator/src/validators/filenames.ts +++ b/bids-validator/src/validators/filenames.ts @@ -34,7 +34,7 @@ export function checkDatatypes(schema: Schema, context: BIDSContext) { for (const key of Object.keys(rules)) { if (validateFilenameAgainstRule(rules[key], schema, context)) { matchedRule = key - possibleDatatypes.add(rules[key].datatype) + possibleDatatypes.add(rules[key].datatypes) break } }