diff --git a/include/fesa/core/diagnostic.hpp b/include/fesa/core/diagnostic.hpp new file mode 100644 index 0000000..68909d3 --- /dev/null +++ b/include/fesa/core/diagnostic.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +namespace fesa { + +enum class DiagnosticStage { + io, + syntax, + semantic, + model, + equation, + solver, + results, + validation +}; + +enum class Severity { warning, error }; + +struct Diagnostic final { + DiagnosticStage stage; + Severity severity; + std::string code; + std::string message; + std::optional source; +}; + +} // namespace fesa diff --git a/include/fesa/core/entity_id.hpp b/include/fesa/core/entity_id.hpp new file mode 100644 index 0000000..1328959 --- /dev/null +++ b/include/fesa/core/entity_id.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace fesa { + +template +class EntityId final { +public: + explicit constexpr EntityId(const std::int64_t value) : value_{value} { + if (value < 0) { + throw std::invalid_argument{"EntityId value must not be negative."}; + } + } + + [[nodiscard]] constexpr std::int64_t value() const noexcept { + return value_; + } + + auto operator<=>(const EntityId&) const = default; + +private: + std::int64_t value_; +}; + +} // namespace fesa diff --git a/include/fesa/core/source_location.hpp b/include/fesa/core/source_location.hpp new file mode 100644 index 0000000..815469a --- /dev/null +++ b/include/fesa/core/source_location.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace fesa { + +struct SourceLocation final { + std::filesystem::path file; + std::size_t line; + std::size_t column; +}; + +} // namespace fesa diff --git a/include/fesa/core/status.hpp b/include/fesa/core/status.hpp new file mode 100644 index 0000000..0bdd52e --- /dev/null +++ b/include/fesa/core/status.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +namespace fesa { + +struct Status final { + bool succeeded; + std::vector diagnostics; +}; + +} // namespace fesa diff --git a/include/fesa/core/vec3.hpp b/include/fesa/core/vec3.hpp new file mode 100644 index 0000000..272f8dd --- /dev/null +++ b/include/fesa/core/vec3.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace fesa { + +struct Vec3 final { + double x; + double y; + double z; +}; + +[[nodiscard]] inline bool is_finite(const Vec3 value) noexcept { + return std::isfinite(value.x) && std::isfinite(value.y) && + std::isfinite(value.z); +} + +} // namespace fesa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a2a5538..bb4f70d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,3 +36,24 @@ set_property( PROPERTY ENVIRONMENT_MODIFICATION ${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS} ) + +add_executable(fesa_core_value_tests + unit/core/diagnostic_test.cpp + unit/core/entity_id_test.cpp + unit/core/status_test.cpp + unit/core/vec3_test.cpp +) + +target_compile_features(fesa_core_value_tests PRIVATE cxx_std_20) +target_compile_options(fesa_core_value_tests PRIVATE /W4 /permissive- /EHsc) + +target_link_libraries(fesa_core_value_tests + PRIVATE + fesa_core + GTest::gtest_main +) + +add_test( + NAME CoreValueTypes + COMMAND "$" +) diff --git a/tests/unit/core/diagnostic_test.cpp b/tests/unit/core/diagnostic_test.cpp new file mode 100644 index 0000000..290fb3f --- /dev/null +++ b/tests/unit/core/diagnostic_test.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +#include + +#include + +namespace { + +TEST(Diagnostic, PreservesSourceLocation) { + const fesa::Diagnostic diagnostic{ + fesa::DiagnosticStage::syntax, + fesa::Severity::error, + "ABAQUS_INVALID_NODE", + "Node data is incomplete.", + fesa::SourceLocation{ + std::filesystem::path{"models/beam.inp"}, 42, 9}}; + + ASSERT_TRUE(diagnostic.source.has_value()); + EXPECT_EQ( + diagnostic.source->file, std::filesystem::path{"models/beam.inp"}); + EXPECT_EQ(diagnostic.source->line, 42U); + EXPECT_EQ(diagnostic.source->column, 9U); +} + +TEST(Diagnostic, AllowsDiagnosticsWithoutSourceLocation) { + const fesa::Diagnostic diagnostic{ + fesa::DiagnosticStage::solver, + fesa::Severity::warning, + "SOLVER_RESIDUAL", + "Residual is above the reporting threshold.", + std::nullopt}; + + EXPECT_FALSE(diagnostic.source.has_value()); +} + +} // namespace diff --git a/tests/unit/core/entity_id_test.cpp b/tests/unit/core/entity_id_test.cpp new file mode 100644 index 0000000..9817a40 --- /dev/null +++ b/tests/unit/core/entity_id_test.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +#include + +#include + +namespace { + +struct NodeTag; +struct ElementTag; + +using NodeId = fesa::EntityId; +using ElementId = fesa::EntityId; + +static_assert(std::is_constructible_v); +static_assert(!std::is_convertible_v); +static_assert(!std::is_convertible_v); + +TEST(EntityId, PreservesValueAndSupportsTypedOrdering) { + constexpr NodeId first{7}; + constexpr NodeId second{11}; + + static_assert(first.value() == 7); + static_assert(first < second); + + EXPECT_EQ(first.value(), 7); + EXPECT_LT(first, second); +} + +TEST(EntityId, RejectsNegativeValues) { + EXPECT_THROW((NodeId{-1}), std::invalid_argument); +} + +} // namespace diff --git a/tests/unit/core/status_test.cpp b/tests/unit/core/status_test.cpp new file mode 100644 index 0000000..32a0ca4 --- /dev/null +++ b/tests/unit/core/status_test.cpp @@ -0,0 +1,35 @@ +#include +#include + +#include + +#include + +namespace { + +TEST(CoreStatus, PreservesFailureDiagnostics) { + const fesa::Status status{ + false, + {fesa::Diagnostic{ + fesa::DiagnosticStage::model, + fesa::Severity::error, + "MODEL_NONFINITE_VECTOR", + "A vector component is not finite.", + fesa::SourceLocation{ + std::filesystem::path{"models/invalid.inp"}, 8, 3}}}}; + + EXPECT_FALSE(status.succeeded); + ASSERT_EQ(status.diagnostics.size(), 1U); + EXPECT_EQ(status.diagnostics.front().code, "MODEL_NONFINITE_VECTOR"); + ASSERT_TRUE(status.diagnostics.front().source.has_value()); + EXPECT_EQ(status.diagnostics.front().source->line, 8U); +} + +TEST(CoreStatus, RepresentsSuccessWithoutDiagnostics) { + const fesa::Status status{true, {}}; + + EXPECT_TRUE(status.succeeded); + EXPECT_TRUE(status.diagnostics.empty()); +} + +} // namespace diff --git a/tests/unit/core/vec3_test.cpp b/tests/unit/core/vec3_test.cpp new file mode 100644 index 0000000..92e4420 --- /dev/null +++ b/tests/unit/core/vec3_test.cpp @@ -0,0 +1,20 @@ +#include + +#include + +#include + +namespace { + +TEST(CoreVec3, ReportsOnlyFiniteVectorsAsFinite) { + EXPECT_TRUE(fesa::is_finite(fesa::Vec3{1.0, -2.0, 0.0})); + + const double infinity = std::numeric_limits::infinity(); + const double nan = std::numeric_limits::quiet_NaN(); + + EXPECT_FALSE(fesa::is_finite(fesa::Vec3{infinity, 0.0, 0.0})); + EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, -infinity, 0.0})); + EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, 0.0, nan})); +} + +} // namespace