diff --git a/+bids/+internal/append_to_layout.m b/+bids/+internal/append_to_layout.m index b4da2aa7..fa6137ef 100644 --- a/+bids/+internal/append_to_layout.m +++ b/+bids/+internal/append_to_layout.m @@ -26,7 +26,7 @@ % Then reparse the file using the entity-label pairs defined in the schema. p = bids.internal.parse_filename(file); - idx = find_suffix_group(modality, p.suffix, schema); + idx = bids.schema.find_suffix_group(modality, p.suffix, schema); if ~isempty(schema) @@ -62,36 +62,3 @@ end end - -function idx = find_suffix_group(modality, suffix, schema) - - idx = []; - - if isempty(schema) - return - end - - % the following loop could probably be improved with some cellfun magic - % cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups) - for i = 1:size(schema.datatypes.(modality), 1) - - this_suffix_group = schema.datatypes.(modality)(i); - - % for CI - if iscell(this_suffix_group) - this_suffix_group = this_suffix_group{1}; - end - - if any(strcmp(suffix, this_suffix_group.suffixes)) - idx = i; - break - end - - end - - if isempty(idx) - warning('findSuffix:noMatchingSuffix', ... - 'No corresponding suffix in schema for %s for datatype %s', suffix, modality); - end - -end diff --git a/+bids/+internal/camel_case.m b/+bids/+internal/camel_case.m new file mode 100644 index 00000000..d23412c6 --- /dev/null +++ b/+bids/+internal/camel_case.m @@ -0,0 +1,28 @@ +function str = camel_case(str) + % + % Removes non alphanumeric characters and uppercase first letter of all + % words but the first + % + % USAGE:: + % + % str = camel_case(str) + % + % :param str: + % :type str: string + % + % :returns: + % :str: (string) returns the input with an upper case for first letter + % for all words but the first one (``camelCase``) and + % removes invalid characters (like spaces). + % + % + + % camel case: upper case for first letter for all words but the first one + spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); + str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); + + % remove invalid characters + [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); + str(unvalidCharacters) = []; + +end diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index c9eae9fe..0028766e 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -54,20 +54,27 @@ % identidy an eventual prefix to the file % and amends the sub entity accordingly p.prefix = ''; + if strfind(parts{1}, 'sub-') + tmp = regexp(parts{1}, '(sub-)', 'split'); p.prefix = tmp{1}; + if ~isempty(p.prefix) - entities = fieldnames(p.entities); + p.entities.sub = p.entities.([p.prefix 'sub']); p.entities = rmfield(p.entities, [p.prefix 'sub']); - % reorder entities to make sure that sub is the first one - entities{1} = 'sub'; - p.entities = orderfields(p.entities, entities); + + % reorder entities so that the 'sub' entity stays on top + entity_order = fieldnames(p.entities); + entity_order = entity_order([end, 1:end - 1]); + p.entities = orderfields(p.entities, entity_order); + end + end - % -Extra fields can be added to the structure and ordered specifically. + % Extra fields can be added to the structure and ordered specifically. if nargin == 2 for i = 1:numel(fields) p.entities = bids.internal.add_missing_field(p.entities, fields{i}); diff --git a/+bids/+schema/find_suffix_datatypes.m b/+bids/+schema/find_suffix_datatypes.m new file mode 100644 index 00000000..72b02afe --- /dev/null +++ b/+bids/+schema/find_suffix_datatypes.m @@ -0,0 +1,33 @@ +function datatypes = find_suffix_datatypes(suffix, schema) + % + % For a given suffix, returns all the possible datatypes that have this + % suffix. + % + % + % (C) Copyright 2021 BIDS-MATLAB developers + + datatypes = {}; + + if isempty(schema) + return + end + + datatypes_list = fieldnames(schema.datatypes); + + for i = 1:size(datatypes_list, 1) + + this_datatype = schema.datatypes.(datatypes_list{i}); + % for CI + if iscell(this_datatype) + this_datatype = this_datatype{1}; + end + + suffix_list = cat(1, this_datatype.suffixes); + + if any(ismember(suffix_list, suffix)) + datatypes{end + 1} = datatypes_list{i}; + end + + end + +end diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m new file mode 100644 index 00000000..643eb41f --- /dev/null +++ b/+bids/+schema/find_suffix_group.m @@ -0,0 +1,42 @@ +function idx = find_suffix_group(modality, suffix, schema, quiet) + % + % For a given sufffix and modality, this returns the "suffix group" this + % suffix belongs to + % + % + % (C) Copyright 2021 BIDS-MATLAB developers + + idx = []; + + if nargin < 4 || isempty(quiet) + quiet = true; + end + + if isempty(schema) + return + end + + % the following loop could probably be improved with some cellfun magic + % cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups) + for i = 1:size(schema.datatypes.(modality), 1) + + this_suffix_group = schema.datatypes.(modality)(i); + + % for CI + if iscell(this_suffix_group) + this_suffix_group = this_suffix_group{1}; + end + + if any(strcmp(suffix, this_suffix_group.suffixes)) + idx = i; + break + end + + end + + if isempty(idx) && ~quiet + warning('findSuffix:noMatchingSuffix', ... + 'No corresponding suffix in schema for %s for datatype %s', suffix, modality); + end + +end diff --git a/+bids/+schema/return_entities_for_suffix.m b/+bids/+schema/return_entities_for_suffix.m new file mode 100644 index 00000000..f985d34e --- /dev/null +++ b/+bids/+schema/return_entities_for_suffix.m @@ -0,0 +1,52 @@ +function [entities, is_required] = return_entities_for_suffix(suffix, schema, quiet) + % + % returns the list of entities for a given suffix + % + % (C) Copyright 2021 BIDS-MATLAB developers + + modalities = bids.schema.return_modality_groups(schema); + + for iModality = 1:numel(modalities) + + datatypes = schema.modalities.(modalities{iModality}).datatypes; + + for iDatatype = 1:numel(datatypes) + + idx = bids.schema.find_suffix_group(datatypes{iDatatype}, suffix, schema, quiet); + if ~isempty(idx) + this_datatype = datatypes{iDatatype}; + this_suffix_group = schema.datatypes.(this_datatype)(idx); + break + end + + end + + if ~isempty(idx) + is_required = check_if_required(this_suffix_group); + entities = bids.schema.return_modality_entities(this_suffix_group, schema); + break + end + + end + +end + +function is_required = check_if_required(this_suffix_group) + + % for CI + if iscell(this_suffix_group) + this_suffix_group = this_suffix_group{1}; + end + + entities = fieldnames(this_suffix_group.entities); + nb_entities = numel(entities); + + is_required = false(1, nb_entities); + + for i = 1:nb_entities + if strcmpi(this_suffix_group.entities.(entities{i}), 'required') + is_required(i) = true; + end + end + +end diff --git a/+bids/create_filename.m b/+bids/create_filename.m new file mode 100644 index 00000000..826ab7e9 --- /dev/null +++ b/+bids/create_filename.m @@ -0,0 +1,153 @@ +function [filename, pth, json] = create_filename(p, file) + % + % Creates a BIDS compatible filename and can be used to create new names to rename files + % + % USAGE:: + % + % [filename, pth, json] = bids.create_filename(p) + % + % :param p: specification of the filename to create, very similar to the output of + % ``bids.internal.parse_filename`` + % :type p: structure + % + % Content of ``p``: + % + % - ``p.suffix`` - required + % - ``p.ext`` - extension (default: ``p.ext = ''``) + % - ``p.entities`` - structure listing the entity-label pairs to compose the filename + % - ``p.prefix`` - prefex to prepend (default: ``p.prefix = ''``) + % - ``p.use_schema`` - bollean to check required entities for a given suffix, + % and reorder entities according to the BIDS schema. + % - ``p.entity_order`` - user specified order in which to arranges the entities + % in the filename. Overrides ``p.use_schema``. + % + % If no entity order is specified and the filename creation is not based on the BIDS + % schema, then the filename will be created by concatenating the entity-label pairs + % found in the content of ``p.entities``. + % + % USAGE:: + % + % [filename, pth, json] = bids.create_filename(p, file) + % + % :param file: file whose name has to be modified by the content of ``p``. + % :type file: string + % + % + % (C) Copyright 2021 BIDS-MATLAB developers + + default.use_schema = true; + default.entity_order = {}; + default.ext = ''; + p = bids.internal.match_structure_fields(p, default); + + if nargin > 1 + p = rename_file(p, file); + end + + if ~isfield(p, 'suffix') + error('We need at least a suffix to create a filename.'); + end + + default.prefix = ''; + p = bids.internal.match_structure_fields(p, default); + + entities = fieldnames(p.entities); + + [p, entities, is_required] = reorder_entities(p, entities); + + filename = ''; + for iEntity = 1:numel(entities) + + thisEntity = entities{iEntity}; + + if is_required(iEntity) && ... + (~isfield(p.entities, thisEntity) || isempty(p.entities.(thisEntity))) + errorStruct.identifier = 'bidsMatlab:requiredEntity'; + errorStruct.message = sprintf('The entity %s cannot not be empty for the suffix %s', ... + thisEntity, ... + p.suffix); + error(errorStruct); + end + + if isfield(p.entities, thisEntity) && ~isempty(p.entities.(thisEntity)) + thisLabel = bids.internal.camel_case(p.entities.(thisEntity)); + filename = [filename '_' thisEntity '-' thisLabel]; %#ok + end + + end + + % remove lead '_' + filename(1) = []; + + filename = [p.prefix, filename '_', p.suffix, p.ext]; + + pth = bids.create_path(filename); + + json = bids.derivatives_json(filename); + +end + +function parsed_file = rename_file(p, file) + + parsed_file = bids.internal.parse_filename(file); + + parsed_file.entity_order = p.entity_order; + parsed_file.use_schema = p.use_schema; + if isfield(p, 'prefix') + parsed_file.prefix = p.prefix; + end + + entities_to_change = fieldnames(p.entities); + + for iEntity = 1:numel(entities_to_change) + parsed_file.entities.(entities_to_change{iEntity}) = p.entities.(entities_to_change{iEntity}); + end + +end + +function [p, entities, is_required] = reorder_entities(p, entities) + % + % reorder entities by one of the following ways + % - user defined: p.entity_order + % - schema based: p.use_schema + % - order defined by entities order in p.entities + % + + if ~isempty(p.entity_order) + + if size(p.entity_order, 2) > 1 + p.entity_order = p.entity_order'; + end + + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); + is_required = false(size(entities)); + + elseif p.use_schema + + quiet = true; + + [p, is_required] = get_entity_order_from_schema(p, quiet); + + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); + + else + + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); + is_required = false(size(entities)); + + end + +end + +function [p, is_required] = get_entity_order_from_schema(p, quiet) + + schema = bids.schema.load_schema(p.use_schema); + [schema_entities, is_required] = bids.schema.return_entities_for_suffix(p.suffix, schema, quiet); + for i = 1:numel(schema_entities) + p.entity_order{i, 1} = schema_entities{i}; + end + +end diff --git a/+bids/create_path.m b/+bids/create_path.m new file mode 100644 index 00000000..6de39076 --- /dev/null +++ b/+bids/create_path.m @@ -0,0 +1,28 @@ +function pth = create_path(filename) + % + % Creates a relative path based on the content of a BIDS filename. + % + % If there is none, or more than one possibility for the datatype, the path will only + % be based on the sub and ses entitiy. + % + % (C) Copyright 2021 BIDS-MATLAB developers + + pth = ''; + + p = bids.internal.parse_filename(filename); + + if isfield(p.entities, 'sub') + pth = ['sub-' p.entities.sub]; + end + + if isfield(p.entities, 'ses') + pth = [pth, filesep, 'ses-', p.entities.ses]; + end + + schema = bids.schema.load_schema(); + datatypes = bids.schema.find_suffix_datatypes(p.suffix, schema); + if numel(datatypes) == 1 + pth = [pth, filesep, datatypes{1}]; + end + +end diff --git a/+bids/derivatives_json.m b/+bids/derivatives_json.m new file mode 100644 index 00000000..5faa4b8f --- /dev/null +++ b/+bids/derivatives_json.m @@ -0,0 +1,78 @@ +function json = derivatives_json(derivative_filename, force) + % + % Creates dummy content for a given BIDS derivative file. + % + % %% Common + % Description RECOMMENDED + % Sources OPTIONAL + % RawSources OPTIONAL + % SpatialReference REQUIRED if no space entity, or if non standard space RECOMMENDED otherwise + % + % %% preprocessed + % SkullStripped REQUIRED for preprocessed data + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + % + % %% Mask + % RawSources REQUIRED + % Type RECOMMENDED (Brain, Lesion, Face, ROI) + % Atlas REQUIRED if "label" entity + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + % + % %% Segmentation + % Manual OPTIONAL + % Atlas OPTIONAL + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + + if nargin < 2 + force = false; + end + + p = bids.internal.parse_filename(derivative_filename); + + json = struct('filename', '', 'content', ''); + + if force || ... + any(ismember(fieldnames(p.entities), {'desc', 'res', 'label', 'den', 'space'})) || ... + any(ismember(p.suffix, {'mask', 'dseg', 'probseg'})) + + content = struct('Description', 'RECOMMENDED'); + + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + %% entity related content + if any(ismember(fieldnames(p.entities), 'res')) + content.Resolution = {{ struct(p.entities.res, 'REQUIRED if "res" entity') }}; + end + + if any(ismember(fieldnames(p.entities), 'den')) + content.Density = {{ struct(p.entities.den, 'REQUIRED if "den" entity') }}; + end + + %% suffix related content + if any(ismember(p.suffix, {'dseg', 'probseg'})) + content.Manual = {{'OPTIONAL'}}; + content.Atlas = {{'OPTIONAL'}}; + end + + if any(ismember(p.suffix, {'mask'})) + content.RawSources = {{'REQUIRED'}}; + content.Atlas = {{'OPTIONAL'}}; + content.Type = {{'OPTIONAL'}}; + end + + % TODO + % preprocessed + % SkullStripped REQUIRED for preprocessed data + + json.content = content; + json.filename = strrep(derivative_filename, p.ext, '.json'); + + end + +end diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m new file mode 100644 index 00000000..af177dff --- /dev/null +++ b/tests/test_create_filename.m @@ -0,0 +1,114 @@ +function test_suite = test_create_filename %#ok<*STOUT> + + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; + +end + +function test_create_filename_derivatives() + + filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + + % Create filename + p.entities = struct('desc', 'preproc'); + p.use_schema = false; + + filename = bids.create_filename(p, filename); + + assertEqual(filename, 'wuasub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); + + % Same but remove prefix + + p.prefix = ''; + + filename = bids.create_filename(p, filename); + + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); + +end + +function test_create_filename_basic() + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + + [filename, pth] = bids.create_filename(p); + + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); + assertEqual(pth, fullfile('sub-01', 'ses-test', 'func')); + + %% Modify existing filename + p.entities = struct( ... + 'sub', '02', ... + 'task', 'new task'); + + filename = bids.create_filename(p, fullfile(pwd, filename)); + + assertEqual(filename, 'sub-02_ses-test_task-newTask_run-02_bold.nii'); + + %% Remove entity from filename + p.entities = struct('ses', ''); + + filename = bids.create_filename(p, filename); + + assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); + +end + +function test_create_filename_order() + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + p.entity_order = {'sub', 'run'}; + + filename = bids.create_filename(p); + + assertEqual(filename, 'sub-01_run-02_ses-test_task-faceRecognition_bold.nii'); + +end + +function test_create_filename_schema_based() + + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'run', '02', ... + 'sub', '01', ... + 'task', 'face recognition'); + p.use_schema = true; + + filename = bids.create_filename(p); + + assertEqual(filename, 'sub-01_task-faceRecognition_run-02_bold.nii'); + +end + +function test_create_filename_schema_error() + + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'run', '02', ... + 'sub', '01'); + p.use_schema = true; + + assertExceptionThrown( ... + @()bids.create_filename(p), ... + 'bidsMatlab:requiredEntity'); + +end diff --git a/tests/test_create_path.m b/tests/test_create_path.m new file mode 100644 index 00000000..27190414 --- /dev/null +++ b/tests/test_create_path.m @@ -0,0 +1,26 @@ +function test_suite = test_create_path %#ok<*STOUT> + + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; + +end + +function test_create_path_basic() + + filename = 'sub-01_ses-test_bold.nii'; + path = bids.create_path(filename); + assertEqual(path, fullfile('sub-01', 'ses-test', 'func')); + + % several modality possiblities for events + filename = 'sub-01_ses-test_task-test_events.tsv'; + path = bids.create_path(filename); + assertEqual(path, fullfile('sub-01', 'ses-test')); + + % filename = 'participants.tsv'; + % path = bids.create_path(filename); + % assertEqual(path, ''); + +end diff --git a/tests/test_derivatives_json.m b/tests/test_derivatives_json.m new file mode 100644 index 00000000..4e529b55 --- /dev/null +++ b/tests/test_derivatives_json.m @@ -0,0 +1,111 @@ +function test_suite = test_derivatives_json %#ok<*STOUT> + + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; + +end + +function test_derivatives_json_basic() + + %% not a derivative file + filename = 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + + json = bids.derivatives_json(filename); + + expected.filename = ''; + expected.content = ''; + + assertEqual(json, expected); + +end + +function test_derivatives_json_force() + + %% force to create default content + filename = 'sub-01_task-faceRecognition_bold.nii'; + force = true; + + json = bids.derivatives_json(filename, force); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + expected.content = content; + expected.filename = 'sub-01_task-faceRecognition_bold.json'; + + assertEqual(json, expected); + +end + +function test_derivatives_json_preproc() + + filename = 'sub-01_task-faceRecognition_res-hi_den-lo_desc-preproc_bold.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + content.Resolution = {{ struct('hi', 'REQUIRED if "res" entity') }}; + content.Density = {{ struct('lo', 'REQUIRED if "den" entity') }}; + + expected.content = content; + expected.filename = 'sub-01_task-faceRecognition_res-hi_den-lo_desc-preproc_bold.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end + +function test_derivatives_json_segmentation() + + filename = 'sub-01_desc-T1w_dseg.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + content.Manual = {{'OPTIONAL'}}; + content.Atlas = {{'OPTIONAL'}}; + + expected.content = content; + expected.filename = 'sub-01_desc-T1w_dseg.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end + +function test_derivatives_json_mask() + + filename = 'sub-01_mask.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'REQUIRED'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + content.Atlas = {{'OPTIONAL'}}; + content.Type = {{'OPTIONAL'}}; + + expected.content = content; + expected.filename = 'sub-01_mask.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end diff --git a/tests/test_find_suffix_datatypes.m b/tests/test_find_suffix_datatypes.m new file mode 100644 index 00000000..e461879a --- /dev/null +++ b/tests/test_find_suffix_datatypes.m @@ -0,0 +1,21 @@ +function test_suite = test_find_suffix_datatypes %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_find_suffix_datatypes_basic + + schema = bids.schema.load_schema(); + + suffix = 'bold'; + + datatypes = bids.schema.find_suffix_datatypes(suffix, schema); + + expected_output = {'func'}; + + assertEqual(datatypes, expected_output); + +end diff --git a/tests/test_return_entities_for_suffix.m b/tests/test_return_entities_for_suffix.m new file mode 100644 index 00000000..85389ff2 --- /dev/null +++ b/tests/test_return_entities_for_suffix.m @@ -0,0 +1,21 @@ +function test_suite = test_return_entities_for_suffix %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_return_entities_for_suffix_basic + + schema = bids.schema.load_schema(); + + quiet = true; + + entities = bids.schema.return_entities_for_suffix('bold', schema, quiet); + + expected_output = {'sub', 'ses', 'task', 'acq', 'ce', 'rec', 'dir', 'run', 'echo', 'part'}; + + assertEqual(entities, expected_output); + +end