From 64c60b3726fe79eac8230d3874db7c634f7b3da7 Mon Sep 17 00:00:00 2001 From: Vladyslav Baranovskyi Date: Thu, 10 Oct 2024 15:35:08 +0300 Subject: [PATCH 1/3] Tests for Pfm.hpp, Pfm bugfix related to the endian swapping --- openVulkanoCpp/IO/Files/Pfm.hpp | 7 +- tests/Files/Pfm.cpp | 169 ++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 tests/Files/Pfm.cpp diff --git a/openVulkanoCpp/IO/Files/Pfm.hpp b/openVulkanoCpp/IO/Files/Pfm.hpp index 3bd44c6..55b1bed 100644 --- a/openVulkanoCpp/IO/Files/Pfm.hpp +++ b/openVulkanoCpp/IO/Files/Pfm.hpp @@ -96,6 +96,10 @@ namespace OpenVulkano inStream >> header; size_t size = header.GetElementCount(); + image = std::make_unique(size); + + inStream.read(reinterpret_cast(image.get()), size * sizeof(float)); + if ((std::endian::native == std::endian::little) != header.littleEndian) { char* data = reinterpret_cast(image.get()); @@ -105,9 +109,6 @@ namespace OpenVulkano std::reverse(&data[idx], &data[idx + sizeof(float)]); } } - - image = std::make_unique(size); - inStream.read(reinterpret_cast(image.get()), size * sizeof(float)); } static PfmImage ReadImage(std::istream& inStream) diff --git a/tests/Files/Pfm.cpp b/tests/Files/Pfm.cpp new file mode 100644 index 0000000..1994461 --- /dev/null +++ b/tests/Files/Pfm.cpp @@ -0,0 +1,169 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include + +#include "IO/Files/Pfm.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace OpenVulkano; + +namespace +{ + bool almostEqual(float a, float b, float epsilon = 0.001f) { return std::fabs(a - b) < epsilon; } +} + +TEST_CASE("Invalid input", "[Pfm]") +{ + std::stringstream ss("Invalid\n1024 768\n1.0\n"); + PfmHeader header; + + REQUIRE_THROWS_AS(ss >> header, std::runtime_error); +} + +TEST_CASE("Extreme maxValue handling", "[Pfm]") +{ + PfmHeader largeMaxValueHeader(1024, 768, 1000000.0f, true); + std::stringstream ss; + ss << largeMaxValueHeader; + + REQUIRE(ss.str() == "PF\n1024 768\n-1000000.0\n"); + + std::stringstream inputSS("PF\n1024 768\n-1000000.0\n"); + PfmHeader parsedHeader; + inputSS >> parsedHeader; + REQUIRE(parsedHeader.maxValue == 1000000.0f); + REQUIRE(parsedHeader.littleEndian == true); +} + +TEST_CASE("Negative maxValue indicating endian", "[Pfm]") +{ + std::stringstream ss("PF\n640 480\n-1.0\n"); + PfmHeader header; + ss >> header; + + REQUIRE(header.color == true); + REQUIRE(header.width == 640); + REQUIRE(header.height == 480); + REQUIRE(header.maxValue == 1.0f); + REQUIRE(header.littleEndian == true); +} + +TEST_CASE("Operator<< output precision", "[Pfm]") +{ + PfmHeader header(640, 480, 1.234567f, true); + std::stringstream ss; + ss << header; + + REQUIRE(ss.str() == "PF\n640 480\n-1.2\n"); +} + +TEST_CASE("Zero width or height", "[Pfm]") +{ + PfmHeader zeroWidth(0, 768, 1.0f, true); + PfmHeader zeroHeight(1024, 0, 1.0f, false); + + REQUIRE(zeroWidth.GetElementCount() == 0); + REQUIRE(zeroHeight.GetElementCount() == 0); + REQUIRE(zeroWidth.GetImageSize() == 0); + REQUIRE(zeroHeight.GetImageSize() == 0); +} + +TEST_CASE("Large width and height", "[Pfm]") +{ + PfmHeader largeHeader(10000, 8000, 1.0f, true); + + REQUIRE(largeHeader.GetElementCount() == 10000 * 8000 * 3); + REQUIRE(largeHeader.GetImageSize() == 10000 * 8000 * 3 * sizeof(float)); +} + +TEST_CASE("Comparing headers", "[Pfm]") +{ + PfmHeader header1(640, 480, 1.0f, true); + PfmHeader header2(640, 480, 1.0f, true); + PfmHeader header3(640, 480, 1.0f, false); + PfmHeader header4(1024, 768, 1.0f, true); + + REQUIRE(header1.width == header2.width); + REQUIRE(header1.height == header2.height); + REQUIRE(header1.color == header2.color); + + REQUIRE(header1.color != header3.color); + REQUIRE(header1.width != header4.width); + REQUIRE(header1.height != header4.height); +} + +TEST_CASE("Read with incorrect header", "[Pfm]") +{ + std::stringstream ss("Invalid\n800 600\n-1.0\n"); + REQUIRE_THROWS_AS(PfmImage::ReadImage(ss), std::runtime_error); +} + +TEST_CASE("Endian little-endian", "[Pfm]") +{ + std::stringstream ss; + ss << "Pf\n2 2\n-1.0\n"; + std::vector imageData = { 0.1f, 0.2f, 0.3f, 0.4f }; + ss.write(reinterpret_cast(imageData.data()), imageData.size() * sizeof(float)); + PfmImage image = PfmImage::ReadImage(ss); + REQUIRE(image.header.width == 2); + REQUIRE(image.header.height == 2); + REQUIRE(image.header.color == false); + REQUIRE(image.header.maxValue == 1.0f); + for (size_t i = 0; i < imageData.size(); ++i) + { + REQUIRE(almostEqual(image.image[i],imageData[i])); + } +} + +TEST_CASE("Read with exact data match", "[Pfm]") +{ + std::stringstream ss; + ss << "Pf\n2 2\n-1.0\n"; + std::vector imageData = { 0.1f, 0.2f, 0.3f, 0.4f }; + ss.write(reinterpret_cast(imageData.data()), imageData.size() * sizeof(float)); + + PfmImage image = PfmImage::ReadImage(ss); + REQUIRE(image.header.width == 2); + REQUIRE(image.header.height == 2); + REQUIRE(image.header.color == false); + REQUIRE(image.header.maxValue == 1.0f); + + for (size_t i = 0; i < imageData.size(); ++i) + { + REQUIRE(almostEqual(image.image[i], imageData[i])); + } +} + +TEST_CASE("ReadImage with big-endian data", "[Pfm]") +{ + std::stringstream ss; + ss << "Pf\n2 2\n1.0\n"; + std::vector imageData = { 0.1f, 0.2f, 0.3f, 0.4f }; + + { + std::vector imageDataBigEndian = imageData; + char* data = reinterpret_cast(imageDataBigEndian.data()); + for (size_t i = 0; i < imageDataBigEndian.size(); i++) + { + size_t idx = i * sizeof(float); + std::reverse(&data[idx], &data[idx + sizeof(float)]); + } + ss.write(reinterpret_cast(imageDataBigEndian.data()), imageDataBigEndian.size() * sizeof(float)); + } + PfmImage image = PfmImage::ReadImage(ss); + + for (size_t i = 0; i < imageData.size(); ++i) + { + REQUIRE(almostEqual(image.image[i], imageData[i])); + } +} \ No newline at end of file From cc20a1fb3b2bb08814a29d2c7d483725db8c6eef Mon Sep 17 00:00:00 2001 From: Vladyslav Baranovskyi Date: Thu, 10 Oct 2024 15:36:22 +0300 Subject: [PATCH 2/3] Tests for Pnm.hpp, Pnm operator>> input robustness --- openVulkanoCpp/IO/Files/Pnm.hpp | 21 +++- tests/Files/Pnm.cpp | 200 ++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 tests/Files/Pnm.cpp diff --git a/openVulkanoCpp/IO/Files/Pnm.hpp b/openVulkanoCpp/IO/Files/Pnm.hpp index f5c4652..99e8e96 100644 --- a/openVulkanoCpp/IO/Files/Pnm.hpp +++ b/openVulkanoCpp/IO/Files/Pnm.hpp @@ -87,8 +87,25 @@ namespace OpenVulkano if (val > 6) throw std::runtime_error("Malformed PNM header!"); pnmHeader.ascii = val < 4; pnmHeader.color = static_cast(val - 3); - inStream >> pnmHeader.width >> pnmHeader.height; - inStream >> pnmHeader.maxValue; + + if (!(inStream >> pnmHeader.width >> pnmHeader.height)) + { + throw std::runtime_error("Malformed PNM header: Unable to read width and height!"); + } + if (pnmHeader.width == 0 || pnmHeader.height == 0) + { + throw std::runtime_error("Malformed PNM header: Width and height must be positive!"); + } + + if (!(inStream >> pnmHeader.maxValue)) + { + throw std::runtime_error("Malformed PNM header: Unable to read maxValue!"); + } + if (pnmHeader.maxValue == 0 || pnmHeader.maxValue > 65535) + { + throw std::runtime_error("Malformed PNM header: Invalid maxValue (must be between 1 and 65535)!"); + } + inStream.get(); return inStream; } diff --git a/tests/Files/Pnm.cpp b/tests/Files/Pnm.cpp new file mode 100644 index 0000000..68cc035 --- /dev/null +++ b/tests/Files/Pnm.cpp @@ -0,0 +1,200 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include + +#include "IO/Files/Pnm.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace OpenVulkano; + +namespace +{ + bool almostEqual(float a, float b, float epsilon = 0.001f) { return std::fabs(a - b) < epsilon; } +} + +TEST_CASE("Default constructor initializes with expected values", "[Pnm]") +{ + PnmHeader header; + + REQUIRE(header.width == 0); + REQUIRE(header.height == 0); + REQUIRE(header.maxValue == 0); + REQUIRE(header.color == PnmHeader::ColorMode::GRAY); + REQUIRE(header.ascii == false); +} + +TEST_CASE("Constructor initializes with correct values", "[Pnm]") +{ + PnmHeader header; + + header = PnmHeader(100, 200, true); + + REQUIRE(header.width == 100); + REQUIRE(header.height == 200); + REQUIRE(header.maxValue == 255); + REQUIRE(header.color == PnmHeader::ColorMode::COLOR); + REQUIRE(header.ascii == false); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::BW, 127, true); + + REQUIRE(header.width == 100); + REQUIRE(header.height == 200); + REQUIRE(header.maxValue == 127); + REQUIRE(header.color == PnmHeader::ColorMode::BW); + REQUIRE(header.ascii == true); +} + +TEST_CASE("GetFileType returns correct file type", "[Pnm]") +{ + PnmHeader header; + + header = PnmHeader(100, 200, PnmHeader::ColorMode::BW); + REQUIRE(header.GetFileType() == "PBM"); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::GRAY); + REQUIRE(header.GetFileType() == "PGM"); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::COLOR); + REQUIRE(header.GetFileType() == "PPM"); +} + +TEST_CASE("GetFileExtension returns correct file extension", "[Pnm]") +{ + PnmHeader header; + + header = PnmHeader(100, 200, PnmHeader::ColorMode::BW); + REQUIRE(header.GetFileExtension() == ".pbm"); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::GRAY); + REQUIRE(header.GetFileExtension() == ".pgm"); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::COLOR); + REQUIRE(header.GetFileExtension() == ".ppm"); +} + +TEST_CASE("GetMagicNumberValue and GetMagicNumberChar return correct values", "[Pnm]") +{ + PnmHeader header; + + header = PnmHeader(100, 200, PnmHeader::ColorMode::BW, 255, true); + REQUIRE(header.GetMagicNumberValue() == 1); + REQUIRE(header.GetMagicNumberChar() == '1'); + + header = PnmHeader(100, 200, PnmHeader::ColorMode::COLOR, 255, false); + REQUIRE(header.GetMagicNumberValue() == 6); + REQUIRE(header.GetMagicNumberChar() == '6'); +} + +TEST_CASE("GetImageSize returns correct size", "[Pnm]") +{ + PnmHeader header; + header = PnmHeader(8, 8, PnmHeader::ColorMode::BW); + REQUIRE(header.GetImageSize() == 8); + + header = PnmHeader(100, 100, PnmHeader::ColorMode::GRAY); + REQUIRE(header.GetImageSize() == 10000); + + header = PnmHeader(50, 50, PnmHeader::ColorMode::COLOR); + REQUIRE(header.GetImageSize() == 7500); + + header = PnmHeader(50, 50, PnmHeader::ColorMode::COLOR, 65535); + REQUIRE(header.GetImageSize() == 15000); +} + +TEST_CASE("Ostream operator<< outputs correct header", "[Pnm]") +{ + PnmHeader header(100, 200, PnmHeader::ColorMode::COLOR, 255, false); + std::stringstream ss; + ss << header; + + std::string expectedOutput = "P6\n100 200\n255\n"; + REQUIRE(ss.str() == expectedOutput); +} + +TEST_CASE("Istream operator>> reads correctly formatted PNM header", "[Pnm]") +{ + std::string pnmData = "P6\n100 200\n255\n"; + std::stringstream ss(pnmData); + + PnmHeader header; + ss >> header; + + REQUIRE(header.width == 100); + REQUIRE(header.height == 200); + REQUIRE(header.maxValue == 255); + REQUIRE(header.color == PnmHeader::ColorMode::COLOR); + REQUIRE(header.ascii == false); +} + +TEST_CASE("Istream operator>> throws on malformed header", "[Pnm]") +{ + { + std::string malformedData = "X6\n100 200\n255\n"; + std::stringstream ss(malformedData); + + PnmHeader header; + REQUIRE_THROWS_AS(ss >> header, std::runtime_error); + } + + { + std::string malformedData = "P9\n100 200\n255\n"; + std::stringstream ss(malformedData); + + PnmHeader header; + REQUIRE_THROWS_AS(ss >> header, std::runtime_error); + } + + { + std::string malformedData = "P6\n100\n255\n"; + std::stringstream ss(malformedData); + + PnmHeader header; + REQUIRE_THROWS_AS(ss >> header, std::runtime_error); + } + + { + std::string malformedData = "GarbageInput"; + std::stringstream ss(malformedData); + + PnmHeader header; + REQUIRE_THROWS_AS(ss >> header, std::runtime_error); + } +} + +TEST_CASE("Read throws on unsupported ascii mode", "[Pnm]") +{ + std::string pnmData = "P3\n100 100\n255\n"; + std::stringstream ss(pnmData); + + PnmImage image; + REQUIRE_THROWS_AS(image.Read(ss), std::runtime_error); +} + +TEST_CASE("Read successfully reads binary image data", "[Pnm]") +{ + std::string pnmHeader = "P6\n2 2\n255\n"; + std::string pnmImage = "\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF\xFF\x00\x00"; + std::stringstream ss(pnmHeader + pnmImage); + + PnmImage image; + REQUIRE_NOTHROW(image.Read(ss)); + + REQUIRE(image.header.width == 2); + REQUIRE(image.header.height == 2); + REQUIRE(image.header.maxValue == 255); + REQUIRE(image.header.color == PnmHeader::ColorMode::COLOR); + + REQUIRE(image.image[0] == '\xFF'); + REQUIRE(image.image[1] == '\x00'); + REQUIRE(image.image[2] == '\x00'); +} \ No newline at end of file From b0b966aab720c7f8dfe6aa7186d6b997be810471 Mon Sep 17 00:00:00 2001 From: Vladyslav Baranovskyi Date: Thu, 10 Oct 2024 15:42:41 +0300 Subject: [PATCH 3/3] Marked unlikely code paths as [[unlikely]] --- openVulkanoCpp/IO/Files/Pnm.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openVulkanoCpp/IO/Files/Pnm.hpp b/openVulkanoCpp/IO/Files/Pnm.hpp index 99e8e96..f0995b4 100644 --- a/openVulkanoCpp/IO/Files/Pnm.hpp +++ b/openVulkanoCpp/IO/Files/Pnm.hpp @@ -88,20 +88,20 @@ namespace OpenVulkano pnmHeader.ascii = val < 4; pnmHeader.color = static_cast(val - 3); - if (!(inStream >> pnmHeader.width >> pnmHeader.height)) + if (!(inStream >> pnmHeader.width >> pnmHeader.height)) [[unlikely]] { throw std::runtime_error("Malformed PNM header: Unable to read width and height!"); } - if (pnmHeader.width == 0 || pnmHeader.height == 0) + if (pnmHeader.width == 0 || pnmHeader.height == 0) [[unlikely]] { throw std::runtime_error("Malformed PNM header: Width and height must be positive!"); } - if (!(inStream >> pnmHeader.maxValue)) + if (!(inStream >> pnmHeader.maxValue)) [[unlikely]] { throw std::runtime_error("Malformed PNM header: Unable to read maxValue!"); } - if (pnmHeader.maxValue == 0 || pnmHeader.maxValue > 65535) + if (pnmHeader.maxValue == 0 || pnmHeader.maxValue > 65535) [[unlikely]] { throw std::runtime_error("Malformed PNM header: Invalid maxValue (must be between 1 and 65535)!"); }