diff --git a/VERSION.txt b/VERSION.txt index 6bf14c909..49a3b77d5 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.20261007.0 +1.20261008.0 diff --git a/src/sed/seddocument.cpp b/src/sed/seddocument.cpp index 6ae4bcef3..d6042f6ca 100644 --- a/src/sed/seddocument.cpp +++ b/src/sed/seddocument.cpp @@ -90,10 +90,17 @@ void SedDocument::Impl::initialiseFromSedmlFile(const SedDocumentPtr &pOwner, co void SedDocument::Impl::initialiseFromCombineArchive(const SedDocumentPtr &pOwner, const FilePtr &pFile) { - const auto &masterFile {pFile->pimpl()->mCombineArchive->masterFile()}; + const auto &combineArchive {pFile->pimpl()->mCombineArchive}; + const auto &masterFile {combineArchive->masterFile()}; if (masterFile == nullptr) { - addError("A simulation experiment description cannot be created using a COMBINE archive with no master file."); + if (combineArchive->sedmlFileCount() > 1) { + addError("A simulation experiment description cannot be created using a COMBINE archive with no master file and several SED-ML files."); + } else if (combineArchive->cellmlFileCount() > 1) { + addError("A simulation experiment description cannot be created using a COMBINE archive with no master file, no SED-ML file, and several CellML files."); + } else { + addError("A simulation experiment description cannot be created using a COMBINE archive with no master file."); + } } else { auto masterFileType {masterFile->type()}; diff --git a/src/support/combine/combinearchive.cpp b/src/support/combine/combinearchive.cpp index a629b89e3..a48d15a98 100644 --- a/src/support/combine/combinearchive.cpp +++ b/src/support/combine/combinearchive.cpp @@ -22,8 +22,30 @@ limitations under the License. #include "combine/combinearchive.h" #include "omex/CaContent.h" +#include + namespace libOpenCOR { +namespace { + +bool isCellmlFormat(const std::string &pFormat) +{ + // Note: we accept versioned formats (e.g., cellml.1.0 or cellml.2.0). + + static const std::regex CELLML_FORMAT_REGEX {R"(^http://identifiers\.org/combine\.specifications/cellml(\..*)?$)"}; + + return std::regex_match(pFormat, CELLML_FORMAT_REGEX); +} + +bool isSedmlFormat(const std::string &pFormat) +{ + static const std::string SEDML_FORMAT {"http://identifiers.org/combine.specifications/sed-ml"}; + + return pFormat == SEDML_FORMAT; +} + +} // namespace + CombineArchive::Impl::Impl(const FilePtr &pFile, libcombine::CombineArchive *pArchive, UnsignedChars &&pArchiveContents) : mArchiveContents(std::move(pArchiveContents)) , mArchive(pArchive) @@ -37,6 +59,9 @@ CombineArchive::Impl::Impl(const FilePtr &pFile, libcombine::CombineArchive *pAr mFiles.reserve(fileCount); mFileNames.reserve(fileCount); + FilePtr cellmlFile; + FilePtr sedmlFile; + for (int i {0}; i < mArchive->getNumEntries(); ++i) { const auto *entry {mArchive->getEntry(i)}; auto location {entry->getLocation()}; @@ -50,6 +75,50 @@ CombineArchive::Impl::Impl(const FilePtr &pFile, libcombine::CombineArchive *pAr if (entry->getMaster()) { mMasterFile = file; } + + // Determine the type of the file using its contents or, if they are not recognised (e.g., malformed XML), using + // the format specified in the manifest. This means that a broken SED-ML file is still considered to be a SED-ML + // file and, as such, cannot be silently ignored in favour of a CellML file. + + auto fileType {file->type()}; + + if (fileType == File::Type::UNKNOWN_FILE) { + const auto &format {entry->getFormat()}; + + if (isCellmlFormat(format)) { + fileType = File::Type::CELLML_FILE; + } else if (isSedmlFormat(format)) { + fileType = File::Type::SEDML_FILE; + } + } + + if (fileType == File::Type::CELLML_FILE) { + if (mCellmlFileCount == 0) { + cellmlFile = file; + } + + ++mCellmlFileCount; + } else if (fileType == File::Type::SEDML_FILE) { + if (mSedmlFileCount == 0) { + sedmlFile = file; + } + + ++mSedmlFileCount; + } + } + + // Determine the master file, if none was specified. + // Note: the master attribute is optional, so if no master file was specified then we consider the SED-ML file to be + // the master file, if there is only one SED-ML file, or the CellML file to be the master file, if there is no + // SED-ML file and only one CellML file. Otherwise, it is up to the user (or a tool like Web OpenCOR) to decide + // which file should be used. + + if (mMasterFile == nullptr) { + if (mSedmlFileCount == 1) { + mMasterFile = sedmlFile; + } else if ((mSedmlFileCount == 0) && (mCellmlFileCount == 1)) { + mMasterFile = cellmlFile; + } } } @@ -63,6 +132,16 @@ const FilePtr &CombineArchive::Impl::masterFile() const return mMasterFile; } +size_t CombineArchive::Impl::cellmlFileCount() const +{ + return mCellmlFileCount; +} + +size_t CombineArchive::Impl::sedmlFileCount() const +{ + return mSedmlFileCount; +} + bool CombineArchive::Impl::hasFiles() const { return mFiles.empty(); @@ -176,6 +255,16 @@ const FilePtr &CombineArchive::masterFile() const return pimpl()->masterFile(); } +size_t CombineArchive::cellmlFileCount() const +{ + return pimpl()->cellmlFileCount(); +} + +size_t CombineArchive::sedmlFileCount() const +{ + return pimpl()->sedmlFileCount(); +} + bool CombineArchive::hasFiles() const { return !pimpl()->hasFiles(); diff --git a/src/support/combine/combinearchive.h b/src/support/combine/combinearchive.h index 7c1cc09c1..6e18485ae 100644 --- a/src/support/combine/combinearchive.h +++ b/src/support/combine/combinearchive.h @@ -44,6 +44,8 @@ class LIBOPENCOR_UNIT_TESTING_EXPORT CombineArchive: public Logger static CombineArchivePtr create(const FilePtr &pFile); const FilePtr &masterFile() const; + size_t cellmlFileCount() const; + size_t sedmlFileCount() const; bool hasFiles() const; size_t fileCount() const; diff --git a/src/support/combine/combinearchive_p.h b/src/support/combine/combinearchive_p.h index e9ff1cb6b..520eae970 100644 --- a/src/support/combine/combinearchive_p.h +++ b/src/support/combine/combinearchive_p.h @@ -32,11 +32,15 @@ class CombineArchive::Impl: public Logger::Impl std::vector mFiles; std::vector mFileNames; FilePtr mMasterFile; + size_t mCellmlFileCount {0}; + size_t mSedmlFileCount {0}; explicit Impl(const FilePtr &pFile, libcombine::CombineArchive *pArchive, UnsignedChars &&pArchiveContents); ~Impl() override; const FilePtr &masterFile() const; + size_t cellmlFileCount() const; + size_t sedmlFileCount() const; bool hasFiles() const; size_t fileCount() const; const Strings &fileNames() const; diff --git a/tests/api/sed/basictests.cpp b/tests/api/sed/basictests.cpp index 6bbd9d87e..f109aeed3 100644 --- a/tests/api/sed/basictests.cpp +++ b/tests/api/sed/basictests.cpp @@ -107,13 +107,79 @@ TEST(BasicSedTest, combineArchiveWithNoManifestFile) EXPECT_EQ_ISSUES(document, EXPECTED_ISSUES); } -TEST(BasicSedTest, combineArchiveWithNoMasterFile) +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndOneCellmlFile) +{ + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_one_cellml_file.omex"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + + EXPECT_FALSE(document->hasIssues()); + EXPECT_EQ(std::dynamic_pointer_cast(document->simulations()[0])->outputEndTime(), 1000.0); + + auto instance {document->instantiate()}; + + instance->run(); + + EXPECT_FALSE(instance->hasIssues()); +} + +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndOneUnknownCellmlFile) { static const libOpenCOR::ExpectedIssues EXPECTED_ISSUES {{ - {libOpenCOR::Issue::Type::ERROR, "A simulation experiment description cannot be created using a COMBINE archive with no master file."}, + {libOpenCOR::Issue::Type::ERROR, "A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported)."}, + }}; + + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_one_unknown_cellml_file.omex"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + + EXPECT_EQ_ISSUES(document, EXPECTED_ISSUES); +} + +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndSeveralCellmlFiles) +{ + static const libOpenCOR::ExpectedIssues EXPECTED_ISSUES {{ + {libOpenCOR::Issue::Type::ERROR, "A simulation experiment description cannot be created using a COMBINE archive with no master file, no SED-ML file, and several CellML files."}, + }}; + + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_several_cellml_files.omex"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + + EXPECT_EQ_ISSUES(document, EXPECTED_ISSUES); +} + +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndOneSedmlFile) +{ + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_one_sedml_file.omex"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + + EXPECT_FALSE(document->hasIssues()); + EXPECT_EQ(std::dynamic_pointer_cast(document->simulations()[0])->outputEndTime(), 50.0); + + auto instance {document->instantiate()}; + + instance->run(); + + EXPECT_FALSE(instance->hasIssues()); +} + +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndOneUnknownSedmlFile) +{ + static const libOpenCOR::ExpectedIssues EXPECTED_ISSUES {{ + {libOpenCOR::Issue::Type::ERROR, "A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported)."}, + }}; + + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_one_unknown_sedml_file.omex"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + + EXPECT_EQ_ISSUES(document, EXPECTED_ISSUES); +} + +TEST(BasicSedTest, combineArchiveWithNoMasterFileAndSeveralSedmlFiles) +{ + static const libOpenCOR::ExpectedIssues EXPECTED_ISSUES {{ + {libOpenCOR::Issue::Type::ERROR, "A simulation experiment description cannot be created using a COMBINE archive with no master file and several SED-ML files."}, }}; - auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file.omex"))}; + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("api/sed/no_master_file_with_several_sedml_files.omex"))}; auto document {libOpenCOR::SedDocument::create(file)}; EXPECT_EQ_ISSUES(document, EXPECTED_ISSUES); diff --git a/tests/bindings/javascript/sed.basic.test.js b/tests/bindings/javascript/sed.basic.test.js index 69818b174..bc3a1bbb1 100644 --- a/tests/bindings/javascript/sed.basic.test.js +++ b/tests/bindings/javascript/sed.basic.test.js @@ -129,8 +129,25 @@ test.describe('Sed basic tests', () => { ]); }); - test('COMBINE archive with no master file', () => { - const file = new loc.File(utils.resourcePath('api/sed/no_master_file.omex')); + test('COMBINE archive with no master file and one CellML file', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_one_cellml_file.omex')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + + assert.strictEqual(document.hasIssues, false); + assert.strictEqual(document.simulations.get(0).outputEndTime, 1000); + + const instance = document.instantiate(); + + instance.run(); + + assert.strictEqual(instance.hasIssues, false); + }); + + test('COMBINE archive with no master file and one unknown CellML file', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_one_unknown_cellml_file.omex')); file.setContents(utils.fileContents(file.path)); @@ -139,7 +156,69 @@ test.describe('Sed basic tests', () => { assertIssues(loc, document, [ [ loc.Issue.Type.ERROR, - 'A simulation experiment description cannot be created using a COMBINE archive with no master file.' + 'A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported).' + ] + ]); + }); + + test('COMBINE archive with no master file and several CellML files', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_several_cellml_files.omex')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + + assertIssues(loc, document, [ + [ + loc.Issue.Type.ERROR, + 'A simulation experiment description cannot be created using a COMBINE archive with no master file, no SED-ML file, and several CellML files.' + ] + ]); + }); + + test('COMBINE archive with no master file and one SED-ML file', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_one_sedml_file.omex')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + + assert.strictEqual(document.hasIssues, false); + assert.strictEqual(document.simulations.get(0).outputEndTime, 50); + + const instance = document.instantiate(); + + instance.run(); + + assert.strictEqual(instance.hasIssues, false); + }); + + test('COMBINE archive with no master file and one unknown SED-ML file', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_one_unknown_sedml_file.omex')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + + assertIssues(loc, document, [ + [ + loc.Issue.Type.ERROR, + 'A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported).' + ] + ]); + }); + + test('COMBINE archive with no master file and several SED-ML files', () => { + const file = new loc.File(utils.resourcePath('api/sed/no_master_file_with_several_sedml_files.omex')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + + assertIssues(loc, document, [ + [ + loc.Issue.Type.ERROR, + 'A simulation experiment description cannot be created using a COMBINE archive with no master file and several SED-ML files.' ] ]); }); diff --git a/tests/bindings/python/test_sed_basic.py b/tests/bindings/python/test_sed_basic.py index 38a4b6c2b..b5606d24b 100644 --- a/tests/bindings/python/test_sed_basic.py +++ b/tests/bindings/python/test_sed_basic.py @@ -109,15 +109,97 @@ def test_combine_archive_with_no_manifest_file(): assert_issues(document, expected_issues) -def test_combine_archive_with_no_master_file(): +def test_combine_archive_with_no_master_file_and_one_cellml_file(): + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_one_cellml_file.omex") + ) + document = loc.SedDocument(file) + + assert not document.has_issues + assert document.simulations[0].output_end_time == 1000.0 + + instance = document.instantiate() + + instance.run() + + assert not instance.has_issues + + +def test_combine_archive_with_no_master_file_and_one_unknown_cellml_file(): expected_issues = [ [ loc.Issue.Type.Error, - "A simulation experiment description cannot be created using a COMBINE archive with no master file.", + "A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported).", + ], + ] + + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_one_unknown_cellml_file.omex") + ) + document = loc.SedDocument(file) + + assert_issues(document, expected_issues) + + +def test_combine_archive_with_no_master_file_and_several_cellml_files(): + expected_issues = [ + [ + loc.Issue.Type.Error, + "A simulation experiment description cannot be created using a COMBINE archive with no master file, no SED-ML file, and several CellML files.", + ], + ] + + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_several_cellml_files.omex") + ) + document = loc.SedDocument(file) + + assert_issues(document, expected_issues) + + +def test_combine_archive_with_no_master_file_and_one_sedml_file(): + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_one_sedml_file.omex") + ) + document = loc.SedDocument(file) + + assert not document.has_issues + assert document.simulations[0].output_end_time == 50.0 + + instance = document.instantiate() + + instance.run() + + assert not instance.has_issues + + +def test_combine_archive_with_no_master_file_and_one_unknown_sedml_file(): + expected_issues = [ + [ + loc.Issue.Type.Error, + "A simulation experiment description cannot be created using a COMBINE archive with an unknown master file (only CellML and SED-ML master files are supported).", + ], + ] + + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_one_unknown_sedml_file.omex") + ) + document = loc.SedDocument(file) + + assert_issues(document, expected_issues) + + +def test_combine_archive_with_no_master_file_and_several_sedml_files(): + expected_issues = [ + [ + loc.Issue.Type.Error, + "A simulation experiment description cannot be created using a COMBINE archive with no master file and several SED-ML files.", ], ] - file = loc.File(utils.resource_path("api/sed/no_master_file.omex")) + file = loc.File( + utils.resource_path("api/sed/no_master_file_with_several_sedml_files.omex") + ) document = loc.SedDocument(file) assert_issues(document, expected_issues) diff --git a/tests/res/api/sed/no_master_file.omex b/tests/res/api/sed/no_master_file.omex deleted file mode 100644 index 33efbd231..000000000 Binary files a/tests/res/api/sed/no_master_file.omex and /dev/null differ diff --git a/tests/res/api/sed/no_master_file_with_one_cellml_file.omex b/tests/res/api/sed/no_master_file_with_one_cellml_file.omex new file mode 100644 index 000000000..a575d6136 Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_one_cellml_file.omex differ diff --git a/tests/res/api/sed/no_master_file_with_one_sedml_file.omex b/tests/res/api/sed/no_master_file_with_one_sedml_file.omex new file mode 100644 index 000000000..bccefb42c Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_one_sedml_file.omex differ diff --git a/tests/res/api/sed/no_master_file_with_one_unknown_cellml_file.omex b/tests/res/api/sed/no_master_file_with_one_unknown_cellml_file.omex new file mode 100644 index 000000000..e22caa02d Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_one_unknown_cellml_file.omex differ diff --git a/tests/res/api/sed/no_master_file_with_one_unknown_sedml_file.omex b/tests/res/api/sed/no_master_file_with_one_unknown_sedml_file.omex new file mode 100644 index 000000000..f4357f2fe Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_one_unknown_sedml_file.omex differ diff --git a/tests/res/api/sed/no_master_file_with_several_cellml_files.omex b/tests/res/api/sed/no_master_file_with_several_cellml_files.omex new file mode 100644 index 000000000..c5f8fad13 Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_several_cellml_files.omex differ diff --git a/tests/res/api/sed/no_master_file_with_several_sedml_files.omex b/tests/res/api/sed/no_master_file_with_several_sedml_files.omex new file mode 100644 index 000000000..72888a658 Binary files /dev/null and b/tests/res/api/sed/no_master_file_with_several_sedml_files.omex differ