IAP GITLAB

Skip to content
Snippets Groups Projects
Commit 8be20b2e authored by ralfulrich's avatar ralfulrich
Browse files

some small fixed, and test coverage

parent 6a7bc574
No related branches found
No related tags found
No related merge requests found
Showing
with 369 additions and 77 deletions
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
namespace corsika { namespace corsika {
ObservationPlaneWriterParquet::ObservationPlaneWriterParquet() inline ObservationPlaneWriterParquet::ObservationPlaneWriterParquet()
: output_() {} : output_() {}
void ObservationPlaneWriterParquet::startOfLibrary( inline void ObservationPlaneWriterParquet::startOfLibrary(
boost::filesystem::path const& directory) { boost::filesystem::path const& directory) {
// setup the streamer // setup the streamer
...@@ -34,24 +34,16 @@ namespace corsika { ...@@ -34,24 +34,16 @@ namespace corsika {
// and build the streamer // and build the streamer
output_.buildStreamer(); output_.buildStreamer();
setInit(true);
} }
void ObservationPlaneWriterParquet::endOfShower() { ++shower_; } inline void ObservationPlaneWriterParquet::endOfShower() { ++shower_; }
void ObservationPlaneWriterParquet::endOfLibrary() { output_.closeStreamer(); }
void ObservationPlaneWriterParquet::write(Code const& pid, HEPEnergyType const& energy, inline void ObservationPlaneWriterParquet::endOfLibrary() { output_.closeStreamer(); }
LengthType const& x, LengthType const& y) {
if (!isInit()) {
std::runtime_error(
"ObservationPlaneWriterParquet not initialized. Either 1) add the "
"corresponding module to "
"the OutputManager, or 2) declare the module to write no output using "
"NoOutput.");
}
inline void ObservationPlaneWriterParquet::write(Code const& pid,
HEPEnergyType const& energy,
LengthType const& x,
LengthType const& y) {
// write the next row - we must write `shower_` first. // write the next row - we must write `shower_` first.
*(output_.getWriter()) << shower_ << static_cast<int>(get_PDG(pid)) *(output_.getWriter()) << shower_ << static_cast<int>(get_PDG(pid))
<< static_cast<float>(energy / 1_GeV) << static_cast<float>(energy / 1_GeV)
......
...@@ -10,10 +10,11 @@ ...@@ -10,10 +10,11 @@
namespace corsika { namespace corsika {
TrackWriterParquet::TrackWriterParquet() inline TrackWriterParquet::TrackWriterParquet()
: output_() {} : output_() {}
void TrackWriterParquet::startOfLibrary(boost::filesystem::path const& directory) { inline void TrackWriterParquet::startOfLibrary(
boost::filesystem::path const& directory) {
// setup the streamer // setup the streamer
output_.initStreamer((directory / "tracks.parquet").string()); output_.initStreamer((directory / "tracks.parquet").string());
...@@ -38,24 +39,15 @@ namespace corsika { ...@@ -38,24 +39,15 @@ namespace corsika {
// and build the streamer // and build the streamer
output_.buildStreamer(); output_.buildStreamer();
setInit(true);
} }
void TrackWriterParquet::endOfShower() { ++shower_; } inline void TrackWriterParquet::endOfShower() { ++shower_; }
void TrackWriterParquet::endOfLibrary() { output_.closeStreamer(); }
void TrackWriterParquet::write(Code const& pid, HEPEnergyType const& energy, inline void TrackWriterParquet::endOfLibrary() { output_.closeStreamer(); }
QuantityVector<length_d> const& start,
QuantityVector<length_d> const& end) {
if (!isInit()) { inline void TrackWriterParquet::write(Code const& pid, HEPEnergyType const& energy,
std::runtime_error( QuantityVector<length_d> const& start,
"TrackWriterParquet not initialized. Either 1) add the corresponding module to " QuantityVector<length_d> const& end) {
"the OutputManager, or 2) declare the module to write no output using "
"NoOutput.");
}
// write the next row - we must write `shower_` first. // write the next row - we must write `shower_` first.
// clang-format off // clang-format off
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
namespace corsika { namespace corsika {
BaseOutput::BaseOutput() inline BaseOutput::BaseOutput()
: shower_(0) {} : shower_(0) {}
} // namespace corsika } // namespace corsika
...@@ -9,19 +9,19 @@ ...@@ -9,19 +9,19 @@
namespace corsika { namespace corsika {
DummyOutputManager::DummyOutputManager() {} inline DummyOutputManager::DummyOutputManager() {}
DummyOutputManager::~DummyOutputManager() {} inline DummyOutputManager::~DummyOutputManager() {}
template <typename TOutput> template <typename TOutput>
void DummyOutputManager::add(std::string const& name, TOutput& output) {} inline void DummyOutputManager::add(std::string const&, TOutput&) {}
void DummyOutputManager::startOfLibrary() {} inline void DummyOutputManager::startOfLibrary() {}
void DummyOutputManager::startOfShower() {} inline void DummyOutputManager::startOfShower() {}
void DummyOutputManager::endOfShower() {} inline void DummyOutputManager::endOfShower() {}
void DummyOutputManager::endOfLibrary() {} inline void DummyOutputManager::endOfLibrary() {}
} // namespace corsika } // namespace corsika
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
namespace corsika { namespace corsika {
void OutputManager::writeNode(YAML::Node const& node, inline void OutputManager::writeNode(YAML::Node const& node,
boost::filesystem::path const& path) const { boost::filesystem::path const& path) const {
// construct a YAML emitter for this config file // construct a YAML emitter for this config file
YAML::Emitter out; YAML::Emitter out;
...@@ -38,7 +38,7 @@ namespace corsika { ...@@ -38,7 +38,7 @@ namespace corsika {
file << out.c_str() << std::endl; file << out.c_str() << std::endl;
} }
void OutputManager::writeTopLevelConfig() const { inline void OutputManager::writeTopLevelConfig() const {
YAML::Node config; YAML::Node config;
...@@ -51,7 +51,7 @@ namespace corsika { ...@@ -51,7 +51,7 @@ namespace corsika {
writeNode(config, root_ / ("config.yaml")); writeNode(config, root_ / ("config.yaml"));
} }
void OutputManager::writeTopLevelSummary() const { inline void OutputManager::writeTopLevelSummary() const {
YAML::Node config; YAML::Node config;
...@@ -89,7 +89,7 @@ namespace corsika { ...@@ -89,7 +89,7 @@ namespace corsika {
writeNode(config, root_ / ("summary.yaml")); writeNode(config, root_ / ("summary.yaml"));
} }
void OutputManager::initOutput(std::string const& name) const { inline void OutputManager::initOutput(std::string const& name) const {
// construct the path to this directory // construct the path to this directory
auto const path{root_ / name}; auto const path{root_ / name};
...@@ -106,7 +106,7 @@ namespace corsika { ...@@ -106,7 +106,7 @@ namespace corsika {
writeNode(config, path / "config.yaml"); writeNode(config, path / "config.yaml");
} }
OutputManager::OutputManager( inline OutputManager::OutputManager(
std::string const& name, std::string const& name,
boost::filesystem::path const& dir = boost::filesystem::current_path()) boost::filesystem::path const& dir = boost::filesystem::current_path())
: name_(name) : name_(name)
...@@ -114,20 +114,19 @@ namespace corsika { ...@@ -114,20 +114,19 @@ namespace corsika {
// check if this directory already exists // check if this directory already exists
if (boost::filesystem::exists(root_)) { if (boost::filesystem::exists(root_)) {
logger->warn( logger->error("Output directory '{}' already exists! Do not overwrite!.",
"Output directory '{}' already exists! This is currenty not supported.", root_.string());
root_.string());
throw std::runtime_error("Output directory already exists."); throw std::runtime_error("Output directory already exists.");
} }
// construct the directory for this library // construct the directory for this library
boost::filesystem::create_directory(root_); boost::filesystem::create_directories(root_);
// write the top level config file // write the top level config file
writeTopLevelConfig(); writeTopLevelConfig();
} }
OutputManager::~OutputManager() { inline OutputManager::~OutputManager() {
if (state_ == OutputState::ShowerInProgress) { if (state_ == OutputState::ShowerInProgress) {
// if this the destructor is called before the shower has been explicitly // if this the destructor is called before the shower has been explicitly
...@@ -148,13 +147,13 @@ namespace corsika { ...@@ -148,13 +147,13 @@ namespace corsika {
} }
template <typename TOutput> template <typename TOutput>
void OutputManager::add(std::string const& name, TOutput& output) { inline void OutputManager::add(std::string const& name, TOutput& output) {
// check if that name is already in the map // check if that name is already in the map
if (outputs_.count(name) > 0) { if (outputs_.count(name) > 0) {
logger->warn("'{}' is already registered. All outputs must have unique names.", logger->error("'{}' is already registered. All outputs must have unique names.",
name); name);
return; throw std::runtime_error("Output already exists. Do not overwrite!");
} }
// if we get here, the name is not already in the map // if we get here, the name is not already in the map
...@@ -165,7 +164,7 @@ namespace corsika { ...@@ -165,7 +164,7 @@ namespace corsika {
initOutput(name); initOutput(name);
} }
void OutputManager::startOfLibrary() { inline void OutputManager::startOfLibrary() {
// this is only valid when we haven't started a library // this is only valid when we haven't started a library
// or have already finished a library // or have already finished a library
...@@ -188,7 +187,7 @@ namespace corsika { ...@@ -188,7 +187,7 @@ namespace corsika {
state_ = OutputState::LibraryReady; state_ = OutputState::LibraryReady;
} }
void OutputManager::startOfShower() { inline void OutputManager::startOfShower() {
// if this is called and we still in the "no init" state, then // if this is called and we still in the "no init" state, then
// this is the first shower in the library so make sure we start it // this is the first shower in the library so make sure we start it
...@@ -204,7 +203,7 @@ namespace corsika { ...@@ -204,7 +203,7 @@ namespace corsika {
state_ = OutputState::ShowerInProgress; state_ = OutputState::ShowerInProgress;
} }
void OutputManager::endOfShower() { inline void OutputManager::endOfShower() {
for (auto& [name, output] : outputs_) { output.get().endOfShower(); } for (auto& [name, output] : outputs_) { output.get().endOfShower(); }
...@@ -212,7 +211,7 @@ namespace corsika { ...@@ -212,7 +211,7 @@ namespace corsika {
state_ = OutputState::LibraryReady; state_ = OutputState::LibraryReady;
} }
void OutputManager::endOfLibrary() { inline void OutputManager::endOfLibrary() {
// we can only call endOfLibrary when we have already started // we can only call endOfLibrary when we have already started
if (state_ == OutputState::NoInit) { if (state_ == OutputState::NoInit) {
......
...@@ -10,9 +10,10 @@ ...@@ -10,9 +10,10 @@
namespace corsika { namespace corsika {
ParquetStreamer::ParquetStreamer() {} inline ParquetStreamer::ParquetStreamer()
: isInit_(false) {}
void ParquetStreamer::initStreamer(std::string const& filepath) { inline void ParquetStreamer::initStreamer(std::string const& filepath) {
// open the file and connect it to our pointer // open the file and connect it to our pointer
PARQUET_ASSIGN_OR_THROW(outfile_, arrow::io::FileOutputStream::Open(filepath)); PARQUET_ASSIGN_OR_THROW(outfile_, arrow::io::FileOutputStream::Open(filepath));
...@@ -26,16 +27,16 @@ namespace corsika { ...@@ -26,16 +27,16 @@ namespace corsika {
} }
template <typename... TArgs> template <typename... TArgs>
void ParquetStreamer::addField(TArgs&&... args) { inline void ParquetStreamer::addField(TArgs&&... args) {
fields_.push_back(parquet::schema::PrimitiveNode::Make(args...)); fields_.push_back(parquet::schema::PrimitiveNode::Make(args...));
} }
void ParquetStreamer::enableCompression(int const /*level*/) { inline void ParquetStreamer::enableCompression(int const /*level*/) {
// builder_.compression(parquet::Compression::ZSTD); // builder_.compression(parquet::Compression::ZSTD);
// builder_.compression_level(level); // builder_.compression_level(level);
} }
void ParquetStreamer::buildStreamer() { inline void ParquetStreamer::buildStreamer() {
// build the top level schema // build the top level schema
schema_ = std::static_pointer_cast<parquet::schema::GroupNode>( schema_ = std::static_pointer_cast<parquet::schema::GroupNode>(
...@@ -45,13 +46,26 @@ namespace corsika { ...@@ -45,13 +46,26 @@ namespace corsika {
// and build the writer // and build the writer
writer_ = std::make_shared<parquet::StreamWriter>( writer_ = std::make_shared<parquet::StreamWriter>(
parquet::ParquetFileWriter::Open(outfile_, schema_, builder_.build())); parquet::ParquetFileWriter::Open(outfile_, schema_, builder_.build()));
// only now this object is ready to stream
isInit_ = true;
} }
void ParquetStreamer::closeStreamer() { inline void ParquetStreamer::closeStreamer() {
writer_.reset(); writer_.reset();
[[maybe_unused]] auto status = outfile_->Close(); [[maybe_unused]] auto status = outfile_->Close();
isInit_ = false;
} }
std::shared_ptr<parquet::StreamWriter> ParquetStreamer::getWriter() { return writer_; } inline std::shared_ptr<parquet::StreamWriter> ParquetStreamer::getWriter() {
if (!isInit()) {
throw std::runtime_error(
"ParquetStreamer not initialized. Either 1) add the "
"corresponding module to "
"the OutputManager, or 2) declare the module to write no output using "
"NoOutput.");
}
return writer_;
}
} // namespace corsika } // namespace corsika
...@@ -51,20 +51,21 @@ namespace corsika { ...@@ -51,20 +51,21 @@ namespace corsika {
/** /**
* Get any summary information for the entire library. * Get any summary information for the entire library.
*/ */
virtual YAML::Node getSummary() { return YAML::Node(); }; virtual YAML::Node getSummary() { return YAML::Node(); }
/** /**
* Flag to indicate readiness. * Flag to indicate readiness.
*/ */
bool isInit() const { return is_init_; } bool isInit() const { return is_init_; }
protected:
/** /**
* Set init flag. * Set init flag.
*/ */
void setInit(bool const v) { is_init_ = v; } void setInit(bool const v) { is_init_ = v; }
protected:
int shower_{0}; ///< The current event number. int shower_{0}; ///< The current event number.
private: private:
bool is_init_{false}; ///< flag to indicate readiness bool is_init_{false}; ///< flag to indicate readiness
}; };
......
...@@ -27,13 +27,6 @@ namespace corsika { ...@@ -27,13 +27,6 @@ namespace corsika {
*/ */
class ParquetStreamer { class ParquetStreamer {
protected:
parquet::WriterProperties::Builder builder_; ///< The writer properties builder.
parquet::schema::NodeVector fields_; ///< The fields in this file.
std::shared_ptr<parquet::schema::GroupNode> schema_; ///< The schema for this file.
std::shared_ptr<arrow::io::FileOutputStream> outfile_; ///< The output file.
std::shared_ptr<parquet::StreamWriter> writer_; ///< The stream writer to 'outfile'
public: public:
/** /**
* ParquetStreamer's take no constructor arguments. * ParquetStreamer's take no constructor arguments.
...@@ -71,7 +64,20 @@ namespace corsika { ...@@ -71,7 +64,20 @@ namespace corsika {
*/ */
std::shared_ptr<parquet::StreamWriter> getWriter(); std::shared_ptr<parquet::StreamWriter> getWriter();
}; // class ParquetHelper /**
* @return status of streamer
*/
bool isInit() const { return isInit_; }
private:
bool isInit_ = false; ///< flag to handle state of writer
parquet::WriterProperties::Builder builder_; ///< The writer properties builder.
parquet::schema::NodeVector fields_; ///< The fields in this file.
std::shared_ptr<parquet::schema::GroupNode> schema_; ///< The schema for this file.
std::shared_ptr<arrow::io::FileOutputStream> outfile_; ///< The output file.
std::shared_ptr<parquet::StreamWriter> writer_; ///< The stream writer to 'outfile'
}; // class ParquetStreamer
} // namespace corsika } // namespace corsika
#include <corsika/detail/output/ParquetStreamer.inl> #include <corsika/detail/output/ParquetStreamer.inl>
...@@ -3,3 +3,4 @@ add_subdirectory (framework) ...@@ -3,3 +3,4 @@ add_subdirectory (framework)
add_subdirectory (media) add_subdirectory (media)
add_subdirectory (stack) add_subdirectory (stack)
add_subdirectory (modules) add_subdirectory (modules)
add_subdirectory (output)
...@@ -28,7 +28,6 @@ using namespace corsika; ...@@ -28,7 +28,6 @@ using namespace corsika;
TEST_CASE("ObservationPlane", "[proccesses][observation_plane]") { TEST_CASE("ObservationPlane", "[proccesses][observation_plane]") {
logging::set_level(logging::level::trace); logging::set_level(logging::level::trace);
corsika_logger->set_pattern("[%n:%^%-8l%$]: %v");
auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen); auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
auto const& cs = *csPtr; auto const& cs = *csPtr;
......
set (test_output_sources
TestMain.cpp
testOutputManager.cpp
testDummyOutputManager.cpp
testParquetStreamer.cpp
#testWriterObservationPlane.cpp
#testWriterTrack.cpp
)
CORSIKA_ADD_TEST (testOutput SOURCES ${test_output_sources})
target_compile_definitions (
testOutput
PRIVATE
REFDATADIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
/*
* (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
*
* This software is distributed under the terms of the GNU General Public
* Licence version 3 (GPL Version 3). See file LICENSE for a full version of
* the license.
*/
#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one
// cpp file
#include <catch2/catch.hpp>
/*
* (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
*
* This software is distributed under the terms of the GNU General Public
* Licence version 3 (GPL Version 3). See file LICENSE for a full version of
* the license.
*/
#include <catch2/catch.hpp>
#include <corsika/output/DummyOutputManager.hpp>
#include <corsika/framework/core/Logging.hpp>
using namespace corsika;
class DummyOutput {};
TEST_CASE("DummyOutputManager", "interface") {
logging::set_level(logging::level::info);
// output manager performs nothing, no action, just interface
DummyOutputManager output;
DummyOutput test;
output.add("test", test);
output.startOfLibrary();
output.startOfShower();
output.endOfShower();
output.endOfLibrary();
}
/*
* (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
*
* This software is distributed under the terms of the GNU General Public
* Licence version 3 (GPL Version 3). See file LICENSE for a full version of
* the license.
*/
#include <catch2/catch.hpp>
#include <boost/filesystem.hpp>
#include <corsika/output/OutputManager.hpp>
#include <corsika/framework/core/Logging.hpp>
using namespace corsika;
struct DummyOutput : public BaseOutput {
mutable bool isConfig_ = false;
mutable bool isSummary_ = false;
bool startLibrary_ = false;
bool startShower_ = false;
bool endLibrary_ = false;
bool endShower_ = false;
void startOfLibrary(boost::filesystem::path const&) { startLibrary_ = true; }
void startOfShower() { startShower_ = true; }
void endOfShower() { endShower_ = true; }
void endOfLibrary() { endLibrary_ = true; }
YAML::Node getConfig() const {
isConfig_ = true;
return YAML::Node();
}
YAML::Node getSummary() {
isSummary_ = true;
return YAML::Node();
}
};
TEST_CASE("OutputManager") {
logging::set_level(logging::level::info);
SECTION("standard") {
// preparation
if (boost::filesystem::exists("./out_test")) {
boost::filesystem::remove_all("./out_test");
}
// output manager performs nothing, no action, just interface
OutputManager output("check", "./out_test");
CHECK(boost::filesystem::is_directory("./out_test/check"));
DummyOutput test;
output.add("test", test);
CHECK_THROWS(output.add(
"test",
test)); // should emit warning which cannot be catched, but no action or failure
CHECK(test.isConfig_);
test.isConfig_ = false;
output.startOfLibrary();
CHECK(test.startLibrary_);
test.startLibrary_ = false;
output.startOfShower();
CHECK(test.startShower_);
test.startShower_ = false;
output.endOfShower();
CHECK(test.endShower_);
test.endShower_ = false;
output.endOfLibrary();
CHECK(test.endLibrary_);
CHECK(test.isSummary_);
test.isSummary_ = false;
test.endLibrary_ = false;
}
SECTION("failures") {
logging::set_level(logging::level::info);
// preparation
if (boost::filesystem::exists("./out_test")) {
boost::filesystem::remove_all("./out_test");
}
// output manager performs nothing, no action, just interface
OutputManager output("check", "./out_test");
CHECK_THROWS(new OutputManager("check", "./out_test"));
// CHECK_THROWS(output.startOfShower());
// CHECK_THROWS(output.endOfShower());
CHECK_THROWS(output.endOfLibrary());
output.startOfLibrary();
CHECK_THROWS(output.startOfLibrary());
// CHECK_THROWS(output.endOfShower());
// CHECK_THROWS(output.endOfLibrary());
output.startOfShower();
CHECK_THROWS(output.startOfLibrary());
// CHECK_THROWS(output.startOfShower());
// CHECK_THROWS(output.endOfLibrary());
output.endOfShower();
CHECK_THROWS(output.startOfLibrary());
// CHECK_THROWS(output.startOfShower());
// CHECK_THROWS(output.endOfShower());
output.endOfLibrary();
// CHECK_THROWS(output.endOfShower());
// CHECK_THROWS(output.startOfShower());
// CHECK_THROWS(output.endOfLibrary());
}
}
\ No newline at end of file
/*
* (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
*
* This software is distributed under the terms of the GNU General Public
* Licence version 3 (GPL Version 3). See file LICENSE for a full version of
* the license.
*/
#include <catch2/catch.hpp>
#include <boost/filesystem.hpp>
#include <corsika/output/ParquetStreamer.hpp>
#include <corsika/framework/core/Logging.hpp>
using namespace corsika;
TEST_CASE("ParquetStreamer") {
logging::set_level(logging::level::info);
SECTION("standard") {
// preparation
if (boost::filesystem::exists("./parquet_test.parquet")) {
boost::filesystem::remove_all("./parquet_test.parquet");
}
ParquetStreamer test;
CHECK_FALSE(test.isInit());
CHECK_THROWS(test.getWriter());
test.initStreamer("./parquet_test.parquet");
test.addField("testint", parquet::Repetition::REQUIRED, parquet::Type::INT32,
parquet::ConvertedType::INT_32);
test.addField("testfloat", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
parquet::ConvertedType::NONE);
test.enableCompression(1);
test.buildStreamer();
CHECK(test.isInit());
int testint = 1;
double testfloat = 2.0;
std::shared_ptr<parquet::StreamWriter> writer = test.getWriter();
(*writer) << static_cast<int>(testint) << static_cast<int>(testint)
<< static_cast<float>(testfloat) << parquet::EndRow;
test.closeStreamer();
CHECK_THROWS(test.getWriter());
CHECK_FALSE(test.isInit());
CHECK(boost::filesystem::exists("./parquet_test.parquet"));
}
}
\ No newline at end of file
/*
* (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
*
* This software is distributed under the terms of the GNU General Public
* Licence version 3 (GPL Version 3). See file LICENSE for a full version of
* the license.
*/
#include <catch2/catch.hpp>
#include <boost/filesystem.hpp>
#include <corsika/output/ParquetStreamer.hpp>
#include <corsika/framework/core/Logging.hpp>
using namespace corsika;
TEST_CASE("ParquetStreamer") {
logging::set_level(logging::level::info);
SECTION("standard") {
// preparation
if (boost::filesystem::exists("./parquet_test.parquet")) {
boost::filesystem::remove_all("./parquet_test.parquet");
}
ParquetStreamer test;
test.initStreamer("./parquet_test.parquet");
test.addField();
test.enableCompression(5);
test.buildStreamer();
test.closeStreamer();
std::shared_ptr<parquet::StreamWriter> writer = test.getWriter();
}
}
\ No newline at end of file
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