Commit cb2de32a authored by Pádraig Ó Conbhuí's avatar Pádraig Ó Conbhuí Committed by Padraig O Conbhui
Browse files

Add Rattlehead project

The Rattlehead project is intended to be used to test the correctness of
Megadep, and to benchmark it in a fine-tunable manner.

Rattlehead generates test project layouts with nested directories and
acyclic dependencies between files.
Project files contain #include directives to a number of other files in the
project to create dependencies between files, suitable for consumption by
Megadep.

Rattlehead has been added in tests/src.
parent 5b7c1c65
Pipeline #738 passed with stages
in 3 minutes and 6 seconds
......@@ -18,11 +18,7 @@ add_library(
libmegadep
src/megadep/filesystem.cpp
)
set_target_properties(
libmegadep
PROPERTIES
OUTPUT_NAME "megadep"
)
set_target_properties(libmegadep PROPERTIES OUTPUT_NAME "megadep")
target_compile_features(libmegadep PUBLIC cxx_std_17)
target_include_directories(libmegadep PUBLIC src)
target_link_system_libraries(libmegadep PUBLIC CLI11::CLI11)
......@@ -46,7 +42,7 @@ if(MEGADEP_ENABLE_TESTING)
src/megadep/filesystem.test.cpp
)
target_link_libraries(
libmegadep.test PRIVATE libmegadep medagep::catch2_runner
libmegadep.test PRIVATE libmegadep megadep::catch2_runner
)
target_include_directories(libmegadep.test PRIVATE src)
......
......@@ -8,4 +8,25 @@ add_library(
target_link_system_libraries(catch2_runner PUBLIC Catch2::Catch2)
target_compile_features(catch2_runner PUBLIC cxx_std_17)
add_library(medagep::catch2_runner ALIAS catch2_runner)
add_library(megadep::catch2_runner ALIAS catch2_runner)
add_library(librattlehead INTERFACE)
target_include_directories(librattlehead INTERFACE src)
target_compile_features(librattlehead INTERFACE cxx_std_17)
add_executable(rattlehead src/rattlehead.main.cpp)
target_link_libraries(rattlehead PRIVATE librattlehead)
target_link_system_libraries(rattlehead PRIVATE CLI11::CLI11)
enable_testing()
add_executable(rattlehead.test src/rattlehead/rattlehead.test.cpp)
target_link_libraries(
rattlehead.test PRIVATE librattlehead megadep::catch2_runner
)
add_test(
NAME rattlehead.test
COMMAND rattlehead.test
)
#include <rattlehead/rattlehead.hpp>
#include <CLI/CLI.hpp>
#include <algorithm>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
int main(int argc, char* argv[])
{
using namespace megadep;
// Default values
size_t number_of_files = 1;
size_t files_per_directory = 1;
size_t directories_per_directory = 1;
size_t includes_per_file = 1;
size_t random_seed = 0;
// Command line options
CLI::App app{"rattlehead: random project generator", "rattlehead"};
app.add_option("--files,-n", number_of_files, "Number of files", true)
->required()
->type_name("<count>");
app.add_option(
"--files-per-dir,-d", files_per_directory, "Files per directory",
true)
->required()
->type_name("<count>");
app.add_option(
"--dirs-per-dir,-r", directories_per_directory,
"Directories per directory", true)
->required()
->type_name("<count>");
app.add_option(
"--includes,-i", includes_per_file, "Includes per file", true)
->required()
->type_name("<count>");
app.add_option("--seed,-s", random_seed, "Random seed", true)
->type_name("<unsigned>");
// Run parser
try {
app.parse(argc, argv);
}
catch (const CLI::ParseError& e) {
return app.exit(e);
}
// Run rattlehead
const auto layout = rattlehead::project_layout(
number_of_files, files_per_directory, directories_per_directory);
const auto dependency_graph = rattlehead::random_project_dependency_graph(
number_of_files, includes_per_file, random_seed);
rattlehead::build_project(
layout, dependency_graph, rattlehead::posix_path_creation_policy{},
rattlehead::default_path_policy{});
}
#ifndef MEGADEP_TEST_RATTLEHEAD_HPP
#define MEGADEP_TEST_RATTLEHEAD_HPP
#include <algorithm>
#include <array>
#include <cassert>
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <random>
#include <string_view>
#include <type_traits>
#include <vector>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace megadep {
namespace rattlehead {
template<typename CountType = size_t>
class counting_iterator {
CountType count_ = 0;
public:
using iterator_category = std::random_access_iterator_tag;
using value_type = CountType;
using difference_type = std::make_unsigned_t<CountType>;
using pointer = CountType*;
using reference = CountType&;
counting_iterator() = default;
counting_iterator(CountType count) : count_(count) {}
value_type operator*() const { return count_; }
counting_iterator& operator++()
{
count_++;
return *this;
}
counting_iterator operator++(int)
{
auto copy = *this;
++(*this);
return copy;
}
friend counting_iterator operator+(counting_iterator it, value_type n)
{
it.count_ += n;
return it;
}
friend counting_iterator operator+(value_type n, counting_iterator it)
{
return it + n;
}
counting_iterator operator+(size_t n)
{
auto copy = *this;
copy.count_ += n;
return copy;
}
counting_iterator& operator+=(const counting_iterator& other)
{
count_ += other.count_;
return *this;
}
counting_iterator& operator--()
{
count_--;
return *this;
}
counting_iterator operator--(int)
{
auto copy = *this;
--(*this);
return copy;
}
friend difference_type operator-(counting_iterator a, counting_iterator b)
{
return static_cast<difference_type>(a.count_)
- static_cast<difference_type>(b.count_);
}
friend counting_iterator operator-(counting_iterator it, value_type n)
{
it.count_ -= n;
return it;
}
counting_iterator operator-(size_t n)
{
auto copy = *this;
copy.count_ -= n;
return copy;
}
counting_iterator& operator-=(const counting_iterator& other)
{
count_ -= other.count_;
return *this;
}
bool operator==(counting_iterator other) const
{
return count_ == other.count_;
}
bool operator!=(counting_iterator other) const
{
return count_ != other.count_;
}
bool operator>(counting_iterator other) const
{
return count_ > other.count_;
}
bool operator>=(counting_iterator other) const
{
return count_ >= other.count_;
}
bool operator<(counting_iterator other) const
{
return count_ < other.count_;
}
bool operator<=(counting_iterator other) const
{
return count_ <= other.count_;
}
};
// Create a map of index -> indices, representing a directed acyclic graph
std::vector<std::vector<size_t>> random_project_dependency_graph(
size_t number_of_files, size_t includes_per_file, size_t seed = 0)
{
std::vector<std::vector<size_t>> dependency_graph;
std::mt19937_64 rand(seed);
static_assert(
std::is_unsigned_v<decltype(rand)::result_type>,
"Expected mt19937_64 to use an unsigned type!");
for (size_t new_file = 0; new_file < number_of_files; new_file++) {
auto& new_file_dependencies = dependency_graph.emplace_back();
// clang++ 8.0.0-3 on Ubuntu 19.04 divides by zero on empty range
if (new_file != 0) {
std::sample(
counting_iterator<size_t>(0),
counting_iterator<size_t>(new_file),
std::back_inserter(new_file_dependencies), includes_per_file,
rand);
}
}
return dependency_graph;
}
constexpr size_t project_layout_root = 0;
// Generate a structured project layout with a given number of files,
// a given number of files per directory, and subdirectories per directory.
// Directory and file names are represented by a list of indices.
// expects:
// files_per_directory > 0
// directories_per_directory == 0 -> files_per_directory =
// number_of_files
std::vector<std::vector<size_t>> project_layout(
size_t number_of_files,
size_t files_per_directory,
size_t directories_per_directory)
{
std::vector<std::vector<size_t>> layout;
size_t last_directory_index = 0;
size_t last_file_index = 0;
std::vector<size_t> current_directory;
current_directory.emplace_back(0);
for (size_t file_index = 0; file_index < number_of_files; file_index++) {
current_directory.clear();
size_t directory_depth = 0;
size_t number_of_sibling_directories = 1;
size_t directory_index_start = 0;
while (true) {
if (directory_index_start * files_per_directory > file_index) {
break;
}
size_t sibling_directory_index =
file_index % number_of_sibling_directories;
current_directory.emplace_back(
directory_index_start + sibling_directory_index);
directory_index_start += number_of_sibling_directories;
if (number_of_sibling_directories == 1) {
number_of_sibling_directories =
std::max<size_t>(directories_per_directory, 1);
}
else {
number_of_sibling_directories *= number_of_sibling_directories;
}
}
current_directory.push_back(file_index);
layout.emplace_back(current_directory);
current_directory.pop_back();
// All layouts should start with project_layout_root
assert(layout.back().front() == project_layout_root);
}
return layout;
}
class default_path_policy {
// Name of the root directory of the path, directory index 0.
std::string m_root_directory_name;
public:
default_path_policy(std::string root_directory_name = "d_0") :
m_root_directory_name(std::move(root_directory_name))
{
}
// Returns the new buffer end position
template<typename PathIterator>
char* directory_path(
char* buffer,
size_t max_buffer_size,
const PathIterator& begin,
const PathIterator& end) const
{
static_assert(
std::is_integral_v<std::decay_t<decltype(*begin)>>,
"PathIterator must be an iterator over an integral type!");
for (PathIterator it = begin; it != end; ++it) {
auto written = ([&] {
// Directory index 0 is the root directory
if (*it == project_layout_root) {
return std::snprintf(
buffer, max_buffer_size, "%s/",
m_root_directory_name.c_str());
}
else {
if (std::is_unsigned_v<std::decay_t<decltype(*it)>>) {
return std::snprintf(
buffer, max_buffer_size, "d_%" PRIu64 "/",
static_cast<uint64_t>(*it));
}
else {
return std::snprintf(
buffer, max_buffer_size, "d_%" PRId64 "/",
static_cast<int64_t>(*it));
}
}
}());
assert(written > 0 && "An encoding error occurred during snprintf");
assert(
written < max_buffer_size && "The input buffer is too small");
buffer += written;
max_buffer_size -= written;
}
return buffer;
}
template<typename PathIterator>
char* file_path(
char* buffer,
size_t max_buffer_size,
const PathIterator& begin,
const PathIterator& end) const
{
static_assert(
std::is_integral_v<std::decay_t<decltype(*begin)>>,
"PathIterator must be an iterator over an integral type!");
if (begin == end) return buffer;
auto* new_buffer_pos = default_path_policy::directory_path(
buffer, max_buffer_size, begin, std::prev(end));
max_buffer_size = max_buffer_size - (new_buffer_pos - buffer);
buffer = new_buffer_pos;
auto written = ([&] {
if (std::is_unsigned_v<std::decay_t<decltype(*begin)>>) {
return std::snprintf(
buffer, max_buffer_size, "f_%" PRIu64 ".c",
static_cast<uint64_t>(*std::prev(end)));
}
else {
return std::snprintf(
buffer, max_buffer_size, "f_%" PRId64 ".c",
static_cast<uint64_t>(*std::prev(end)));
}
}());
assert(written > 0 && "An encoding error occurred during snprintf");
assert(written < max_buffer_size && "The input buffer is too small");
return buffer + written;
}
};
class posix_path_creation_policy {
public:
template<typename PathIterator, typename PathPolicy>
static void create_path(
const PathIterator& path_begin,
const PathIterator& path_end,
PathPolicy&& path_policy)
{
if (path_begin == path_end) return;
std::array<char, 4096> path_buffer;
char* path_buffer_pos = path_buffer.data();
const auto remaining_buffer = [&] {
return path_buffer.size()
- std::distance(path_buffer.data(), path_buffer_pos);
};
// If we have at least one directory entry (i.e. 2 path entries)
if (path_begin != std::prev(path_end)) {
for (auto it = path_begin; it != std::prev(path_end); ++it) {
path_buffer_pos = path_policy.directory_path(
path_buffer_pos, remaining_buffer(), it, std::next(it));
int err = mkdir(path_buffer.data(), 0755);
if (err == -1) {
assert(
errno == EEXIST
&& "Something went wrong creating a directory");
}
}
}
path_policy.file_path(
path_buffer_pos, remaining_buffer(), std::prev(path_end), path_end);
int fd = open(path_buffer.data(), O_CREAT, 0755);
close(fd);
}
};
template<
typename Layout,
typename DependencyGraph,
typename PathCreationPolicy,
typename PathPolicy>
void build_project(
const Layout& layout,
const DependencyGraph dependency_graph,
PathCreationPolicy&& path_creation_policy,
PathPolicy&& path_policy)
{
auto layout_it = std::begin(layout);
auto dependency_graph_it = std::begin(dependency_graph);
for (size_t file_index = 0; file_index != layout.size(); file_index++) {
const auto& file_path = layout[file_index];
const auto& dependencies = dependency_graph[file_index];
path_creation_policy.create_path(
std::begin(file_path), std::end(file_path), path_policy);
std::array<char, 4096> path_buffer;
path_policy.file_path(
path_buffer.data(), path_buffer.size(), std::begin(file_path),
std::end(file_path));
std::ofstream file(
path_buffer.data(), std::ofstream::out | std::ofstream::trunc);
for (size_t dependency_index = 0;
dependency_index != dependencies.size(); dependency_index++) {
const auto& dependency_path =
layout[dependencies[dependency_index]];
// pre-copy #include into path_buffer
constexpr std::string_view include_start = "#include \"";
include_start.copy(path_buffer.data(), include_start.size());
char* path_pos = path_buffer.data() + include_start.size();
size_t path_size_remaining =
path_buffer.size() - include_start.size();
path_pos = path_policy.file_path(
path_pos, path_size_remaining, std::begin(dependency_path),
std::end(dependency_path));
// Close #include quotes and add \0.
*path_pos = '\"';
path_pos++;
*path_pos = '\0';
file << path_buffer.data() << '\n';
}
}
}
} // namespace rattlehead
} // namespace megadep
#endif // MEGADEP_TEST_RATTLEHEAD_HPP
#include <rattlehead/rattlehead.hpp>
#include <catch2/catch.hpp>
#include <algorithm>
#include <fstream>
#include <iterator>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include <unistd.h>
namespace rattlehead = megadep::rattlehead;
TEST_CASE("rattlehead::random_project_dependency_graph")
{
size_t number_of_files = GENERATE(0, 1, 2, 10, 100, 1000);
size_t includes_per_file = GENERATE(0, 1, 2, 10, 100, 1000);
size_t seed = GENERATE(0, 1, 2);
auto project_graph = rattlehead::random_project_dependency_graph(
number_of_files, includes_per_file, seed);
REQUIRE(project_graph.size() == number_of_files);
{
INFO("The dependencies per file should match the requested number");
for (size_t index = 0; index < project_graph.size(); index++) {
// for index < includes_per_file, the size of the dependencies
// can be at most index.
REQUIRE(
project_graph[index].size()
== std::min(index, includes_per_file));
}
}
{
INFO("The project graph should be acyclic");
REQUIRE([&] {
for (size_t index = 0; index < project_graph.size(); index++) {
for (const auto& dependency : project_graph[index]) {
if (dependency > index) return false;
}
}
return true;
}());
}
}
TEST_CASE("rattlehead::project_layout")
{
size_t number_of_files = GENERATE(0, 1, 2, 5, 10, 100, 1000);
size_t directories_per_directory = GENERATE(0, 1, 2, 5, 10, 1000);
// When directories_per_directory > 0:
// files_per_directory > 0
// When directories_per_directory == 0:
// files_per_directory = number_of_files
size_t files_per_directory = GENERATE_REF(filter(
[&](size_t value) {