diff --git a/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h b/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h index d533b7b742db96e84f7605602f2ef0b99f84130e..010873195fe3b13635b66c32fd5b62dffac5e4b5 100644 --- a/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h +++ b/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h @@ -8,37 +8,58 @@ #include "MantidKernel/System.h" +#include <Poco/AutoPtr.h> #include <map> +#include <memory> #include <string> #include <vector> +namespace Poco::XML { +class Document; +} + namespace Mantid { +namespace Kernel { +class InternetHelper; +} + namespace DataHandling { namespace ISISJournal { /** - Defines functions to aid in fetching ISIS specific run information from - journal files + * ISISJournal: Helper class to aid in fetching ISIS specific run information + * from journal files */ -using ISISJournalTags = std::vector<std::string>; -using ISISJournalFilters = std::map<std::string, std::string>; -using ISISJournalData = std::map<std::string, std::string>; +class DLLExport ISISJournal { +public: + using RunData = std::map<std::string, std::string>; -std::vector<ISISJournalData> DLLExport -getRunDataFromFile(std::string const &fileContents, - ISISJournalTags const &tags = ISISJournalTags(), - ISISJournalFilters const &filters = ISISJournalFilters()); + ISISJournal(std::string const &instrument, std::string const &cycle, + std::unique_ptr<Kernel::InternetHelper> internetHelper = + std::make_unique<Kernel::InternetHelper>()); + virtual ~ISISJournal(); -std::vector<ISISJournalData> DLLExport -getRunData(std::string const &instrument, std::string const &cycle, - ISISJournalTags const &tags = ISISJournalTags(), - ISISJournalFilters const &filters = ISISJournalFilters()); + ISISJournal(ISISJournal const &rhs) = delete; + ISISJournal(ISISJournal &&rhs); + ISISJournal const &operator=(ISISJournal const &rhs) = delete; + ISISJournal &operator=(ISISJournal &&rhs); -std::vector<std::string> - DLLExport getCycleListFromFile(std::string const &fileContents); + /// Get the list of cycle names + std::vector<std::string> getCycleNames(); + /// Get data for runs that match the given filters + std::vector<RunData> + getRuns(std::vector<std::string> const &valuesToLookup = {}, + RunData const &filters = RunData()); -std::vector<std::string> DLLExport getCycleList(std::string const &instrument); +private: + std::unique_ptr<Kernel::InternetHelper> m_internetHelper; + std::string m_runsFileURL; + std::string m_indexFileURL; + Poco::AutoPtr<Poco::XML::Document> m_runsDocument; + Poco::AutoPtr<Poco::XML::Document> m_indexDocument; + std::string getURLContents(std::string const &url); +}; } // namespace ISISJournal } // namespace DataHandling } // namespace Mantid diff --git a/Framework/DataHandling/src/ISISJournal.cpp b/Framework/DataHandling/src/ISISJournal.cpp index e32583a2ea9df311574d30f62b78b29c3ab24ace..69cc30270cdee3f3bebd7c6f2d9e41625f9054a2 100644 --- a/Framework/DataHandling/src/ISISJournal.cpp +++ b/Framework/DataHandling/src/ISISJournal.cpp @@ -10,7 +10,6 @@ #include "MantidKernel/InternetHelper.h" #include "Poco/SAX/SAXException.h" -#include <Poco/AutoPtr.h> #include <Poco/DOM/DOMParser.h> #include <Poco/DOM/Document.h> #include <Poco/DOM/NodeFilter.h> @@ -34,59 +33,41 @@ using Poco::XML::NodeFilter; using Poco::XML::TreeWalker; namespace { -static constexpr const char *URL_PREFIX = "http://data.isis.rl.ac.uk/"; -static constexpr const char *INSTRUMENT_PREFIX = "journals/ndx"; -static constexpr const char *JOURNAL_INDEX_FILE = "main"; -static constexpr const char *JOURNAL_PREFIX = "journal_"; +static constexpr const char *URL_PREFIX = + "http://data.isis.rl.ac.uk/journals/ndx"; +static constexpr const char *INDEX_FILE_NAME = "main"; +static constexpr const char *JOURNAL_PREFIX = "/journal_"; static constexpr const char *JOURNAL_EXT = ".xml"; static constexpr const char *NXROOT_TAG = "NXroot"; static constexpr const char *NXENTRY_TAG = "NXentry"; static constexpr const char *JOURNAL_TAG = "journal"; static constexpr const char *FILE_TAG = "file"; -/* Construct the URL for a journal file containing run data for a particular +/** Construct the URL for a journal file containing run data for a particular * instrument and cycle, e.g. * http://data.isis.rl.ac.uk/journals/ndxinter/journal_19_4.xml + * @param instrument : the ISIS instrument name (case insensitive) + * @param name : the file name excluding the "journal_" prefix and extension, + * e.g. "19_4" for "journal_19_4.xml" + * @returns : the URL as a string */ -std::string constructRunsFileURL(std::string const &instrument, - std::string const &cycle) { - std::stringstream url; - url << URL_PREFIX << INSTRUMENT_PREFIX << instrument << "/" << JOURNAL_PREFIX - << cycle << JOURNAL_EXT; +std::string constructURL(std::string instrument, std::string const &name) { + boost::algorithm::to_lower(instrument); + std::ostringstream url; + url << URL_PREFIX << instrument << JOURNAL_PREFIX << name << JOURNAL_EXT; return url.str(); } -/* Construct the URL for the journal index file for a particular instrument, - * e.g. http://data.isis.rl.ac.uk/journals/ndxinter/journal_main.xml +/** Parse a given XML string into a Document object + * @param xmlString : the XML file contents as a string + * @returns : the XML Document + * @throws : if there was an error parsing the file */ -std::string constructIndexFileURL(std::string const &instrument) { - std::stringstream url; - url << URL_PREFIX << INSTRUMENT_PREFIX << instrument << "/" << JOURNAL_PREFIX - << JOURNAL_INDEX_FILE << JOURNAL_EXT; - return url.str(); -} - -/* Get the contents of a file at a given URL - */ -std::string getURLContents(std::string const &url) { - Kernel::InternetHelper inetHelper; - std::stringstream serverReply; - auto const statusCode = inetHelper.sendRequest(url, serverReply); - if (statusCode != Poco::Net::HTTPResponse::HTTP_OK) { - throw Kernel::Exception::InternetError( - std::string("Failed to access journal file: ") + - std::to_string(statusCode)); - } - return serverReply.str(); -} - -/* Parse a given XML file contents into a Document object - */ -Poco::AutoPtr<Document> parse(std::string const &fileContents) { +Poco::AutoPtr<Document> parse(std::string const &xmlString) { DOMParser domParser; Poco::AutoPtr<Document> xmldoc; try { - xmldoc = domParser.parseString(fileContents); + xmldoc = domParser.parseString(xmlString); } catch (Poco::XML::SAXParseException const &ex) { std::ostringstream msg; msg << "ISISJournal: Error parsing file: " << ex.what(); @@ -95,26 +76,30 @@ Poco::AutoPtr<Document> parse(std::string const &fileContents) { return xmldoc; } -/* Check the given element is not null and has the given name and throw if not +/** Check the given element is valid. + * @param expectedName : the node name that the element is expected to have + * @throws : if the element is null or does not match the expected name */ -void validateElement(Element *element, const char *expectedName) { +void throwIfNotValid(Element *element, const char *expectedName) { if (!element) { std::ostringstream msg; - msg << "ISISJournal::validateElement() - invalid element for '" - << NXROOT_TAG << "'\n"; + msg << "ISISJournal::throwIfNotValid() - invalid element for '" + << expectedName << "'\n"; throw std::invalid_argument(msg.str()); } if (element->nodeName() != expectedName) { std::ostringstream msg; - msg << "ISISJournal::validateElement() - Element tag does not match '" - << NXROOT_TAG << "'. Found " << element->nodeName() << "\n"; + msg << "ISISJournal::throwIfNotValid() - Element name does not match '" + << expectedName << "'. Found " << element->nodeName() << "\n"; throw std::invalid_argument(msg.str()); } } -/* Return the text value contained in the given node, or an empty string if it - * does not contain a text value. +/** Get the text contained in a node. + * @param node : the node + * @returns : the value contained in the child #text node, or an empty string if + * it does not contain a text value. */ std::string getTextValue(Node *node) { if (!node) @@ -130,12 +115,14 @@ std::string getTextValue(Node *node) { return std::string(); } -/* Check if an element matches a set of filter criteria. Checks for a child - * element with the filter name and checks that its value matches the given - * filter value. +/** Check if an element matches a set of filter criteria. + * @param element : the element to check + * @param filters : a map of names and values to check against + * @returns : true if, for all filters, the element has a child node with + * the filter name that contains text matching the filter value */ -bool matchesAllFilters(Element *element, ISISJournalFilters const &filters) { - for (auto filterKvp : filters) { +bool matchesAllFilters(Element *element, ISISJournal::RunData const &filters) { + for (auto const &filterKvp : filters) { auto const childElement = element->getChildElement(filterKvp.first); if (getTextValue(childElement) != filterKvp.second) return false; @@ -143,61 +130,87 @@ bool matchesAllFilters(Element *element, ISISJournalFilters const &filters) { return true; } -/* Extract a list of named "tag" values for the given element. Gets the text - * values of the child elements with the given names and returns a map of the - * tag name to the value. Also always adds the element name to the list so there - * is always a results for every run. +/** Utility function to create the run data for an element. + * @param element : the element to get run data for + * @returns : a map of name->value pairs initialised with the mandatory data + * that is returned for all runs (currently just the name attribute, e.g. + * "name": "INTER00013460") */ -ISISJournalData getTagsForNode(Element *element, ISISJournalTags const &tags) { - auto result = ISISJournalData{}; - // Add the run name - result["name"] = element->getAttribute("name"); - // Add any tags in the tags list - for (auto &tag : tags) - result[tag] = getTextValue(element->getChildElement(tag)); - return result; +ISISJournal::RunData createRunDataForElement(Element *element) { + return ISISJournal::RunData{{"name", element->getAttribute("name")}}; } -/* Extract a list of "tag" values for all child nodes in the given "root" or - * parent element. Here, a "tag" is a name for a child element in the element - * we're checking i.e. a grandchild of the root node. +/** Extract a list of named values for the given element. + * @param element : the element to extract values for + * @param valuesToLookup : a list of values to extract (which may be empty if + * nothing is requested) + * @param result : a map of names to values to which we add key-value pairs for + * each requested value */ -std::vector<ISISJournalData> -getTagsForAllNodes(Element *root, ISISJournalTags const &tags, - ISISJournalFilters const &filters) { - auto results = std::vector<ISISJournalData>{}; +void addValuesForElement(Element *element, + std::vector<std::string> const &valuesToLookup, + ISISJournal::RunData &result) { + for (auto &name : valuesToLookup) + result[name] = getTextValue(element->getChildElement(name)); +} - auto nodeIter = TreeWalker(root, NodeFilter::SHOW_ELEMENT); +/** Extract a list of named values for all child nodes in the given parent + * element. + * @param parentElement : the element containing all child nodes we want to + * check + * @param valuesToLookup : the names of the values to extract from within the + * child nodes + * @param filters : a map of names and values to filter the results by + * @returns : a map of name->value pairs containing mandatory run data as well + * as the values that were requested + */ +std::vector<ISISJournal::RunData> +getValuesForAllElements(Element *parentElement, + std::vector<std::string> const &valuesToLookup, + ISISJournal::RunData const &filters) { + auto results = std::vector<ISISJournal::RunData>{}; + + auto nodeIter = TreeWalker(parentElement, NodeFilter::SHOW_ELEMENT); for (auto node = nodeIter.nextNode(); node; node = nodeIter.nextSibling()) { auto element = dynamic_cast<Element *>(node); - validateElement(element, NXENTRY_TAG); - if (matchesAllFilters(element, filters)) - results.emplace_back(getTagsForNode(element, tags)); + throwIfNotValid(element, NXENTRY_TAG); + + if (matchesAllFilters(element, filters)) { + auto result = createRunDataForElement(element); + addValuesForElement(element, valuesToLookup, result); + results.emplace_back(std::move(result)); + } } return results; } -/* Get the text values for all direct child elements of a given parent - * element. The child elements should all have the given tag name - throws if - * not. +/** Extract an attribute value for all direct children of a given element. + * @param parentElement : the element containing the child nodes to check + * @param childElementName : the name that the child elements should have + * @returns : the attribute values for all of the child elements + * @throws : if any of the child elements does not have the expected name */ -std::vector<std::string> getTagValuesForChildElements(Element *root, - const char *tag) { +std::vector<std::string> +getAttributeForAllChildElements(Element *parentElement, + const char *childElementName, + const char *attributeName) { auto results = std::vector<std::string>{}; - auto nodeIter = TreeWalker(root, NodeFilter::SHOW_ELEMENT); + auto nodeIter = TreeWalker(parentElement, NodeFilter::SHOW_ELEMENT); for (auto node = nodeIter.nextNode(); node; node = nodeIter.nextSibling()) { auto element = dynamic_cast<Element *>(node); - validateElement(element, tag); - results.emplace_back(element->getAttribute("name")); + throwIfNotValid(element, childElementName); + results.emplace_back(element->getAttribute(attributeName)); } return results; } -/* Convert a cycle filename to the cycle name or return an empty string if it - * doesn't match the required pattern +/** Convert a cycle filename to the cycle name + * @param filename : a filename e.g. journal_19_4.xml + * @return the cycle name e.g. 19_4, or an empty string if the name + * does not match the required pattern */ std::string convertFilenameToCycleName(std::string const &filename) { boost::regex pattern("[0-9]+_[0-9]+"); @@ -210,9 +223,11 @@ std::string convertFilenameToCycleName(std::string const &filename) { return std::string(); } -/* Convert a list of cycle filenames to a list of cycle names e.g. - * journal_19_4.xml -> 19_4. Also exlcudes files from the list if they do not - * match the required pattern. +/** Convert a list of cycle filenames to a list of cycle names. + * @param filenames : the list of filenames e.g. journal_main.xml, + * journal_19_4.xml + * @returns : the list of cycle names for all valid journal files e.g. 19_4 + * (note that this excludes files that do not match the journal-file pattern). */ std::vector<std::string> convertFilenamesToCycleNames(std::vector<std::string> const &filenames) { @@ -227,62 +242,78 @@ convertFilenamesToCycleNames(std::vector<std::string> const &filenames) { } } // namespace -/** Get specified data from a journal file for all runs that match given filter - * criteria. - * - * @param fileContents : the XML journal file contents - * @param tags : the tag names of the required values to be returned e.g. - * "run_number" - * @param filters : optional tag names and values to filter the results by +/** Construct the journal class for a specific instrument and cycle + * @param instrument : the ISIS instrument name to request data for e.g. "INTER" + * (case insensitive) + * @param cycle : the ISIS cycle the required data is from e.g. "19_4" + * @param internetHelper : class for sending internet requests */ -std::vector<ISISJournalData> -getRunDataFromFile(std::string const &fileContents, ISISJournalTags const &tags, - ISISJournalFilters const &filters) { - auto xmldoc = parse(fileContents); - auto root = xmldoc->documentElement(); - validateElement(root, NXROOT_TAG); - return getTagsForAllNodes(root, tags, filters); -} +ISISJournal::ISISJournal(std::string const &instrument, + std::string const &cycle, + std::unique_ptr<InternetHelper> internetHelper) + : m_internetHelper(std::move(internetHelper)), + m_runsFileURL(constructURL(instrument, cycle)), + m_indexFileURL(constructURL(instrument, INDEX_FILE_NAME)) {} + +ISISJournal::~ISISJournal() = default; + +ISISJournal::ISISJournal(ISISJournal &&rhs) = default; + +ISISJournal &ISISJournal::operator=(ISISJournal &&rhs) = default; -/** Get specified data for all runs in a specific instrument and cycle that - * match given filter criteria. +/** Get the cycle names * - * @param instrument : the instrument to request data for - * @param cycle : the ISIS cycle the required data is from e.g. "19_4" - * @param tags : the tag names of the required values to be returned e.g. - * "run_number" - * @param filters : optional tag names and values to filter the results by + * @returns : a list of all ISIS cycle names for the instrument + * @throws : if there was an error fetching the runs */ -std::vector<ISISJournalData> getRunData(std::string const &instrument, - std::string const &cycle, - ISISJournalTags const &tags, - ISISJournalFilters const &filters) { - auto const url = constructRunsFileURL(instrument, cycle); - auto fileContents = getURLContents(url); - return getRunDataFromFile(fileContents, tags, filters); +std::vector<std::string> ISISJournal::getCycleNames() { + if (!m_indexDocument) { + auto xmlString = getURLContents(m_indexFileURL); + m_indexDocument = parse(xmlString); + } + auto rootElement = m_indexDocument->documentElement(); + throwIfNotValid(rootElement, JOURNAL_TAG); + auto filenames = + getAttributeForAllChildElements(rootElement, FILE_TAG, "name"); + return convertFilenamesToCycleNames(filenames); } -/** Get the list of cycle names from the given index file - * @param fileContents : the contents of the XML index file - * @returns : a list of cycle names +/** Get run names and other specified data for all runs that match the given + * filter criteria. + * + * @param valuesToLookup : optional list of additional values to be returned + * e.g. "run_number", "title" + * @param filters : optional element names and values to filter the results by + * @throws : if there was an error fetching the runs */ -std::vector<std::string> getCycleListFromFile(std::string const &fileContents) { - auto xmldoc = parse(fileContents); - auto root = xmldoc->documentElement(); - validateElement(root, JOURNAL_TAG); - auto filenames = getTagValuesForChildElements(root, FILE_TAG); - return convertFilenamesToCycleNames(filenames); +std::vector<ISISJournal::RunData> +ISISJournal::getRuns(std::vector<std::string> const &valuesToLookup, + ISISJournal::RunData const &filters) { + if (!m_runsDocument) { + auto xmlString = getURLContents(m_runsFileURL); + m_runsDocument = parse(xmlString); + } + auto rootElement = m_runsDocument->documentElement(); + throwIfNotValid(rootElement, NXROOT_TAG); + return getValuesForAllElements(rootElement, valuesToLookup, filters); } -/** Get the list of cycle names for the given instrument - * @param instrument : the instrument name - * @returns : a list of cycle names +/** Get the contents of a file at a given URL + * @param url : the URL to fetch + * @returns : the contents of the file as a string + * @throws : if there was an error fetching the file */ -std::vector<std::string> getCycleList(std::string const &instrument) { - auto const url = constructIndexFileURL(instrument); - auto fileContents = getURLContents(url); - return getCycleList(fileContents); +std::string ISISJournal::getURLContents(std::string const &url) { + std::ostringstream serverReply; + auto const statusCode = m_internetHelper->sendRequest(url, serverReply); + if (statusCode != Poco::Net::HTTPResponse::HTTP_OK) { + throw Kernel::Exception::InternetError( + std::string("Failed to access journal file: HTTP Code: ") + + std::to_string(statusCode)); + } + return serverReply.str(); } + } // namespace ISISJournal } // namespace DataHandling } // namespace Mantid diff --git a/Framework/DataHandling/test/CMakeLists.txt b/Framework/DataHandling/test/CMakeLists.txt index 75f98634a22221d34ffad2469c2c323e8ec7fb05..7bffa2e35f9c7f2e3d4920b7ea96de0ddbc52fa7 100644 --- a/Framework/DataHandling/test/CMakeLists.txt +++ b/Framework/DataHandling/test/CMakeLists.txt @@ -33,7 +33,8 @@ if(CXXTEST_FOUND) HistogramData ${NEXUS_LIBRARIES} ${HDF5_LIBRARIES} - ${HDF5_HL_LIBRARIES}) + ${HDF5_HL_LIBRARIES} + gmock) add_dependencies(DataHandlingTest Algorithms MDAlgorithms) add_dependencies(FrameworkTests DataHandlingTest) # Test data diff --git a/Framework/DataHandling/test/ISISJournalTest.h b/Framework/DataHandling/test/ISISJournalTest.h index a731871ad7fd448bf85885b6ced807f53c2e2700..f1036130c74f8b3f4c05e01e5d8b0f02f9d36706 100644 --- a/Framework/DataHandling/test/ISISJournalTest.h +++ b/Framework/DataHandling/test/ISISJournalTest.h @@ -6,171 +6,297 @@ // SPDX - License - Identifier: GPL - 3.0 + #pragma once -#include <cxxtest/TestSuite.h> - #include "MantidDataHandling/ISISJournal.h" +#include "MantidKernel/Exception.h" +#include "MantidKernel/InternetHelper.h" +#include <Poco/Net/HTTPResponse.h> + +#include <cxxtest/TestSuite.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> using namespace Mantid::DataHandling::ISISJournal; +using Mantid::Kernel::InternetHelper; +using Mantid::Kernel::Exception::InternetError; +using testing::_; +using testing::NiceMock; +using testing::Return; + +namespace { +static constexpr const char *emptyFile = ""; + +static constexpr const char *badFile = "<NXroot"; + +static constexpr const char *emptyJournalFile = "\ +<?xml version=\"1.0\" encoding=\"UTF-8\"?>\ + <NXroot NeXus_version=\"4.3.0\" XML_version=\"mxml\"></NXroot>"; + +static constexpr const char *emptyIndexFile = "<journal></journal>"; + +static constexpr const char *invalidIndexFile = "<journal><badtag/></journal>"; + +static constexpr const char *indexFile = "\ +<journal>\ + <file name=\"journal.xml\" />\ + <file name=\"journal_17_1.xml\" />\ + <file name=\"journal_18_1.xml\" />\ + <file name=\"journal_19_1.xml\" />\ + <file name=\"journal_19_2.xml\" />\ +</journal>"; + +static constexpr const char *journalFile = "\ + <NXroot>\ + <NXentry name=\"INTER22345\">\ + <title>Experiment 2 run 1</title>\ + <experiment_id>200001</experiment_id>\ + <run_number> 22345</run_number>\ + <count> 5 </count>\ + </NXentry>\ + <NXentry name=\"INTER12345\">\ + <title>Experiment 1 run 1</title>\ + <experiment_id>100001</experiment_id>\ + <run_number> 12345</run_number>\ + <count> 3 </count>\ + </NXentry>\ + <NXentry name=\"INTER12346\">\ + <title>Experiment 1 run 2</title>\ + <experiment_id>100001</experiment_id>\ + <run_number> 12346</run_number>\ + <count> 5 </count>\ + </NXentry>\ + </NXroot>"; +} // namespace + +class MockInternetHelper : public InternetHelper { +public: + // The constructor specifies the string that we want sendRequest to return + MockInternetHelper(std::string const &returnString) + : m_returnString(returnString){}; + + MOCK_METHOD2(sendRequestProxy, int(std::string const &, std::ostream &)); + + int sendRequest(std::string const &url, std::ostream &serverReply) override { + serverReply << m_returnString; + return sendRequestProxy(url, serverReply); + } + +private: + std::string m_returnString; +}; class ISISJournalTest : public CxxTest::TestSuite { public: - void test_getRunData_with_empty_file_throws() { - TS_ASSERT_THROWS( - getRunDataFromFile(emptyFile, defaultTags(), defaultFilters()), - std::runtime_error const &); + void test_getRuns_requests_correct_url() { + auto journal = makeJournal(); + auto url = std::string( + "http://data.isis.rl.ac.uk/journals/ndxinter/journal_19_4.xml"); + EXPECT_CALL(*m_internetHelper, sendRequestProxy(url, _)).Times(1); + journal.getRuns(); + verifyAndClear(); } - void test_getRunData_with_bad_xml_throws() { - TS_ASSERT_THROWS( - getRunDataFromFile(badFile, defaultTags(), defaultFilters()), - std::runtime_error const &); + void test_getCycleNames_requests_correct_url() { + auto journal = makeJournal(indexFile); + auto url = std::string( + "http://data.isis.rl.ac.uk/journals/ndxinter/journal_main.xml"); + EXPECT_CALL(*m_internetHelper, sendRequestProxy(url, _)).Times(1); + journal.getCycleNames(); + verifyAndClear(); } - void test_getRunData_with_empty_xml_file_returns_empty_results() { - auto results = - getRunDataFromFile(emptyJournalFile, defaultTags(), defaultFilters()); - TS_ASSERT_EQUALS(results, std::vector<ISISJournalData>{}); + void test_getRuns_when_server_returns_journalFile() { + auto journal = makeJournal(journalFile); + auto results = journal.getRuns(); + auto const expected = + std::vector<ISISJournal::RunData>{{{"name", "INTER22345"}}, + {{"name", "INTER12345"}}, + {{"name", "INTER12346"}}}; + TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); } - void test_getRunData_still_returns_run_names_when_tags_list_is_empty() { - auto results = - getRunDataFromFile(journalFile, emptyTags(), defaultFilters()); - auto const expected = std::vector<ISISJournalData>{ + void test_getCycleNames_when_server_returns_indexFile() { + auto journal = makeJournal(indexFile); + auto results = journal.getCycleNames(); + auto const expected = + std::vector<std::string>{"17_1", "18_1", "19_1", "19_2"}; + TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); + } + + void test_getRuns_throws_if_url_not_found() { + auto journal = makeJournal(); + expectURLNotFound(); + TS_ASSERT_THROWS(journal.getRuns(), InternetError const &); + verifyAndClear(); + } + + void test_getCycleNames_throws_if_url_not_found() { + auto journal = makeJournal(indexFile); + expectURLNotFound(); + TS_ASSERT_THROWS(journal.getCycleNames(), InternetError const &); + verifyAndClear(); + } + + void test_getRuns_with_empty_file_throws() { + auto journal = makeJournal(emptyFile); + TS_ASSERT_THROWS(journal.getRuns(), std::runtime_error const &); + verifyAndClear(); + } + + void test_getRuns_with_bad_xml_throws() { + auto journal = makeJournal(badFile); + TS_ASSERT_THROWS(journal.getRuns(), std::runtime_error const &); + verifyAndClear(); + } + + void test_getRuns_with_empty_xml_file_returns_empty_results() { + auto journal = makeJournal(emptyJournalFile); + auto results = journal.getRuns(valuesToLookup(), filters()); + TS_ASSERT_EQUALS(results, std::vector<ISISJournal::RunData>{}); + verifyAndClear(); + } + + void + test_getRuns_still_returns_run_names_when_requested_values_list_is_empty() { + auto journal = makeJournal(); + auto results = journal.getRuns(emptyValueNames(), filters()); + auto const expected = std::vector<ISISJournal::RunData>{ {{"name", "INTER12345"}}, {{"name", "INTER12346"}}}; } - void test_getRunData_returns_all_run_names_when_tags_and_filters_are_empty() { - auto results = getRunDataFromFile(journalFile); + void + test_getRuns_returns_all_run_names_when_values_list_and_filters_are_empty() { + auto journal = makeJournal(); + auto results = journal.getRuns(); auto const expected = - std::vector<ISISJournalData>{{{"name", "INTER22345"}}, - {{"name", "INTER12345"}}, - {{"name", "INTER12346"}}}; + std::vector<ISISJournal::RunData>{{{"name", "INTER22345"}}, + {{"name", "INTER12345"}}, + {{"name", "INTER12346"}}}; + verifyAndClear(); } - void test_getRunData_returns_requested_tags_filtered_by_one_filter() { - auto results = - getRunDataFromFile(journalFile, defaultTags(), defaultFilters()); + void test_getRuns_returns_requested_values_filtered_by_one_filter() { + auto journal = makeJournal(); + auto results = journal.getRuns(valuesToLookup(), filters()); auto const expected = - std::vector<ISISJournalData>{{{"name", "INTER12345"}, - {"run_number", "12345"}, - {"title", "Experiment 1 run 1"}}, - {{"name", "INTER12346"}, - {"run_number", "12346"}, - {"title", "Experiment 1 run 2"}}}; + std::vector<ISISJournal::RunData>{{{"name", "INTER12345"}, + {"run_number", "12345"}, + {"title", "Experiment 1 run 1"}}, + {{"name", "INTER12346"}, + {"run_number", "12346"}, + {"title", "Experiment 1 run 2"}}}; TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); } - void test_getRunData_returns_requested_tags_filtered_by_multiple_filters() { - auto results = - getRunDataFromFile(journalFile, defaultTags(), multipleFilters()); + void test_getRuns_returns_requested_values_filtered_by_multiple_filters() { + auto journal = makeJournal(); + auto results = journal.getRuns(valuesToLookup(), multipleFilters()); auto const expected = - std::vector<ISISJournalData>{{{"name", "INTER12346"}, - {"run_number", "12346"}, - {"title", "Experiment 1 run 2"}}}; + std::vector<ISISJournal::RunData>{{{"name", "INTER12346"}, + {"run_number", "12346"}, + {"title", "Experiment 1 run 2"}}}; TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); } void - test_getRunData_returns_requested_tags_for_all_entries_when_no_filter_is_set() { - auto results = - getRunDataFromFile(journalFile, defaultTags(), emptyFilters()); + test_getRuns_returns_requested_values_for_all_entries_when_no_filter_is_set() { + auto journal = makeJournal(); + auto results = journal.getRuns(valuesToLookup(), emptyFilters()); auto const expected = - std::vector<ISISJournalData>{{{"name", "INTER22345"}, - {"run_number", "22345"}, - {"title", "Experiment 2 run 1"}}, - {{"name", "INTER12345"}, - {"run_number", "12345"}, - {"title", "Experiment 1 run 1"}}, - {{"name", "INTER12346"}, - {"run_number", "12346"}, - {"title", "Experiment 1 run 2"}}}; + std::vector<ISISJournal::RunData>{{{"name", "INTER22345"}, + {"run_number", "22345"}, + {"title", "Experiment 2 run 1"}}, + {{"name", "INTER12345"}, + {"run_number", "12345"}, + {"title", "Experiment 1 run 1"}}, + {{"name", "INTER12346"}, + {"run_number", "12346"}, + {"title", "Experiment 1 run 2"}}}; TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); } void test_getCycleList_with_empty_file_throws() { - TS_ASSERT_THROWS(getCycleListFromFile(emptyFile), - std::runtime_error const &); + auto journal = makeJournal(emptyFile); + TS_ASSERT_THROWS(journal.getCycleNames(), std::runtime_error const &); + verifyAndClear(); } void test_getCycleList_with_bad_xml_throws() { - TS_ASSERT_THROWS(getCycleListFromFile(badFile), std::runtime_error const &); + auto journal = makeJournal(badFile); + TS_ASSERT_THROWS(journal.getCycleNames(), std::runtime_error const &); + verifyAndClear(); } void test_getCycleList_with_empty_xml_file_returns_empty_results() { - auto results = getCycleListFromFile(emptyIndexFile); + auto journal = makeJournal(emptyIndexFile); + auto results = journal.getCycleNames(); TS_ASSERT_EQUALS(results, std::vector<std::string>{}); + verifyAndClear(); } void test_getCycleList_throws_when_invalid_element_names() { - TS_ASSERT_THROWS(getCycleListFromFile(invalidIndexFile), - std::invalid_argument const &); + auto journal = makeJournal(invalidIndexFile); + TS_ASSERT_THROWS(journal.getCycleNames(), std::invalid_argument const &); + verifyAndClear(); } void test_getCycleList_returns_all_valid_cycles() { - auto results = getCycleListFromFile(indexFile); + auto journal = makeJournal(indexFile); + auto results = journal.getCycleNames(); auto const expected = std::vector<std::string>{"17_1", "18_1", "19_1", "19_2"}; TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); } private: - ISISJournalTags defaultTags() { - return ISISJournalTags{"run_number", "title"}; - } + NiceMock<MockInternetHelper> *m_internetHelper; - ISISJournalTags emptyTags() { return ISISJournalTags{}; } - - ISISJournalFilters defaultFilters() { - return ISISJournalFilters{{"experiment_id", "100001"}}; + ISISJournal + makeJournal(std::string const &xmlContents = std::string(journalFile)) { + // Inject a mock internet helper. The journal takes ownership of the + // unique_ptr but we store a raw pointer to use in our test. + auto internetHelper = + std::make_unique<NiceMock<MockInternetHelper>>(xmlContents); + m_internetHelper = internetHelper.get(); + auto journal = ISISJournal("INTER", "19_4", std::move(internetHelper)); + // Ensure the internet helper returns an ok status by default. + auto status = Poco::Net::HTTPResponse::HTTP_OK; + ON_CALL(*m_internetHelper, sendRequestProxy(_, _)) + .WillByDefault(Return(status)); + return journal; } - ISISJournalFilters multipleFilters() { - return ISISJournalFilters{{"experiment_id", "100001"}, {"count", "5"}}; + std::vector<std::string> valuesToLookup() { + return std::vector<std::string>{"run_number", "title"}; } - ISISJournalFilters emptyFilters() { return ISISJournalFilters{}; } - - static constexpr const char *emptyFile = ""; - - static constexpr const char *badFile = "<NXroot"; + std::vector<std::string> emptyValueNames() { + return std::vector<std::string>{}; + } - static constexpr const char *emptyJournalFile = "\ -<?xml version=\"1.0\" encoding=\"UTF-8\"?>\ - <NXroot NeXus_version=\"4.3.0\" XML_version=\"mxml\"></NXroot>"; + ISISJournal::RunData filters() { + return ISISJournal::RunData{{"experiment_id", "100001"}}; + } - static constexpr const char *emptyIndexFile = "<journal></journal>"; + ISISJournal::RunData multipleFilters() { + return ISISJournal::RunData{{"experiment_id", "100001"}, {"count", "5"}}; + } - static constexpr const char *invalidIndexFile = - "<journal><badtag/></journal>"; + ISISJournal::RunData emptyFilters() { return ISISJournal::RunData{}; } - static constexpr const char *indexFile = "\ -<journal>\ - <file name=\"journal.xml\" />\ - <file name=\"journal_17_1.xml\" />\ - <file name=\"journal_18_1.xml\" />\ - <file name=\"journal_19_1.xml\" />\ - <file name=\"journal_19_2.xml\" />\ -</journal>"; + void expectURLNotFound() { + auto status = Poco::Net::HTTPResponse::HTTP_NOT_FOUND; + EXPECT_CALL(*m_internetHelper, sendRequestProxy(_, _)) + .Times(1) + .WillOnce(Return(status)); + } - static constexpr const char *journalFile = "\ - <NXroot>\ - <NXentry name=\"INTER22345\">\ - <title>Experiment 2 run 1</title>\ - <experiment_id>200001</experiment_id>\ - <run_number> 22345</run_number>\ - <count> 5 </count>\ - </NXentry>\ - <NXentry name=\"INTER12345\">\ - <title>Experiment 1 run 1</title>\ - <experiment_id>100001</experiment_id>\ - <run_number> 12345</run_number>\ - <count> 3 </count>\ - </NXentry>\ - <NXentry name=\"INTER12346\">\ - <title>Experiment 1 run 2</title>\ - <experiment_id>100001</experiment_id>\ - <run_number> 12346</run_number>\ - <count> 5 </count>\ - </NXentry>\ - </NXroot>"; + void verifyAndClear() { + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(m_internetHelper)); + } };