diff --git a/Framework/DataHandling/CMakeLists.txt b/Framework/DataHandling/CMakeLists.txt index 0de48b0bcaba3811e81fabd30699de4295974dfd..6f54eb66fda726a0fae59c1f7f1dfc64f6f22de7 100644 --- a/Framework/DataHandling/CMakeLists.txt +++ b/Framework/DataHandling/CMakeLists.txt @@ -29,6 +29,7 @@ set(SRC_FILES src/GroupDetectors2.cpp src/H5Util.cpp src/ISISDataArchive.cpp + src/ISISJournal.cpp src/ISISRunLogs.cpp src/JoinISISPolarizationEfficiencies.cpp src/Load.cpp @@ -235,6 +236,7 @@ set(INC_FILES inc/MantidDataHandling/GroupDetectors2.h inc/MantidDataHandling/H5Util.h inc/MantidDataHandling/ISISDataArchive.h + inc/MantidDataHandling/ISISJournal.h inc/MantidDataHandling/ISISRunLogs.h inc/MantidDataHandling/JoinISISPolarizationEfficiencies.h inc/MantidDataHandling/Load.h @@ -440,6 +442,7 @@ set(TEST_FILES GroupDetectorsTest.h H5UtilTest.h ISISDataArchiveTest.h + ISISJournalTest.h InstrumentRayTracerTest.h JoinISISPolarizationEfficienciesTest.h LoadAscii2Test.h diff --git a/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h b/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h new file mode 100644 index 0000000000000000000000000000000000000000..010873195fe3b13635b66c32fd5b62dffac5e4b5 --- /dev/null +++ b/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h @@ -0,0 +1,65 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2020 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#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 { +/** + * ISISJournal: Helper class to aid in fetching ISIS specific run information + * from journal files + */ + +class DLLExport ISISJournal { +public: + using RunData = std::map<std::string, std::string>; + + ISISJournal(std::string const &instrument, std::string const &cycle, + std::unique_ptr<Kernel::InternetHelper> internetHelper = + std::make_unique<Kernel::InternetHelper>()); + virtual ~ISISJournal(); + + ISISJournal(ISISJournal const &rhs) = delete; + ISISJournal(ISISJournal &&rhs); + ISISJournal const &operator=(ISISJournal const &rhs) = delete; + ISISJournal &operator=(ISISJournal &&rhs); + + /// 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()); + +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 new file mode 100644 index 0000000000000000000000000000000000000000..69cc30270cdee3f3bebd7c6f2d9e41625f9054a2 --- /dev/null +++ b/Framework/DataHandling/src/ISISJournal.cpp @@ -0,0 +1,319 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2020 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +// Includes +#include "MantidDataHandling/ISISJournal.h" +#include "MantidKernel/Exception.h" +#include "MantidKernel/InternetHelper.h" + +#include "Poco/SAX/SAXException.h" +#include <Poco/DOM/DOMParser.h> +#include <Poco/DOM/Document.h> +#include <Poco/DOM/NodeFilter.h> +#include <Poco/DOM/TreeWalker.h> +#include <Poco/Net/HTTPResponse.h> + +#include <boost/algorithm/string.hpp> +#include <boost/regex.hpp> +#include <sstream> + +namespace Mantid { +namespace DataHandling { +namespace ISISJournal { + +using Kernel::InternetHelper; +using Poco::XML::Document; +using Poco::XML::DOMParser; +using Poco::XML::Element; +using Poco::XML::Node; +using Poco::XML::NodeFilter; +using Poco::XML::TreeWalker; + +namespace { +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 + * 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 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(); +} + +/** 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 + */ +Poco::AutoPtr<Document> parse(std::string const &xmlString) { + DOMParser domParser; + Poco::AutoPtr<Document> xmldoc; + try { + xmldoc = domParser.parseString(xmlString); + } catch (Poco::XML::SAXParseException const &ex) { + std::ostringstream msg; + msg << "ISISJournal: Error parsing file: " << ex.what(); + throw std::runtime_error(msg.str()); + } + return xmldoc; +} + +/** 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 throwIfNotValid(Element *element, const char *expectedName) { + if (!element) { + std::ostringstream msg; + msg << "ISISJournal::throwIfNotValid() - invalid element for '" + << expectedName << "'\n"; + throw std::invalid_argument(msg.str()); + } + + if (element->nodeName() != expectedName) { + std::ostringstream msg; + msg << "ISISJournal::throwIfNotValid() - Element name does not match '" + << expectedName << "'. Found " << element->nodeName() << "\n"; + throw std::invalid_argument(msg.str()); + } +} + +/** 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) + return std::string(); + + auto child = node->firstChild(); + if (child && child->nodeName() == "#text") { + auto value = child->nodeValue(); + boost::algorithm::trim(value); + return value; + } + + return std::string(); +} + +/** 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, ISISJournal::RunData const &filters) { + for (auto const &filterKvp : filters) { + auto const childElement = element->getChildElement(filterKvp.first); + if (getTextValue(childElement) != filterKvp.second) + return false; + } + return true; +} + +/** 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") + */ +ISISJournal::RunData createRunDataForElement(Element *element) { + return ISISJournal::RunData{{"name", element->getAttribute("name")}}; +} + +/** 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 + */ +void addValuesForElement(Element *element, + std::vector<std::string> const &valuesToLookup, + ISISJournal::RunData &result) { + for (auto &name : valuesToLookup) + result[name] = getTextValue(element->getChildElement(name)); +} + +/** 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); + throwIfNotValid(element, NXENTRY_TAG); + + if (matchesAllFilters(element, filters)) { + auto result = createRunDataForElement(element); + addValuesForElement(element, valuesToLookup, result); + results.emplace_back(std::move(result)); + } + } + + return results; +} + +/** 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> +getAttributeForAllChildElements(Element *parentElement, + const char *childElementName, + const char *attributeName) { + auto results = std::vector<std::string>{}; + + auto nodeIter = TreeWalker(parentElement, NodeFilter::SHOW_ELEMENT); + for (auto node = nodeIter.nextNode(); node; node = nodeIter.nextSibling()) { + auto element = dynamic_cast<Element *>(node); + throwIfNotValid(element, childElementName); + results.emplace_back(element->getAttribute(attributeName)); + } + + return results; +} + +/** 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]+"); + boost::smatch matches; + boost::regex_search(filename, matches, pattern); + + if (matches.size() == 1) + return matches[0]; + + return std::string(); +} + +/** 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) { + auto cycles = std::vector<std::string>(); + cycles.reserve(filenames.size()); + for (const auto &filename : filenames) { + auto cycle = convertFilenameToCycleName(filename); + if (!cycle.empty()) + cycles.emplace_back(std::move(cycle)); + } + return cycles; +} +} // namespace + +/** 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 + */ +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 the cycle names + * + * @returns : a list of all ISIS cycle names for the instrument + * @throws : if there was an error fetching the runs + */ +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 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<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 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::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 new file mode 100644 index 0000000000000000000000000000000000000000..f1036130c74f8b3f4c05e01e5d8b0f02f9d36706 --- /dev/null +++ b/Framework/DataHandling/test/ISISJournalTest.h @@ -0,0 +1,302 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2020 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#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_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_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_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_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_getRuns_returns_all_run_names_when_values_list_and_filters_are_empty() { + auto journal = makeJournal(); + auto results = journal.getRuns(); + auto const expected = + std::vector<ISISJournal::RunData>{{{"name", "INTER22345"}}, + {{"name", "INTER12345"}}, + {{"name", "INTER12346"}}}; + verifyAndClear(); + } + + void test_getRuns_returns_requested_values_filtered_by_one_filter() { + auto journal = makeJournal(); + auto results = journal.getRuns(valuesToLookup(), filters()); + auto const expected = + 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_getRuns_returns_requested_values_filtered_by_multiple_filters() { + auto journal = makeJournal(); + auto results = journal.getRuns(valuesToLookup(), multipleFilters()); + auto const expected = + std::vector<ISISJournal::RunData>{{{"name", "INTER12346"}, + {"run_number", "12346"}, + {"title", "Experiment 1 run 2"}}}; + TS_ASSERT_EQUALS(results, expected); + verifyAndClear(); + } + + void + 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<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() { + auto journal = makeJournal(emptyFile); + TS_ASSERT_THROWS(journal.getCycleNames(), std::runtime_error const &); + verifyAndClear(); + } + + void test_getCycleList_with_bad_xml_throws() { + 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 journal = makeJournal(emptyIndexFile); + auto results = journal.getCycleNames(); + TS_ASSERT_EQUALS(results, std::vector<std::string>{}); + verifyAndClear(); + } + + void test_getCycleList_throws_when_invalid_element_names() { + auto journal = makeJournal(invalidIndexFile); + TS_ASSERT_THROWS(journal.getCycleNames(), std::invalid_argument const &); + verifyAndClear(); + } + + void test_getCycleList_returns_all_valid_cycles() { + 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: + NiceMock<MockInternetHelper> *m_internetHelper; + + 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; + } + + std::vector<std::string> valuesToLookup() { + return std::vector<std::string>{"run_number", "title"}; + } + + std::vector<std::string> emptyValueNames() { + return std::vector<std::string>{}; + } + + ISISJournal::RunData filters() { + return ISISJournal::RunData{{"experiment_id", "100001"}}; + } + + ISISJournal::RunData multipleFilters() { + return ISISJournal::RunData{{"experiment_id", "100001"}, {"count", "5"}}; + } + + ISISJournal::RunData emptyFilters() { return ISISJournal::RunData{}; } + + void expectURLNotFound() { + auto status = Poco::Net::HTTPResponse::HTTP_NOT_FOUND; + EXPECT_CALL(*m_internetHelper, sendRequestProxy(_, _)) + .Times(1) + .WillOnce(Return(status)); + } + + void verifyAndClear() { + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(m_internetHelper)); + } +};