Skip to content
Snippets Groups Projects
Commit c0aee984 authored by Gemma Guest's avatar Gemma Guest
Browse files

Move helper functions into a class

This means we can hide the parsing function that takes an xml string from the public interface and add a friend class to test this.
It also means that getRuns() can be called multiple times with different criteria without having to re-parse the file.

Re #26897
parent 4c9fcf6e
No related branches found
No related tags found
No related merge requests found
...@@ -8,37 +8,58 @@ ...@@ -8,37 +8,58 @@
#include "MantidKernel/System.h" #include "MantidKernel/System.h"
#include <Poco/AutoPtr.h>
#include <map> #include <map>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
namespace Poco::XML {
class Document;
}
namespace Mantid { namespace Mantid {
namespace Kernel {
class InternetHelper;
}
namespace DataHandling { namespace DataHandling {
namespace ISISJournal { namespace ISISJournal {
/** /**
Defines functions to aid in fetching ISIS specific run information from * ISISJournal: Helper class to aid in fetching ISIS specific run information
journal files * from journal files
*/ */
using ISISJournalTags = std::vector<std::string>; class DLLExport ISISJournal {
using ISISJournalFilters = std::map<std::string, std::string>; public:
using ISISJournalData = std::map<std::string, std::string>; using RunData = std::map<std::string, std::string>;
std::vector<ISISJournalData> DLLExport ISISJournal(std::string const &instrument, std::string const &cycle,
getRunDataFromFile(std::string const &fileContents, std::unique_ptr<Kernel::InternetHelper> internetHelper =
ISISJournalTags const &tags = ISISJournalTags(), std::make_unique<Kernel::InternetHelper>());
ISISJournalFilters const &filters = ISISJournalFilters()); virtual ~ISISJournal();
std::vector<ISISJournalData> DLLExport ISISJournal(ISISJournal const &rhs) = delete;
getRunData(std::string const &instrument, std::string const &cycle, ISISJournal(ISISJournal &&rhs);
ISISJournalTags const &tags = ISISJournalTags(), ISISJournal const &operator=(ISISJournal const &rhs) = delete;
ISISJournalFilters const &filters = ISISJournalFilters()); ISISJournal &operator=(ISISJournal &&rhs);
std::vector<std::string> /// Get the list of cycle names
DLLExport getCycleListFromFile(std::string const &fileContents); 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 ISISJournal
} // namespace DataHandling } // namespace DataHandling
} // namespace Mantid } // namespace Mantid
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
#include "MantidKernel/InternetHelper.h" #include "MantidKernel/InternetHelper.h"
#include "Poco/SAX/SAXException.h" #include "Poco/SAX/SAXException.h"
#include <Poco/AutoPtr.h>
#include <Poco/DOM/DOMParser.h> #include <Poco/DOM/DOMParser.h>
#include <Poco/DOM/Document.h> #include <Poco/DOM/Document.h>
#include <Poco/DOM/NodeFilter.h> #include <Poco/DOM/NodeFilter.h>
...@@ -34,59 +33,41 @@ using Poco::XML::NodeFilter; ...@@ -34,59 +33,41 @@ using Poco::XML::NodeFilter;
using Poco::XML::TreeWalker; using Poco::XML::TreeWalker;
namespace { namespace {
static constexpr const char *URL_PREFIX = "http://data.isis.rl.ac.uk/"; static constexpr const char *URL_PREFIX =
static constexpr const char *INSTRUMENT_PREFIX = "journals/ndx"; "http://data.isis.rl.ac.uk/journals/ndx";
static constexpr const char *JOURNAL_INDEX_FILE = "main"; static constexpr const char *INDEX_FILE_NAME = "main";
static constexpr const char *JOURNAL_PREFIX = "journal_"; static constexpr const char *JOURNAL_PREFIX = "/journal_";
static constexpr const char *JOURNAL_EXT = ".xml"; static constexpr const char *JOURNAL_EXT = ".xml";
static constexpr const char *NXROOT_TAG = "NXroot"; static constexpr const char *NXROOT_TAG = "NXroot";
static constexpr const char *NXENTRY_TAG = "NXentry"; static constexpr const char *NXENTRY_TAG = "NXentry";
static constexpr const char *JOURNAL_TAG = "journal"; static constexpr const char *JOURNAL_TAG = "journal";
static constexpr const char *FILE_TAG = "file"; 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. * instrument and cycle, e.g.
* http://data.isis.rl.ac.uk/journals/ndxinter/journal_19_4.xml * 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 constructURL(std::string instrument, std::string const &name) {
std::string const &cycle) { boost::algorithm::to_lower(instrument);
std::stringstream url; std::ostringstream url;
url << URL_PREFIX << INSTRUMENT_PREFIX << instrument << "/" << JOURNAL_PREFIX url << URL_PREFIX << instrument << JOURNAL_PREFIX << name << JOURNAL_EXT;
<< cycle << JOURNAL_EXT;
return url.str(); return url.str();
} }
/* Construct the URL for the journal index file for a particular instrument, /** Parse a given XML string into a Document object
* e.g. http://data.isis.rl.ac.uk/journals/ndxinter/journal_main.xml * @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) { Poco::AutoPtr<Document> parse(std::string const &xmlString) {
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) {
DOMParser domParser; DOMParser domParser;
Poco::AutoPtr<Document> xmldoc; Poco::AutoPtr<Document> xmldoc;
try { try {
xmldoc = domParser.parseString(fileContents); xmldoc = domParser.parseString(xmlString);
} catch (Poco::XML::SAXParseException const &ex) { } catch (Poco::XML::SAXParseException const &ex) {
std::ostringstream msg; std::ostringstream msg;
msg << "ISISJournal: Error parsing file: " << ex.what(); msg << "ISISJournal: Error parsing file: " << ex.what();
...@@ -95,26 +76,30 @@ Poco::AutoPtr<Document> parse(std::string const &fileContents) { ...@@ -95,26 +76,30 @@ Poco::AutoPtr<Document> parse(std::string const &fileContents) {
return xmldoc; 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) { if (!element) {
std::ostringstream msg; std::ostringstream msg;
msg << "ISISJournal::validateElement() - invalid element for '" msg << "ISISJournal::throwIfNotValid() - invalid element for '"
<< NXROOT_TAG << "'\n"; << expectedName << "'\n";
throw std::invalid_argument(msg.str()); throw std::invalid_argument(msg.str());
} }
if (element->nodeName() != expectedName) { if (element->nodeName() != expectedName) {
std::ostringstream msg; std::ostringstream msg;
msg << "ISISJournal::validateElement() - Element tag does not match '" msg << "ISISJournal::throwIfNotValid() - Element name does not match '"
<< NXROOT_TAG << "'. Found " << element->nodeName() << "\n"; << expectedName << "'. Found " << element->nodeName() << "\n";
throw std::invalid_argument(msg.str()); throw std::invalid_argument(msg.str());
} }
} }
/* Return the text value contained in the given node, or an empty string if it /** Get the text contained in a node.
* does not contain a text value. * @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) { std::string getTextValue(Node *node) {
if (!node) if (!node)
...@@ -130,12 +115,14 @@ std::string getTextValue(Node *node) { ...@@ -130,12 +115,14 @@ std::string getTextValue(Node *node) {
return std::string(); return std::string();
} }
/* Check if an element matches a set of filter criteria. Checks for a child /** Check if an element matches a set of filter criteria.
* element with the filter name and checks that its value matches the given * @param element : the element to check
* filter value. * @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) { bool matchesAllFilters(Element *element, ISISJournal::RunData const &filters) {
for (auto filterKvp : filters) { for (auto const &filterKvp : filters) {
auto const childElement = element->getChildElement(filterKvp.first); auto const childElement = element->getChildElement(filterKvp.first);
if (getTextValue(childElement) != filterKvp.second) if (getTextValue(childElement) != filterKvp.second)
return false; return false;
...@@ -143,61 +130,87 @@ bool matchesAllFilters(Element *element, ISISJournalFilters const &filters) { ...@@ -143,61 +130,87 @@ bool matchesAllFilters(Element *element, ISISJournalFilters const &filters) {
return true; return true;
} }
/* Extract a list of named "tag" values for the given element. Gets the text /** Utility function to create the run data for an element.
* values of the child elements with the given names and returns a map of the * @param element : the element to get run data for
* tag name to the value. Also always adds the element name to the list so there * @returns : a map of name->value pairs initialised with the mandatory data
* is always a results for every run. * that is returned for all runs (currently just the name attribute, e.g.
* "name": "INTER00013460")
*/ */
ISISJournalData getTagsForNode(Element *element, ISISJournalTags const &tags) { ISISJournal::RunData createRunDataForElement(Element *element) {
auto result = ISISJournalData{}; return ISISJournal::RunData{{"name", element->getAttribute("name")}};
// 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;
} }
/* Extract a list of "tag" values for all child nodes in the given "root" or /** Extract a list of named values for the given element.
* parent element. Here, a "tag" is a name for a child element in the element * @param element : the element to extract values for
* we're checking i.e. a grandchild of the root node. * @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> void addValuesForElement(Element *element,
getTagsForAllNodes(Element *root, ISISJournalTags const &tags, std::vector<std::string> const &valuesToLookup,
ISISJournalFilters const &filters) { ISISJournal::RunData &result) {
auto results = std::vector<ISISJournalData>{}; 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()) { for (auto node = nodeIter.nextNode(); node; node = nodeIter.nextSibling()) {
auto element = dynamic_cast<Element *>(node); auto element = dynamic_cast<Element *>(node);
validateElement(element, NXENTRY_TAG); throwIfNotValid(element, NXENTRY_TAG);
if (matchesAllFilters(element, filters))
results.emplace_back(getTagsForNode(element, tags)); if (matchesAllFilters(element, filters)) {
auto result = createRunDataForElement(element);
addValuesForElement(element, valuesToLookup, result);
results.emplace_back(std::move(result));
}
} }
return results; return results;
} }
/* Get the text values for all direct child elements of a given parent /** Extract an attribute value for all direct children of a given element.
* element. The child elements should all have the given tag name - throws if * @param parentElement : the element containing the child nodes to check
* not. * @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, std::vector<std::string>
const char *tag) { getAttributeForAllChildElements(Element *parentElement,
const char *childElementName,
const char *attributeName) {
auto results = std::vector<std::string>{}; 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()) { for (auto node = nodeIter.nextNode(); node; node = nodeIter.nextSibling()) {
auto element = dynamic_cast<Element *>(node); auto element = dynamic_cast<Element *>(node);
validateElement(element, tag); throwIfNotValid(element, childElementName);
results.emplace_back(element->getAttribute("name")); results.emplace_back(element->getAttribute(attributeName));
} }
return results; return results;
} }
/* Convert a cycle filename to the cycle name or return an empty string if it /** Convert a cycle filename to the cycle name
* doesn't match the required pattern * @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) { std::string convertFilenameToCycleName(std::string const &filename) {
boost::regex pattern("[0-9]+_[0-9]+"); boost::regex pattern("[0-9]+_[0-9]+");
...@@ -210,9 +223,11 @@ std::string convertFilenameToCycleName(std::string const &filename) { ...@@ -210,9 +223,11 @@ std::string convertFilenameToCycleName(std::string const &filename) {
return std::string(); return std::string();
} }
/* Convert a list of cycle filenames to a list of cycle names e.g. /** Convert a list of cycle filenames to a list of cycle names.
* journal_19_4.xml -> 19_4. Also exlcudes files from the list if they do not * @param filenames : the list of filenames e.g. journal_main.xml,
* match the required pattern. * 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> std::vector<std::string>
convertFilenamesToCycleNames(std::vector<std::string> const &filenames) { convertFilenamesToCycleNames(std::vector<std::string> const &filenames) {
...@@ -227,62 +242,78 @@ convertFilenamesToCycleNames(std::vector<std::string> const &filenames) { ...@@ -227,62 +242,78 @@ convertFilenamesToCycleNames(std::vector<std::string> const &filenames) {
} }
} // namespace } // namespace
/** Get specified data from a journal file for all runs that match given filter /** Construct the journal class for a specific instrument and cycle
* criteria. * @param instrument : the ISIS instrument name to request data for e.g. "INTER"
* * (case insensitive)
* @param fileContents : the XML journal file contents * @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. * @param internetHelper : class for sending internet requests
* "run_number"
* @param filters : optional tag names and values to filter the results by
*/ */
std::vector<ISISJournalData> ISISJournal::ISISJournal(std::string const &instrument,
getRunDataFromFile(std::string const &fileContents, ISISJournalTags const &tags, std::string const &cycle,
ISISJournalFilters const &filters) { std::unique_ptr<InternetHelper> internetHelper)
auto xmldoc = parse(fileContents); : m_internetHelper(std::move(internetHelper)),
auto root = xmldoc->documentElement(); m_runsFileURL(constructURL(instrument, cycle)),
validateElement(root, NXROOT_TAG); m_indexFileURL(constructURL(instrument, INDEX_FILE_NAME)) {}
return getTagsForAllNodes(root, tags, filters);
} 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 /** Get the cycle names
* match given filter criteria.
* *
* @param instrument : the instrument to request data for * @returns : a list of all ISIS cycle names for the instrument
* @param cycle : the ISIS cycle the required data is from e.g. "19_4" * @throws : if there was an error fetching the runs
* @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
*/ */
std::vector<ISISJournalData> getRunData(std::string const &instrument, std::vector<std::string> ISISJournal::getCycleNames() {
std::string const &cycle, if (!m_indexDocument) {
ISISJournalTags const &tags, auto xmlString = getURLContents(m_indexFileURL);
ISISJournalFilters const &filters) { m_indexDocument = parse(xmlString);
auto const url = constructRunsFileURL(instrument, cycle); }
auto fileContents = getURLContents(url); auto rootElement = m_indexDocument->documentElement();
return getRunDataFromFile(fileContents, tags, filters); 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 /** Get run names and other specified data for all runs that match the given
* @param fileContents : the contents of the XML index file * filter criteria.
* @returns : a list of cycle names *
* @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) { std::vector<ISISJournal::RunData>
auto xmldoc = parse(fileContents); ISISJournal::getRuns(std::vector<std::string> const &valuesToLookup,
auto root = xmldoc->documentElement(); ISISJournal::RunData const &filters) {
validateElement(root, JOURNAL_TAG); if (!m_runsDocument) {
auto filenames = getTagValuesForChildElements(root, FILE_TAG); auto xmlString = getURLContents(m_runsFileURL);
return convertFilenamesToCycleNames(filenames); 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 /** Get the contents of a file at a given URL
* @param instrument : the instrument name * @param url : the URL to fetch
* @returns : a list of cycle names * @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) { std::string ISISJournal::getURLContents(std::string const &url) {
auto const url = constructIndexFileURL(instrument); std::ostringstream serverReply;
auto fileContents = getURLContents(url); auto const statusCode = m_internetHelper->sendRequest(url, serverReply);
return getCycleList(fileContents); 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 ISISJournal
} // namespace DataHandling } // namespace DataHandling
} // namespace Mantid } // namespace Mantid
...@@ -33,7 +33,8 @@ if(CXXTEST_FOUND) ...@@ -33,7 +33,8 @@ if(CXXTEST_FOUND)
HistogramData HistogramData
${NEXUS_LIBRARIES} ${NEXUS_LIBRARIES}
${HDF5_LIBRARIES} ${HDF5_LIBRARIES}
${HDF5_HL_LIBRARIES}) ${HDF5_HL_LIBRARIES}
gmock)
add_dependencies(DataHandlingTest Algorithms MDAlgorithms) add_dependencies(DataHandlingTest Algorithms MDAlgorithms)
add_dependencies(FrameworkTests DataHandlingTest) add_dependencies(FrameworkTests DataHandlingTest)
# Test data # Test data
......
...@@ -6,171 +6,297 @@ ...@@ -6,171 +6,297 @@
// SPDX - License - Identifier: GPL - 3.0 + // SPDX - License - Identifier: GPL - 3.0 +
#pragma once #pragma once
#include <cxxtest/TestSuite.h>
#include "MantidDataHandling/ISISJournal.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 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 { class ISISJournalTest : public CxxTest::TestSuite {
public: public:
void test_getRunData_with_empty_file_throws() { void test_getRuns_requests_correct_url() {
TS_ASSERT_THROWS( auto journal = makeJournal();
getRunDataFromFile(emptyFile, defaultTags(), defaultFilters()), auto url = std::string(
std::runtime_error const &); "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() { void test_getCycleNames_requests_correct_url() {
TS_ASSERT_THROWS( auto journal = makeJournal(indexFile);
getRunDataFromFile(badFile, defaultTags(), defaultFilters()), auto url = std::string(
std::runtime_error const &); "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() { void test_getRuns_when_server_returns_journalFile() {
auto results = auto journal = makeJournal(journalFile);
getRunDataFromFile(emptyJournalFile, defaultTags(), defaultFilters()); auto results = journal.getRuns();
TS_ASSERT_EQUALS(results, std::vector<ISISJournalData>{}); 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() { void test_getCycleNames_when_server_returns_indexFile() {
auto results = auto journal = makeJournal(indexFile);
getRunDataFromFile(journalFile, emptyTags(), defaultFilters()); auto results = journal.getCycleNames();
auto const expected = std::vector<ISISJournalData>{ 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"}}}; {{"name", "INTER12345"}}, {{"name", "INTER12346"}}};
} }
void test_getRunData_returns_all_run_names_when_tags_and_filters_are_empty() { void
auto results = getRunDataFromFile(journalFile); test_getRuns_returns_all_run_names_when_values_list_and_filters_are_empty() {
auto journal = makeJournal();
auto results = journal.getRuns();
auto const expected = auto const expected =
std::vector<ISISJournalData>{{{"name", "INTER22345"}}, std::vector<ISISJournal::RunData>{{{"name", "INTER22345"}},
{{"name", "INTER12345"}}, {{"name", "INTER12345"}},
{{"name", "INTER12346"}}}; {{"name", "INTER12346"}}};
verifyAndClear();
} }
void test_getRunData_returns_requested_tags_filtered_by_one_filter() { void test_getRuns_returns_requested_values_filtered_by_one_filter() {
auto results = auto journal = makeJournal();
getRunDataFromFile(journalFile, defaultTags(), defaultFilters()); auto results = journal.getRuns(valuesToLookup(), filters());
auto const expected = auto const expected =
std::vector<ISISJournalData>{{{"name", "INTER12345"}, std::vector<ISISJournal::RunData>{{{"name", "INTER12345"},
{"run_number", "12345"}, {"run_number", "12345"},
{"title", "Experiment 1 run 1"}}, {"title", "Experiment 1 run 1"}},
{{"name", "INTER12346"}, {{"name", "INTER12346"},
{"run_number", "12346"}, {"run_number", "12346"},
{"title", "Experiment 1 run 2"}}}; {"title", "Experiment 1 run 2"}}};
TS_ASSERT_EQUALS(results, expected); TS_ASSERT_EQUALS(results, expected);
verifyAndClear();
} }
void test_getRunData_returns_requested_tags_filtered_by_multiple_filters() { void test_getRuns_returns_requested_values_filtered_by_multiple_filters() {
auto results = auto journal = makeJournal();
getRunDataFromFile(journalFile, defaultTags(), multipleFilters()); auto results = journal.getRuns(valuesToLookup(), multipleFilters());
auto const expected = auto const expected =
std::vector<ISISJournalData>{{{"name", "INTER12346"}, std::vector<ISISJournal::RunData>{{{"name", "INTER12346"},
{"run_number", "12346"}, {"run_number", "12346"},
{"title", "Experiment 1 run 2"}}}; {"title", "Experiment 1 run 2"}}};
TS_ASSERT_EQUALS(results, expected); TS_ASSERT_EQUALS(results, expected);
verifyAndClear();
} }
void void
test_getRunData_returns_requested_tags_for_all_entries_when_no_filter_is_set() { test_getRuns_returns_requested_values_for_all_entries_when_no_filter_is_set() {
auto results = auto journal = makeJournal();
getRunDataFromFile(journalFile, defaultTags(), emptyFilters()); auto results = journal.getRuns(valuesToLookup(), emptyFilters());
auto const expected = auto const expected =
std::vector<ISISJournalData>{{{"name", "INTER22345"}, std::vector<ISISJournal::RunData>{{{"name", "INTER22345"},
{"run_number", "22345"}, {"run_number", "22345"},
{"title", "Experiment 2 run 1"}}, {"title", "Experiment 2 run 1"}},
{{"name", "INTER12345"}, {{"name", "INTER12345"},
{"run_number", "12345"}, {"run_number", "12345"},
{"title", "Experiment 1 run 1"}}, {"title", "Experiment 1 run 1"}},
{{"name", "INTER12346"}, {{"name", "INTER12346"},
{"run_number", "12346"}, {"run_number", "12346"},
{"title", "Experiment 1 run 2"}}}; {"title", "Experiment 1 run 2"}}};
TS_ASSERT_EQUALS(results, expected); TS_ASSERT_EQUALS(results, expected);
verifyAndClear();
} }
void test_getCycleList_with_empty_file_throws() { void test_getCycleList_with_empty_file_throws() {
TS_ASSERT_THROWS(getCycleListFromFile(emptyFile), auto journal = makeJournal(emptyFile);
std::runtime_error const &); TS_ASSERT_THROWS(journal.getCycleNames(), std::runtime_error const &);
verifyAndClear();
} }
void test_getCycleList_with_bad_xml_throws() { 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() { 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>{}); TS_ASSERT_EQUALS(results, std::vector<std::string>{});
verifyAndClear();
} }
void test_getCycleList_throws_when_invalid_element_names() { void test_getCycleList_throws_when_invalid_element_names() {
TS_ASSERT_THROWS(getCycleListFromFile(invalidIndexFile), auto journal = makeJournal(invalidIndexFile);
std::invalid_argument const &); TS_ASSERT_THROWS(journal.getCycleNames(), std::invalid_argument const &);
verifyAndClear();
} }
void test_getCycleList_returns_all_valid_cycles() { void test_getCycleList_returns_all_valid_cycles() {
auto results = getCycleListFromFile(indexFile); auto journal = makeJournal(indexFile);
auto results = journal.getCycleNames();
auto const expected = auto const expected =
std::vector<std::string>{"17_1", "18_1", "19_1", "19_2"}; std::vector<std::string>{"17_1", "18_1", "19_1", "19_2"};
TS_ASSERT_EQUALS(results, expected); TS_ASSERT_EQUALS(results, expected);
verifyAndClear();
} }
private: private:
ISISJournalTags defaultTags() { NiceMock<MockInternetHelper> *m_internetHelper;
return ISISJournalTags{"run_number", "title"};
}
ISISJournalTags emptyTags() { return ISISJournalTags{}; } ISISJournal
makeJournal(std::string const &xmlContents = std::string(journalFile)) {
ISISJournalFilters defaultFilters() { // Inject a mock internet helper. The journal takes ownership of the
return ISISJournalFilters{{"experiment_id", "100001"}}; // 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() { std::vector<std::string> valuesToLookup() {
return ISISJournalFilters{{"experiment_id", "100001"}, {"count", "5"}}; return std::vector<std::string>{"run_number", "title"};
} }
ISISJournalFilters emptyFilters() { return ISISJournalFilters{}; } std::vector<std::string> emptyValueNames() {
return std::vector<std::string>{};
static constexpr const char *emptyFile = ""; }
static constexpr const char *badFile = "<NXroot";
static constexpr const char *emptyJournalFile = "\ ISISJournal::RunData filters() {
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\ return ISISJournal::RunData{{"experiment_id", "100001"}};
<NXroot NeXus_version=\"4.3.0\" XML_version=\"mxml\"></NXroot>"; }
static constexpr const char *emptyIndexFile = "<journal></journal>"; ISISJournal::RunData multipleFilters() {
return ISISJournal::RunData{{"experiment_id", "100001"}, {"count", "5"}};
}
static constexpr const char *invalidIndexFile = ISISJournal::RunData emptyFilters() { return ISISJournal::RunData{}; }
"<journal><badtag/></journal>";
static constexpr const char *indexFile = "\ void expectURLNotFound() {
<journal>\ auto status = Poco::Net::HTTPResponse::HTTP_NOT_FOUND;
<file name=\"journal.xml\" />\ EXPECT_CALL(*m_internetHelper, sendRequestProxy(_, _))
<file name=\"journal_17_1.xml\" />\ .Times(1)
<file name=\"journal_18_1.xml\" />\ .WillOnce(Return(status));
<file name=\"journal_19_1.xml\" />\ }
<file name=\"journal_19_2.xml\" />\
</journal>";
static constexpr const char *journalFile = "\ void verifyAndClear() {
<NXroot>\ TS_ASSERT(testing::Mock::VerifyAndClearExpectations(m_internetHelper));
<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>";
}; };
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment