diff --git a/bids/validator/bidsHedTsvValidator.js b/bids/validator/bidsHedTsvValidator.js index 231ea14b..74bf9da2 100644 --- a/bids/validator/bidsHedTsvValidator.js +++ b/bids/validator/bidsHedTsvValidator.js @@ -82,6 +82,10 @@ export class BidsHedTsvValidator { * @private */ _validateHedColumn() { + if (this.tsvFile.hedColumnHedStrings.length === 0) { + // no HED column strings to validate + return [] + } return this.tsvFile.hedColumnHedStrings.flatMap((hedString, rowIndexMinusTwo) => this._validateHedColumnString(hedString, rowIndexMinusTwo + 2), ) @@ -230,13 +234,14 @@ export class BidsHedTsvParser { */ _parseHedRows(tsvHedRows) { const hedStrings = [] - - tsvHedRows.forEach((row, index) => { - const hedString = this._parseHedRow(row, index + 2) - if (hedString !== null) { - hedStrings.push(hedString) - } - }) + if (tsvHedRows.size > 0) { + tsvHedRows.forEach((row, index) => { + const hedString = this._parseHedRow(row, index + 2) + if (hedString !== null) { + hedStrings.push(hedString) + } + }) + } return hedStrings } @@ -248,13 +253,15 @@ export class BidsHedTsvParser { * @private */ _mergeEventRows(rowStrings) { - const groupedTsvRows = groupBy(rowStrings, (rowString) => rowString.onset) - const sortedOnsetTimes = Array.from(groupedTsvRows.keys()).sort((a, b) => a - b) const eventStrings = [] - for (const onset of sortedOnsetTimes) { - const onsetRows = groupedTsvRows.get(onset) - const onsetEventString = new BidsTsvEvent(this.tsvFile, onsetRows) - eventStrings.push(onsetEventString) + if (rowStrings.length > 0) { + const groupedTsvRows = groupBy(rowStrings, (rowString) => rowString.onset) + const sortedOnsetTimes = Array.from(groupedTsvRows.keys()).sort((a, b) => a - b) + for (const onset of sortedOnsetTimes) { + const onsetRows = groupedTsvRows.get(onset) + const onsetEventString = new BidsTsvEvent(this.tsvFile, onsetRows) + eventStrings.push(onsetEventString) + } } return eventStrings } diff --git a/tests/bids.spec.js b/tests/bids.spec.js index 7ae2ccee..589cfa83 100644 --- a/tests/bids.spec.js +++ b/tests/bids.spec.js @@ -176,7 +176,15 @@ describe('BIDS datasets', () => { all_bad: new BidsDataset(badDatasets, []), } const expectedIssues = { - all_good: [], + all_good: [ + BidsHedIssue.fromHedIssue( + generateIssue('duplicateTag', { + tag: 'Boat', + }), + goodDatasets[0].file, + { tsvLine: 5 }, + ), + ], all_bad: [ // BidsHedIssue.fromHedIssue(generateIssue('invalidTag', { tag: 'Confused' }), badDatasets[0].file), BidsHedIssue.fromHedIssue(generateIssue('invalidTag', { tag: 'Confused' }), badDatasets[0].file), @@ -220,6 +228,13 @@ describe('BIDS datasets', () => { badDatasets[3].file, { tsvLine: 2 }, ), + BidsHedIssue.fromHedIssue( + generateIssue('invalidTopLevelTagGroupTag', { + tag: 'Duration/ferry s', + }), + badDatasets[3].file, + { tsvLine: 2 }, + ), BidsHedIssue.fromHedIssue( generateIssue('sidecarKeyMissing', { key: 'purple', diff --git a/tests/bidsErrorData.js b/tests/bidsErrorData.js new file mode 100644 index 00000000..90b0ffa7 --- /dev/null +++ b/tests/bidsErrorData.js @@ -0,0 +1,30 @@ +import { BidsHedIssue } from '../bids' +import { generateIssue } from '../common/issues/issues' + +export const errorBidsTests = [ + { + name: 'invalid-bids-datasets', + description: 'Who knows', + warning: false, + tests: [ + { + name: 'valid-sidecar-bad-tag-tsv', + explanation: 'Valid-sidecar, but invalid tsv', + schemaVersion: '8.3.0', + sidecar: { + event_code: { + HED: { + face: '(Red, Blue), (Green, (Yellow))', + }, + }, + }, + sidecarValid: true, + sidecarErrors: [], + eventsString: 'onset\tduration\tHED\n' + '7\t4\tBaloney', + eventsValid: false, + eventsErrors: [BidsHedIssue.fromHedIssue(generateIssue('invalidTag', {}), 'valid-sidecar-invalid-tsv.tsv')], + comboValid: false, + }, + ], + }, +] diff --git a/tests/bidsErrorTests.spec.js b/tests/bidsErrorTests.spec.js new file mode 100644 index 00000000..03527246 --- /dev/null +++ b/tests/bidsErrorTests.spec.js @@ -0,0 +1,132 @@ +import chai from 'chai' +const assert = chai.assert +import { beforeAll, describe, afterAll } from '@jest/globals' +import path from 'path' +import { BidsHedIssue } from '../bids/types/issues' +import { buildSchemas } from '../validator/schema/init' +import { SchemaSpec, SchemasSpec } from '../common/schema/types' +import { BidsDataset, BidsEventFile, BidsHedTsvValidator, BidsSidecar, BidsTsvFile } from '../bids' +import { generateIssue, IssueError } from '../common/issues/issues' + +import { HedStringTokenizerOriginal } from '../parser/tokenizerOriginal' +import { HedStringTokenizer } from '../parser/tokenizer' +import { passingBidsTests } from './bidsPassingData' +import { BidsHedTsvParser } from '../bids/validator/bidsHedTsvValidator' +import parseTSV from '../bids/tsvParser' +const fs = require('fs') + +const displayLog = process.env.DISPLAY_LOG === 'true' + +const skippedErrors = {} + +describe('HED tokenizer validation', () => { + const schemaMap = new Map([ + ['8.2.0', undefined], + ['8.3.0', undefined], + ]) + + const badLog = [] + let totalTests = 0 + let wrongErrors = 0 + let unexpectedErrors = 0 + + beforeAll(async () => { + const spec2 = new SchemaSpec('', '8.2.0', '', path.join(__dirname, '../tests/data/HED8.2.0.xml')) + const specs2 = new SchemasSpec().addSchemaSpec(spec2) + const schemas2 = await buildSchemas(specs2) + const spec3 = new SchemaSpec('', '8.3.0', '', path.join(__dirname, '../tests/data/HED8.3.0.xml')) + const specs3 = new SchemasSpec().addSchemaSpec(spec3) + const schemas3 = await buildSchemas(specs3) + schemaMap.set('8.2.0', schemas2) + schemaMap.set('8.3.0', schemas3) + }) + + afterAll(() => { + const outBad = path.join(__dirname, 'runLog.txt') + const summary = `Total tests:${totalTests} Unexpected errors:${unexpectedErrors}\n` + if (displayLog) { + fs.writeFileSync(outBad, summary + badLog.join('\n'), 'utf8') + } + }) + + describe('BIDS validation - validData', () => { + const badLog = [] + let totalTests = 0 + let unexpectedErrors = 0 + + beforeAll(async () => {}) + + afterAll(() => { + const outBad = path.join(__dirname, 'runLog.txt') + const summary = `Total tests:${totalTests} Unexpected errors:${unexpectedErrors}\n` + if (displayLog) { + fs.writeFileSync(outBad, summary + badLog.join('\n'), 'utf8') + } + }) + + describe.each(passingBidsTests)('$name : $description', ({ tests }) => { + let itemLog + + const assertErrors = function (header, issues, iLog) { + iLog.push(`${header}\n`) + totalTests += 1 + + let errors = [] + if (issues.length > 0) { + errors = issues.map((item) => item.hedIssue.hedCode) // list of hedCodes in the issues + } + const errorString = errors.join(',') + if (errors.length > 0) { + iLog.push(`---expected no errors but got errors [${errorString}]\n`) + unexpectedErrors += 1 + assert(errors.length === 0, `${header}---expected no errors but got errors [${errorString}]`) + } + } + + const validate = function (test, iLog) { + // Make sure that the schema is available + const header = `\n[${test.name} (Expect pass)]` + const thisSchema = schemaMap.get(test.schemaVersion) + assert(thisSchema !== undefined, `${test.schemaVersion} is not available in test ${test.name}`) + + // Validate the sidecar by itself + const sidecarName = test.name + '.json' + const bidsSidecar = new BidsSidecar('thisOne', test.sidecar, { relativePath: sidecarName, path: sidecarName }) + assert(bidsSidecar instanceof BidsSidecar, 'Test') + const sidecarIssues = bidsSidecar.validate(thisSchema) + assertErrors(header + ':Validating just the sidecar', sidecarIssues, iLog) + + // Parse the events file + const eventName = test.name + '.tsv' + const parsedTsv = parseTSV(test.eventsString) + assert(parsedTsv instanceof Map, `${eventName} cannot be parsed`) + + // Validate the events file by itself + const bidsTsv = new BidsTsvFile(test.name, parsedTsv, { relativePath: 'eventName' }, [], {}) + const validator = new BidsHedTsvValidator(bidsTsv, thisSchema) + validator.validate() + assertErrors(header + ':Parsing events alone', validator.issues, iLog) + + // Validate the events file with the sidecar + const bidsTsvSide = new BidsTsvFile(test.name, parsedTsv, { relativePath: 'eventName' }, [], bidsSidecar) + const validatorSide = new BidsHedTsvValidator(bidsTsvSide, thisSchema) + validatorSide.validate() + assertErrors(header + ':Parsing events with ', validatorSide.issues, iLog) + } + + beforeAll(async () => { + itemLog = [] + }) + + afterAll(() => { + badLog.push(itemLog.join('\n')) + }) + + if (tests && tests.length > 0) { + test.each(tests)('BIDS: %s ', (test) => { + validate(test, itemLog) + }) + } + }) + }) +}) diff --git a/tests/bidsTestData.js b/tests/bidsTestData.js new file mode 100644 index 00000000..71222435 --- /dev/null +++ b/tests/bidsTestData.js @@ -0,0 +1,73 @@ +import { BidsHedIssue } from '../bids' +import { generateIssue } from '../common/issues/issues' + +export const bidsTestData = [ + // { + // name: 'valid-bids-datasets', + // description: 'Who knows', + // tests: [ + // { + // name: 'no-hed-at-all', + // explanation: 'Neither the sidecar or tsv has HED', + // schemaVersion: '8.3.0', + // sidecar: { + // duration: { + // description: 'Duration of the event in seconds.', + // }, + // }, + // eventsString: 'onset\tduration\n' + '7\t4', + // sidecarOnlyErrors: [], + // eventsOnlyErrors: [], + // comboErrors: [] + // }, + // { + // name: 'only-header-in-tsv', + // explanation: 'TSV only has header and some extra white space', + // schemaVersion: '8.3.0', + // sidecar: { + // duration: { + // description: 'Duration of the event in seconds.', + // }, + // }, + // eventsString: 'onset\tduration\n', + // sidecarOnlyErrors: [], + // eventsOnlyErrors: [], + // comboErrors: [] + // }, + // ] + // }, + { + name: 'invalid-bids-datasets', + description: 'Who knows this', + tests: [ + { + name: 'valid-sidecar-bad-tag-tsv', + explanation: 'Valid-sidecar, but invalid tsv', + schemaVersion: '8.3.0', + sidecar: { + event_code: { + HED: { + face: '(Red, Blue), (Green, (Yellow))', + }, + }, + }, + eventsString: 'onset\tduration\tHED\n' + '7\t4\tBaloney', + sidecarOnlyErrors: [], + eventsOnlyErrors: [ + BidsHedIssue.fromHedIssue( + generateIssue('invalidTag', { tag: 'Baloney' }), + { relativePath: 'valid-sidecar-bad-tag-tsv.tsv' }, + { tsvLine: 2 }, + ), + ], + comboErrors: [ + BidsHedIssue.fromHedIssue( + generateIssue('invalidTag', { tag: 'Baloney' }), + { relativePath: 'valid-sidecar-bad-tag-tsv.tsv' }, + { tsvLine: 2 }, + ), + ], + }, + ], + }, +] diff --git a/tests/bidsTests.spec.js b/tests/bidsTests.spec.js new file mode 100644 index 00000000..b8dbf071 --- /dev/null +++ b/tests/bidsTests.spec.js @@ -0,0 +1,162 @@ +import chai from 'chai' +const assert = chai.assert +const difference = require('lodash/difference') +import { beforeAll, describe, afterAll } from '@jest/globals' +import path from 'path' +import { BidsHedIssue } from '../bids/types/issues' +import { buildSchemas } from '../validator/schema/init' +import { SchemaSpec, SchemasSpec } from '../common/schema/types' +import { BidsDataset, BidsEventFile, BidsHedTsvValidator, BidsSidecar, BidsTsvFile } from '../bids' +import { generateIssue, IssueError } from '../common/issues/issues' + +import { HedStringTokenizerOriginal } from '../parser/tokenizerOriginal' +import { HedStringTokenizer } from '../parser/tokenizer' +import { bidsTestData } from './bidsTestData' +import { BidsHedTsvParser } from '../bids/validator/bidsHedTsvValidator' +import parseTSV from '../bids/tsvParser' +const fs = require('fs') + +//const displayLog = process.env.DISPLAY_LOG === 'true' +const displayLog = true +const skippedErrors = {} + +// Return an array of hedCode values extracted from an issues list. +function extractHedCodes(issues) { + const errors = [] + for (const issue of issues) { + if (issue instanceof BidsHedIssue) { + errors.push(`${issue.hedIssue.hedCode}`) + } else { + errors.push(`${issue.hedCode}`) + } + } + return errors +} + +describe('BIDS validation', () => { + const schemaMap = new Map([ + ['8.2.0', undefined], + ['8.3.0', undefined], + ]) + + const badLog = [] + let totalTests + let wrongErrors + let missingErrors + + beforeAll(async () => { + const spec2 = new SchemaSpec('', '8.2.0', '', path.join(__dirname, '../tests/data/HED8.2.0.xml')) + const specs2 = new SchemasSpec().addSchemaSpec(spec2) + const schemas2 = await buildSchemas(specs2) + const spec3 = new SchemaSpec('', '8.3.0', '', path.join(__dirname, '../tests/data/HED8.3.0.xml')) + const specs3 = new SchemasSpec().addSchemaSpec(spec3) + const schemas3 = await buildSchemas(specs3) + schemaMap.set('8.2.0', schemas2) + schemaMap.set('8.3.0', schemas3) + totalTests = 0 + wrongErrors = 0 + missingErrors = 0 + }) + + afterAll(() => { + const outBad = path.join(__dirname, 'runLog.txt') + const summary = `Total tests:${totalTests} Wrong errors:${wrongErrors} MissingErrors:${missingErrors}\n` + if (displayLog) { + fs.writeFileSync(outBad, summary + badLog.join('\n'), 'utf8') + } + }) + + describe.each(bidsTestData)('$name : $description', ({ tests }) => { + let itemLog + + const assertErrors = function (test, type, expectedErrors, issues, iLog) { + const status = expectedErrors.length > 0 ? 'Expect fail' : 'Expect pass' + const header = `[${test.name}:${type}:](${status})` + const log = [header] + totalTests += 1 + + const errors = extractHedCodes(issues) + const errorString = errors.join(',') + if (errors.length > 0) { + log.push(`---has errors [${errorString}]`) + } + if (expectedErrors.length === 0 && errorString.length > 0) { + const hasErrors = `---expected no errors but got errors [${errorString}]` + log.push(hasErrors) + iLog.push(log.join('\n')) + wrongErrors += 1 + assert(errorString.length === 0, `${header}${hasErrors}]`) + return + } + const expectedErrorCodes = extractHedCodes(expectedErrors) + const wrong = difference(errors, expectedErrorCodes) + const missing = difference(expectedErrors, errors) + let errorMessage = '' + if (wrong.length > 0) { + errorMessage = `---received unexpected errors ${wrong.join(',')}\n` + wrongErrors += 1 + } + if (missing.length > 0) { + errorMessage = errorMessage + `---did not receive expected errors ${missing.join(',')}` + missingErrors += 1 + } + if (errorMessage.length > 0) { + log.push(errorMessage) + iLog.push(log.join('\n')) + } + assert.sameDeepMembers(issues, expectedErrors, header) + } + + const validate = function (test, iLog) { + // Make sure that the schema is available + const header = `\n[${test.name} (Expect pass)]` + iLog.push(header) + const thisSchema = schemaMap.get(test.schemaVersion) + assert(thisSchema !== undefined, `${test.schemaVersion} is not available in test ${test.name}`) + + // Validate the sidecar by itself + const sidecarName = test.name + '.json' + const bidsSidecar = new BidsSidecar('thisOne', test.sidecar, { relativePath: sidecarName, path: sidecarName }) + assert(bidsSidecar instanceof BidsSidecar, 'Test') + const sidecarIssues = bidsSidecar.validate(thisSchema) + assertErrors(test, 'Sidecar only', test.sidecarOnlyErrors, sidecarIssues, iLog) + + // Parse the events file + const eventName = test.name + '.tsv' + const parsedTsv = parseTSV(test.eventsString) + assert(parsedTsv instanceof Map, `${eventName} cannot be parsed`) + + // Validate the events file by itself + // const bidsTsv = new BidsTsvFile(test.name, parsedTsv, { relativePath: eventName }, [], {}) + // const validator = new BidsHedTsvValidator(bidsTsv, thisSchema) + // validator.validate() + // assertErrors(test, 'Events only', test.eventsOnlyErrors, validator.issues, iLog) + + // Validate the events file with the sidecar + const bidsTsvSide = new BidsTsvFile( + test.name, + parsedTsv, + { relativePath: eventName, path: eventName }, + [], + bidsSidecar, + ) + const validatorWithSide = new BidsHedTsvValidator(bidsTsvSide, thisSchema) + validatorWithSide.validate() + assertErrors(test, 'Events+side', test.comboErrors, validatorWithSide.issues, iLog) + } + + beforeAll(async () => { + itemLog = [] + }) + + afterAll(() => { + badLog.push(itemLog.join('\n')) + }) + + if (tests && tests.length > 0) { + test.each(tests)('$name $explanation ', (test) => { + validate(test, itemLog) + }) + } + }) +}) diff --git a/tests/runLog.txt b/tests/runLog.txt new file mode 100644 index 00000000..8aee2242 --- /dev/null +++ b/tests/runLog.txt @@ -0,0 +1,7 @@ +Total tests:5 Wrong errors:0 MissingErrors:0 + +[no-hed-at-all (Expect pass)] + +[only-header-in-tsv (Expect pass)] + +[valid-sidecar-bad-tag-tsv (Expect pass)] \ No newline at end of file diff --git a/tests/tockenizerErrorTests.spec.js b/tests/tockenizerErrorTests.spec.js index 5370f71b..b84ac6c2 100644 --- a/tests/tockenizerErrorTests.spec.js +++ b/tests/tockenizerErrorTests.spec.js @@ -89,7 +89,7 @@ describe('Tokenizer validation using JSON tests', () => { }) }) -describe.skip('Original tokenizer validation using JSON tests', () => { +describe('Original tokenizer validation using JSON tests', () => { const badLog = [] let totalTests = 0 let wrongErrors = 0 diff --git a/tests/tokenizerPassingTests.spec.js b/tests/tokenizerPassingTests.spec.js index 2115df42..c91ac41b 100644 --- a/tests/tokenizerPassingTests.spec.js +++ b/tests/tokenizerPassingTests.spec.js @@ -81,7 +81,7 @@ describe('HED tokenizer validation', () => { }) }) - describe.skip('Original tokenizer validation - validData', () => { + describe('Original tokenizer validation - validData', () => { const badLog = [] let totalTests = 0 let unexpectedErrors = 0