diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/integration/Main.cpp | 353 | ||||
-rw-r--r-- | test/integration/TestLogger.cpp | 70 | ||||
-rw-r--r-- | test/integration/TestLogger.hpp | 63 | ||||
-rw-r--r-- | test/integration/TestXmlParser.cpp | 249 | ||||
-rw-r--r-- | test/integration/TestXmlParser.hpp | 89 |
5 files changed, 824 insertions, 0 deletions
diff --git a/test/integration/Main.cpp b/test/integration/Main.cpp new file mode 100644 index 0000000..06e1c4a --- /dev/null +++ b/test/integration/Main.cpp @@ -0,0 +1,353 @@ +/* + Ousía + Copyright (C) 2015 Benjamin Paaßen, Andreas Stöckel + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file Main.cpp + * + * This file provides the integration test framework used in Ousía. The + * integration test framework recursively iterates over the files in the + * "testdata/integration" folder and searches for pairs of X.in.os[x]ml and + * X.out.osxml files. The "in" files are then processed, converted to XML and + * compared to the "out" XML files. Comparison is performed by parsing both + * files using eXpat, sorting the arguments and ignoring certain tags that may + * well differ between two files. + * + * @author Andreas Stöckel (astoecke@techfak.uni-bielefeld.de) + */ + +#include <unistd.h> // Non-portable, needed for isatty + +#include <fstream> +#include <iostream> +#include <memory> +#include <ostream> +#include <sstream> +#include <queue> +#include <vector> + +#include <boost/filesystem.hpp> + +#include <core/common/Utils.hpp> +#include <core/frontend/TerminalLogger.hpp> +#include <core/managed/Manager.hpp> +#include <core/model/Document.hpp> +#include <core/model/Ontology.hpp> +#include <core/model/Project.hpp> +#include <core/model/Typesystem.hpp> +#include <core/parser/ParserContext.hpp> +#include <core/parser/ParserScope.hpp> +#include <core/resource/ResourceManager.hpp> +#include <core/Registry.hpp> +#include <formats/osxml/OsxmlParser.hpp> +#include <formats/osml/OsmlParser.hpp> +#include <plugins/filesystem/SpecialPaths.hpp> +#include <plugins/filesystem/FileLocator.hpp> +#include <plugins/xml/XmlOutput.hpp> + +#include "TestXmlParser.hpp" +#include "TestLogger.hpp" + +using namespace ousia; + +namespace fs = boost::filesystem; + +namespace { + +const size_t SUCCESS = 0; +const size_t ERROR = 1; + +/** + * Structure representing a single test case. + */ +struct Test { + /** + * Input file. + */ + std::string infile; + + /** + * Output file. + */ + std::string outfile; + + /** + * Set to true if the test is expected to fail. + */ + bool shouldFail : 1; + + /** + * Set to true once the test was successful, otherwise initializes to false. + */ + bool success : 1; + + /** + * Default constructor. + */ + Test() : shouldFail(false), success(false) {} + + /** + * Constructor for a standard test. + * + * @param infile is the input file. + * @param outfile is the output file containing the expected input. + */ + Test(const std::string &infile, const std::string &outfile) + : infile(infile), outfile(outfile), shouldFail(false), success(false) + { + } + + /** + * Constructor for a test with expected failure. + * + * @param infile is the input file. + */ + Test(const std::string &infile) + : infile(infile), shouldFail(true), success(false) + { + } +}; + +static bool parseFile(const std::string &infile, std::ostream &os) +{ + // TODO: Share this code with the main CLI + + // Initialize global instances. + bool useColor = isatty(STDERR_FILENO); + TerminalLogger logger{std::cerr, useColor}; + Manager manager; + Registry registry; + ResourceManager resourceManager; + ParserScope scope; + Rooted<Project> project{new Project(manager)}; + FileLocator fileLocator; + ParserContext context{registry, resourceManager, scope, project, logger}; + + // Connect the Source Context Callback of the logger to provide the user + // with context information (line, column, filename, text) for log messages + logger.setSourceContextCallback(resourceManager.getSourceContextCallback()); + + // Fill registry + registry.registerDefaultExtensions(); + OsmlParser osmlParser; + OsxmlParser osxmlParser; + registry.registerParser( + {"text/vnd.ousia.osml"}, + {&RttiTypes::Document, &RttiTypes::Ontology, &RttiTypes::Typesystem}, + &osmlParser); + registry.registerParser( + {"text/vnd.ousia.osml+xml"}, + {&RttiTypes::Document, &RttiTypes::Ontology, &RttiTypes::Typesystem}, + &osxmlParser); + registry.registerResourceLocator(&fileLocator); + + // Register search paths + fileLocator.addDefaultSearchPaths(); + + // Now all preparation is done and we can parse the input document. + Rooted<Node> docNode = + context.import(infile, "", "", {&RttiTypes::Document}); + + if (logger.hasError() || docNode == nullptr) { + return false; + } + Rooted<Document> doc = docNode.cast<Document>(); + + xml::XmlTransformer transform; + transform.writeXml(doc, os, logger, resourceManager, true); + return true; +} + +static bool runTest(test::Logger &logger, const Test &test) +{ + // Parse the infile and dump it as OSXML to a string stream + std::stringstream actual_output; + bool res = parseFile(test.infile, actual_output); + + // If this is a test with expected failure, check whether this failure + // occured. + if (test.shouldFail) { + if (!res) { + logger.success("Parsing failed as expected"); + return true; + } + logger.fail("Expected error while parsing, but parsing succeeded!"); + logger.note("Got following output from " + test.infile); + logger.result(actual_output); + return false; + } else if (!res) { + logger.fail("Unexpected error while parsing input file"); + return false; + } + + // Write the actual_output to disk + + + // Parse both the actual output and the expected output stream + std::ifstream expected_output(test.outfile); + logger.note("Parsing serialized XML"); + std::set<int> errExpected, errActual; + auto actual = test::parseXml(logger, actual_output, errActual); + logger.note("Parsing expected XML from " + test.outfile); + auto expected = test::parseXml(logger, expected_output, errExpected); + + bool ok = false; + if (actual.first && expected.first && + expected.second->compareTo(logger, actual.second, errExpected, + errActual)) { + logger.success("OK!"); + ok = true; + } + + if (!ok) { + logger.note("XML returned by serializer:"); + logger.result(actual_output, errActual); + logger.note("XML stored in file:"); + logger.result(expected_output, errExpected); + return false; + } + + return ok; +} + +/** + * Method used to gather the integration tests. + * + * @param root is the root directory from which the test cases should be + * gathered. + * @return a list of "Test" structures describing the test cases. + */ +static std::vector<Test> gatherTests(fs::path root) +{ + // Result list + std::vector<Test> res; + + // End of a directory iterator + const fs::directory_iterator end; + + // Search all subdirectories of the given "root" directory, do so by using + // a stack + std::queue<fs::path> dirs{{root}}; + while (!dirs.empty()) { + // Fetch the current directory from the queue and remove it + fs::path dir = dirs.front(); + dirs.pop(); + + // Iterate over the contents of this directory + for (fs::directory_iterator it(dir); it != end; it++) { + fs::path p = it->path(); + // If the path p is itself a directory, store it on the stack, + if (fs::is_directory(p)) { + dirs.emplace(p); + } else if (fs::is_regular_file(p)) { + // Fetch the filename + const std::string &fn = p.native(); + + // Check whether the test ends with ".in.osml" or ".in.osxml" + std::string testname; + bool shouldFail = false; + if (Utils::endsWith(fn, ".in.osml")) { + testname = fn.substr(0, fn.size() - 8); + } else if (Utils::endsWith(fn, ".in.osxml")) { + testname = fn.substr(0, fn.size() - 9); + } else if (Utils::endsWith(fn, ".fail.osml")) { + testname = fn.substr(0, fn.size() - 10); + shouldFail = true; + } else if (Utils::endsWith(fn, ".fail.osxml")) { + testname = fn.substr(0, fn.size() - 11); + shouldFail = true; + } + + // If yes, check whether the same file exists ending with + // .out.osxml -- if this is the case, add the resulting test + // case to the result + if (!testname.empty()) { + if (shouldFail) { + res.emplace_back(fn); + } else { + const std::string outFn = testname + ".out.osxml"; + if (fs::is_regular_file(outFn)) { + res.emplace_back(fn, outFn); + } + } + } + } + } + } + + // Return the unit test list + return res; +} +} + +int main(int argc, char **argv) +{ + // Initialize terminal logger. Only use color if writing to a terminal (tty) + bool useColor = isatty(STDERR_FILENO); + test::Logger logger(std::cerr, useColor); + logger.headline("OUSÍA INTEGRATION TEST FRAMEWORK"); + logger.note("(c) Benjamin Paaßen, Andreas Stöckel 2015"); + logger.note("This program is free software licensed under the GPLv3"); + + // Use boost filesystem to recursively iterate over all files + fs::path root = + fs::path(SpecialPaths::getDebugTestdataDir()) / "integration"; + if (!fs::is_directory(root)) { + logger.fail("Could not find integration test data directory: " + + root.native()); + return ERROR; + } + + // Fetch all test cases + logger.headline("GATHER TESTS"); + std::vector<Test> tests = gatherTests(root); + std::string testsWord = tests.size() == 1 ? " test" : " tests"; + logger.note(std::to_string(tests.size()) + testsWord + " found"); + + logger.headline("RUN TESTS"); + size_t successCount = 0; + size_t failCount = 0; + for (auto &test : tests) { + logger.note("Running test " + test.infile); + if (runTest(logger, test)) { + test.success = true; + successCount++; + } else { + failCount++; + } + } + + // Write the final error messages + logger.headline("TEST SUMMARY"); + logger.note(std::string("Ran ") + std::to_string(failCount + successCount) + + testsWord + ", " + std::to_string(failCount) + " failed, " + + std::to_string(successCount) + " succeeded"); + if (failCount > 0) { + logger.note("The following tests failed:"); + for (const auto &test : tests) { + if (!test.success) { + logger.fail(test.infile); + } + } + } else { + logger.success("All tests completed successfully!"); + } + + // Inform the shell about failing integration tests + return failCount > 0 ? ERROR : SUCCESS; +} + diff --git a/test/integration/TestLogger.cpp b/test/integration/TestLogger.cpp new file mode 100644 index 0000000..d745c0f --- /dev/null +++ b/test/integration/TestLogger.cpp @@ -0,0 +1,70 @@ +/* + Ousía + Copyright (C) 2015 Benjamin Paaßen, Andreas Stöckel + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iomanip> + +#include "TestLogger.hpp" + +namespace ousia { +namespace test { + +Logger::Logger(std::ostream &os, bool useColor) : os(os), terminal(useColor) {} + +void Logger::fail(const std::string &msg) +{ + os << terminal.color(Terminal::RED, true) << "[Fail]" << terminal.reset() + << " " << msg << std::endl; +} + +void Logger::success(const std::string &msg) +{ + os << terminal.color(Terminal::GREEN, true) << "[Success]" + << terminal.reset() << " " << msg << std::endl; +} + +void Logger::note(const std::string &msg) +{ + os << terminal.color(Terminal::BLUE, true) << "[Note]" << terminal.reset() + << " " << msg << std::endl; +} + +void Logger::result(std::istream &is, const std::set<int> &errLines) +{ + std::string line; + is.clear(); + is.seekg(0); + size_t lineNumber = 0; + while (std::getline(is, line)) { + lineNumber++; + const bool hasErr = errLines.count(lineNumber); + if (hasErr) { + os << terminal.background(Terminal::RED); + } + os << terminal.color(Terminal::BLACK, !hasErr) << (hasErr ? "!" : " ") + << std::setw(3) << lineNumber << ":" << terminal.reset() << " " + << line << std::endl; + } +} + +void Logger::headline(const std::string &msg) +{ + os << std::endl << "== " << terminal.bright() << msg << terminal.reset() + << " ==" << std::endl; +} +} +} diff --git a/test/integration/TestLogger.hpp b/test/integration/TestLogger.hpp new file mode 100644 index 0000000..1c18191 --- /dev/null +++ b/test/integration/TestLogger.hpp @@ -0,0 +1,63 @@ +/* + Ousía + Copyright (C) 2015 Benjamin Paaßen, Andreas Stöckel + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file TestLogger.hpp + * + * Class used to print messages and streams in the integration test utility. + * + * @author Andreas Stöckel (astoecke@techfak.uni-bielefeld.de) + */ + +#ifndef _OUSIA_TEST_LOGGER_HPP_ +#define _OUSIA_TEST_LOGGER_HPP_ + +#include <ostream> +#include <istream> +#include <set> +#include <string> + +#include <core/frontend/Terminal.hpp> + +namespace ousia { +namespace test { + +/** + * Class used for printing messages to the screen. + */ +class Logger { +private: + std::ostream &os; + Terminal terminal; + +public: + Logger(std::ostream &os, bool useColor); + + void fail(const std::string &msg); + void success(const std::string &msg); + void note(const std::string &msg); + void result(std::istream &is, + const std::set<int> &errLines = std::set<int>{}); + void headline(const std::string &msg); +}; + +} +} + +#endif /* _OUSIA_TEST_LOGGER_HPP_ */ + diff --git a/test/integration/TestXmlParser.cpp b/test/integration/TestXmlParser.cpp new file mode 100644 index 0000000..b9eae0e --- /dev/null +++ b/test/integration/TestXmlParser.cpp @@ -0,0 +1,249 @@ +/* + Ousía + Copyright (C) 2015 Benjamin Paaßen, Andreas Stöckel + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <expat.h> + +#include <core/common/Utils.hpp> + +#include "TestXmlParser.hpp" + +namespace ousia { +namespace test { + +/* Class XmlNode */ + +std::string XmlNode::path() +{ + std::string p; + if (!parent.expired()) { + std::shared_ptr<XmlNode> parentPtr = parent.lock(); + if (parentPtr.get() != nullptr) { + p = parentPtr->path() + "/"; + } + } + return p + name; +} + +bool XmlNode::compareTo(Logger &logger, std::shared_ptr<XmlNode> other, + std::set<int> &errExpected, std::set<int> &errActual) +{ + bool ok = true; + + // Compare name and text + if (name != other->name) { + logger.fail(path() + ": names differ, expected \"" + name + + "\", but got \"" + other->name + "\""); + ok = false; + } + if (text != other->text) { + logger.fail(path() + ": texts differ, expected \"" + text + + "\", but got \"" + other->text + "\""); + ok = false; + } + + // Compare the attributes + if (attributes.size() != other->attributes.size()) { + logger.fail( + path() + ": attribute count differs, expected " + + std::to_string(attributes.size()) + " attributes, but got " + + std::to_string(other->attributes.size()) + " attributes"); + ok = false; + } + for (const auto &attribute : attributes) { + auto it = other->attributes.find(attribute.first); + if (it == other->attributes.end()) { + logger.fail(path() + ": attribute \"" + attribute.first + + "\" is missing in actual output"); + ok = false; + } else if (it->second != attribute.second) { + logger.fail(path() + ": expected \"" + attribute.second + + "\" for attribute \"" + attribute.first + + "\" but got \"" + it->second + "\""); + ok = false; + } + } + + // Compare the children + if (children.size() != other->children.size()) { + logger.fail(path() + ": children count differs, expected " + + std::to_string(children.size()) + + " children, but got " + + std::to_string(other->children.size()) + " children"); + ok = false; + } + + // Store the actual position + if (!ok) { + logger.fail("Location in expected output is " + + std::to_string(line) + ":" + std::to_string(column) + + ", location in actual output is " + + std::to_string(other->line) + ":" + + std::to_string(other->column)); + errExpected.insert(line); + errActual.insert(other->line); + } + + // Compare the children + const size_t count = std::min(children.size(), other->children.size()); + for (size_t i = 0; i < count; i++) { + ok = children[i]->compareTo(logger, other->children[i], errExpected, + errActual) & + ok; + } + + return ok; +} + +static const std::vector<std::string> IGNORE_TAGS{"import"}; +static const std::vector<std::string> IGNORE_ATTRS{"xmlns"}; + +static bool checkIgnore(const std::vector<std::string> &ignoreList, + const std::string &name) +{ + for (const auto &s : ignoreList) { + if (Utils::startsWith(s, name)) { + return true; + } + } + return false; +} + +/** + * Callback called by eXpat whenever a start handler is reached. + */ +static void xmlStartElementHandler(void *ref, const XML_Char *name, + const XML_Char **attrs) +{ + XML_Parser parser = static_cast<XML_Parser>(ref); + std::shared_ptr<XmlNode> &node = + *(static_cast<std::shared_ptr<XmlNode> *>(XML_GetUserData(parser))); + + // Store the child node in the parent node, check for ignoring nodes once + // an element ends + std::shared_ptr<XmlNode> childNode = + std::make_shared<XmlNode>(node, name); + childNode->line = XML_GetCurrentLineNumber(parser); + childNode->column = XML_GetCurrentColumnNumber(parser); + node->children.push_back(childNode); + node = childNode; + + // Assemble the node attributes + const XML_Char **attr = attrs; + while (*attr) { + // Convert the C string to a std::string + const std::string key{*(attr++)}; + const std::string value{*(attr++)}; + + // Ignore certain attributes + if (!checkIgnore(IGNORE_ATTRS, key)) { + childNode->attributes.emplace(key, value); + } + } +} + +static void xmlEndElementHandler(void *ref, const XML_Char *name) +{ + XML_Parser parser = static_cast<XML_Parser>(ref); + std::shared_ptr<XmlNode> &node = + *(static_cast<std::shared_ptr<XmlNode> *>(XML_GetUserData(parser))); + + // Set the current node to the parent node + node = node->parent.lock(); + + // If the child node should have been ignored, remove it now + if (checkIgnore(IGNORE_TAGS, name)) { + node->children.pop_back(); + } +} + +static void xmlCharacterDataHandler(void *ref, const XML_Char *s, int len) +{ + // Fetch a reference at the currently active node + XML_Parser parser = static_cast<XML_Parser>(ref); + std::shared_ptr<XmlNode> &node = + *(static_cast<std::shared_ptr<XmlNode> *>(XML_GetUserData(parser))); + + // Store a new text node in the current node + std::string text = Utils::trim(std::string(s, len)); + if (!text.empty()) { + std::shared_ptr<XmlNode> textNode = + std::make_shared<XmlNode>(node, "$text"); + textNode->text = text; + textNode->line = XML_GetCurrentLineNumber(parser); + textNode->column = XML_GetCurrentColumnNumber(parser); + node->children.push_back(textNode); + } +} + +std::pair<bool, std::shared_ptr<XmlNode>> parseXml( + Logger &logger, std::istream &is, std::set<int> &errLines) +{ + std::shared_ptr<XmlNode> root = std::make_shared<XmlNode>(); + std::shared_ptr<XmlNode> currentNode = root; + + XML_Parser parser = XML_ParserCreate("UTF-8"); + + // Pass the reference to this parser instance to the XML handler + XML_UseParserAsHandlerArg(parser); + XML_SetUserData(parser, ¤tNode); + + // Set the callback functions + XML_SetStartElementHandler(parser, xmlStartElementHandler); + XML_SetEndElementHandler(parser, xmlEndElementHandler); + XML_SetCharacterDataHandler(parser, xmlCharacterDataHandler); + + // Feed data into expat while there is data to process + constexpr size_t BUFFER_SIZE = 64 * 1024; + bool ok = true; + while (true) { + // Fetch a buffer from expat for the input data + char *buf = static_cast<char *>(XML_GetBuffer(parser, BUFFER_SIZE)); + if (!buf) { + logger.fail("Cannot parse XML, out of memory"); + ok = false; + break; + } + + // Read into the buffer + size_t bytesRead = is.read(buf, BUFFER_SIZE).gcount(); + + // Parse the data and handle any XML error as exception + if (!XML_ParseBuffer(parser, bytesRead, bytesRead == 0)) { + int line = XML_GetCurrentLineNumber(parser); + int column = XML_GetCurrentColumnNumber(parser); + logger.fail("Cannot parse XML, " + + std::string(XML_ErrorString(XML_GetErrorCode(parser))) + + ", at line " + std::to_string(line) + ", column " + + std::to_string(column)); + errLines.insert(line); + ok = false; + break; + } + + // Abort once there are no more bytes in the stream + if (bytesRead == 0) { + break; + } + } + + return std::pair<bool, std::shared_ptr<XmlNode>>(ok, root); +} + +} +} + diff --git a/test/integration/TestXmlParser.hpp b/test/integration/TestXmlParser.hpp new file mode 100644 index 0000000..d6a2c45 --- /dev/null +++ b/test/integration/TestXmlParser.hpp @@ -0,0 +1,89 @@ +/* + Ousía + Copyright (C) 2015 Benjamin Paaßen, Andreas Stöckel + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file TestXmlParser.hpp + * + * Contains a method to parse a XML file into a simple tree representation. + * + * @author Andreas Stöckel (astoecke@techfak.uni-bielefeld.de) + */ + +#ifndef _OUSIA_TEST_XML_PARSER_HPP_ +#define _OUSIA_TEST_XML_PARSER_HPP_ + +#include <memory> +#include <map> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "TestLogger.hpp" + +namespace ousia { +namespace test { + +/** + * Class used to represent a XML file. + */ +struct XmlNode { + std::weak_ptr<XmlNode> parent; + std::vector<std::shared_ptr<XmlNode>> children; + std::map<std::string, std::string> attributes; + std::string name; + std::string text; + int line; + int column; + + /** + * Default constructor, creates a root node. + */ + XmlNode() : line(0), column(0) {} + + /** + * Creates a node for the given parent with the given name. + * + * @param parent is the parent XML node. + */ + XmlNode(std::weak_ptr<XmlNode> parent, const std::string &name) + : parent(parent), name(name), line(0), column(0) + { + } + + /** + * Returns the path to this node. + */ + std::string path(); + + /** + * Compares two XmlNode instances, logs differences to the given + * test::logger instance. + */ + bool compareTo(Logger &logger, std::shared_ptr<XmlNode> other, + std::set<int> &errExpected, std::set<int> &errActual); +}; + +std::pair<bool, std::shared_ptr<XmlNode>> parseXml(Logger &logger, + std::istream &is, + std::set<int> &errLines); + +} +} + +#endif /* _OUSIA_TEST_XML_PARSER_HPP_ */ |