/*
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 .
*/
/**
* @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 // Non-portable, needed for isatty
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "TestXmlParser.hpp"
#include "TestLogger.hpp"
using namespace ousia;
namespace fs = boost::filesystem;
namespace {
const size_t SUCCESS = 0;
const size_t ERROR = 1;
/**
* Removes the prefix string "prefix" from the given string "s".
*
* @param s is the string from which the prefix should be removed.
* @param prefix is the prefix that should be removed from s.
* @return s with the prefix removed.
*/
std::string removePrefix(const std::string &s, const std::string &prefix)
{
if (s.size() > prefix.size()) {
return s.substr(prefix.size(), s.size() - prefix.size());
}
return std::string{};
}
/**
* Structure representing a single test case.
*/
struct Test {
/**
* Test name.
*/
std::string name;
/**
* 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 name is the name of the test.
* @param infile is the input file.
* @param outfile is the output file containing the expected input.
*/
Test(const std::string &name, const std::string &infile,
const std::string &outfile)
: name(name),
infile(infile),
outfile(outfile),
shouldFail(false),
success(false)
{
}
/**
* Constructor for a test with expected failure.
*
* @param name is the name of the test.
* @param infile is the input file.
*/
Test(const std::string &name, const std::string &infile)
: name(name), 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{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 docNode =
context.import(infile, "", "", {&RttiTypes::Document});
if (logger.hasError() || docNode == nullptr) {
return false;
}
Rooted doc = docNode.cast();
xml::XmlTransformer transform;
transform.writeXml(doc, os, logger, resourceManager, true);
return true;
}
static bool runTest(test::Logger &logger, const Test &test,
const std::string &targetFile)
{
// Parse the infile and dump it as OSXML to a string stream
logger.note("Parsing " + test.infile);
std::stringstream actual_output;
bool res = parseFile(test.infile, actual_output);
// Write the actual_output to disk
{
logger.note("Writing serialized output to " + targetFile);
std::ofstream target(targetFile);
target << actual_output.str();
}
// 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;
}
// Parse both the actual output and the expected output stream
std::ifstream expected_output(test.outfile);
logger.note("Parsing serialized XML");
std::set 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("Actual result:");
logger.result(actual_output, errActual);
logger.note("Expected result:");
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 gatherTests(fs::path root)
{
// Result list
std::vector 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 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
std::string inPath = p.native();
std::string testName = p.filename().native();
std::string testPath;
// Check whether the test ends with ".in.osml" or ".in.osxml"
bool shouldFail = false;
if (Utils::endsWith(inPath, ".in.osml")) {
testPath = inPath.substr(0, inPath.size() - 8);
testName = testName.substr(0, testName.size() - 8);
} else if (Utils::endsWith(inPath, ".in.osxml")) {
testPath = inPath.substr(0, inPath.size() - 9);
testName = testName.substr(0, testName.size() - 9);
} else if (Utils::endsWith(inPath, ".fail.osml")) {
testPath = inPath.substr(0, inPath.size() - 10);
testName = testName.substr(0, testName.size() - 10);
shouldFail = true;
} else if (Utils::endsWith(inPath, ".fail.osxml")) {
testPath = inPath.substr(0, inPath.size() - 11);
testName = testName.substr(0, testName.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 filename the result
if (!testPath.empty()) {
if (shouldFail) {
res.emplace_back(testName, testPath);
} else {
const std::string outPath = testPath + ".out.osxml";
if (fs::is_regular_file(outPath)) {
res.emplace_back(testName, inPath, outPath);
}
}
}
}
}
}
// 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");
// Parse the arguments
std::set filter;
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg == "-h" || arg == "--help") {
logger.headline("USAGE");
logger.note("To execute all tests run:");
logger.note("\t\t./ousia_test_integration");
logger.note("To execute specific tests run:");
logger.note("\t\t./ousia_test_integration [TEST NAMES]");
logger.note("To display help run one of:");
logger.note("\t\t./ousia_test_integration -h");
logger.note("\t\t./ousia_test_integration --help");
return SUCCESS;
} else {
filter.insert(arg);
}
}
// Check whether the root path exists, make it a canonical path
logger.headline("GATHER TESTS");
fs::path root =
fs::path(SpecialPaths::getDebugTestdataDir()) / "integration";
if (!fs::is_directory(root)) {
logger.fail("Could not find integration test data directory: " +
root.native());
#ifdef NDEBUG
logger.note(
"This is a release build, copy the \"testdata\" folder into the "
"same directory as the executable.");
#endif
return ERROR;
}
root = fs::canonical(root);
// Fetch all test cases
std::vector tests = gatherTests(root);
std::string testsWord = tests.size() == 1 ? " test" : " tests";
logger.note(std::to_string(tests.size()) + testsWord + " found");
// Run them, count the number of successes and failures
logger.headline("RUN TESTS");
size_t successCount = 0;
size_t failureCount = 0;
for (auto &test : tests) {
// Filter tests by name
if (!filter.empty() && !filter.count(test.name)) {
test.success = true;
continue;
}
logger.headline("Test \"" + test.name + "\"");
// Create the target directory (use CTest folder)
fs::path target =
fs::path("Testing") / fs::path("Integration") /
removePrefix(fs::path(test.infile).parent_path().native(),
root.native()) /
(test.name + ".out.osxml");
fs::path targetDir = target.parent_path();
if (!fs::is_directory(targetDir) &&
!fs::create_directories(targetDir)) {
logger.fail("Cannot create or access directory " +
targetDir.native());
return ERROR;
}
// Assemble the full target file path
if (runTest(logger, test, target.native())) {
test.success = true;
successCount++;
} else {
failureCount++;
}
}
// Write the summary
logger.headline("TEST SUMMARY");
size_t count = successCount + failureCount;
testsWord = count == 1 ? " test" : " tests";
logger.note(std::string("Ran ") +
std::to_string(failureCount + successCount) + testsWord + ", " +
std::to_string(failureCount) + " failed, " +
std::to_string(successCount) + " succeeded");
if (failureCount > 0) {
logger.note("The following tests failed:");
for (const auto &test : tests) {
if (!test.success) {
logger.fail(test.name + " (" + test.infile + ")");
}
}
} else {
logger.success("All tests completed successfully!");
}
// Inform the shell about failing integration tests
return failureCount > 0 ? ERROR : SUCCESS;
}