Skip to content

Commit

Permalink
Merge pull request #263 from OutpostUniverse/addDynamicStreamWriter
Browse files Browse the repository at this point in the history
Add DynamicMemoryWriter class
  • Loading branch information
DanRStevens authored Mar 5, 2019
2 parents 1ba5206 + e5f3feb commit aafd60a
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 29 deletions.
2 changes: 2 additions & 0 deletions OP2Utility.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<ClCompile Include="src\Sprite\SectionHeader.cpp" />
<ClCompile Include="src\Stream\FileReader.cpp" />
<ClCompile Include="src\Stream\FileWriter.cpp" />
<ClCompile Include="src\Stream\DynamicMemoryWriter.cpp" />
<ClCompile Include="src\Stream\MemoryWriter.cpp" />
<ClCompile Include="src\Stream\MemoryReader.cpp" />
<ClInclude Include="src\Sprite\Animation.h" />
Expand All @@ -180,6 +181,7 @@
<ClInclude Include="src\Sprite\OP2BmpLoader.h" />
<ClInclude Include="src\Sprite\PaletteHeader.h" />
<ClInclude Include="src\Sprite\SectionHeader.h" />
<ClInclude Include="src\Stream\DynamicMemoryWriter.h" />
<ClInclude Include="src\Stream\SliceReader.h" />
<ClInclude Include="src\Stream\FileReader.h" />
<ClInclude Include="src\Stream\FileWriter.h" />
Expand Down
6 changes: 6 additions & 0 deletions OP2Utility.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
<ClInclude Include="src\Tag.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\Stream\DynamicMemoryWriter.h">
<Filter>Stream</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\XFile.cpp">
Expand Down Expand Up @@ -221,6 +224,9 @@
<ClCompile Include="src\Stream\MemoryWriter.cpp">
<Filter>Stream</Filter>
</ClCompile>
<ClCompile Include="src\Stream\DynamicMemoryWriter.cpp">
<Filter>Stream</Filter>
</ClCompile>
<ClCompile Include="src\Archive\BitStreamReader.cpp">
<Filter>Archive</Filter>
</ClCompile>
Expand Down
59 changes: 59 additions & 0 deletions src/Stream/DynamicMemoryWriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "DynamicMemoryWriter.h"
#include <cstring> // memcpy
#include <limits>
#include <stdexcept>

namespace Stream
{
DynamicMemoryWriter::DynamicMemoryWriter() {
}

DynamicMemoryWriter::DynamicMemoryWriter(std::size_t preallocateSize) {
streamBuffer.reserve(preallocateSize);
}

void DynamicMemoryWriter::WriteImplementation(const void* buffer, std::size_t size)
{
auto streamSize = streamBuffer.size();
streamBuffer.resize(streamSize + size);
std::memcpy(streamBuffer.data() + streamSize, buffer, size);
}

uint64_t DynamicMemoryWriter::Length()
{
return streamBuffer.size();
}

uint64_t DynamicMemoryWriter::Position()
{
return streamBuffer.size();
}

void DynamicMemoryWriter::SeekForward(uint64_t offset)
{
auto streamSize = streamBuffer.size();
if (offset > std::numeric_limits<SizeType>::max() - streamSize) {
throw std::runtime_error("Seek forward beyond stream size limit");
}
// Zero fill to new size
streamBuffer.resize(static_cast<SizeType>(streamSize + offset), 0);
}

void DynamicMemoryWriter::SeekBackward(uint64_t offset)
{
auto streamSize = streamBuffer.size();
if (offset > streamSize) {
throw std::runtime_error("Seek backward before beginning of stream");
}
streamBuffer.resize(static_cast<SizeType>(streamSize - offset), 0);
}

void DynamicMemoryWriter::Seek(uint64_t offset)
{
streamBuffer.resize(static_cast<SizeType>(offset), 0);
}

MemoryReader DynamicMemoryWriter::GetReader() {
return MemoryReader(streamBuffer.data(), streamBuffer.size());
}
}
52 changes: 52 additions & 0 deletions src/Stream/DynamicMemoryWriter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once

#include "BidirectionalWriter.h"
#include "MemoryReader.h"
#include <cstddef>
#include <cstdint>
#include <vector>

namespace Stream
{
/**
* \brief Memory backed stream, which grows dynamically in size as data is added
*
* This stream can be used as a destination when the output size is not known exactly ahead of time.
* Written data can later be accessed by calling GetReader to retrieve a MemoryReader for the written data.
* The DynamicMemoryWriter is particularly useful when writing test code for stream serializaation,
* as it does not need to write to disk, nor cleanup temporary files, during round-trip testing.
*
* An internal memory buffer is managed by DynamicMemoryWriter, which grows as needed to accommodate new data.
* This is in contrast to MemoryWriter, which is a view to a non-owned pre-set sized buffer, which can not grow.
*
* \note Adding additional data to the stream after calling GetReader invalidates existing readers.
* (They may be left pointing at old memory after a re-allocation).
*/
class DynamicMemoryWriter : public BidirectionalWriter
{
public:
DynamicMemoryWriter();
DynamicMemoryWriter(std::size_t preallocateSize);

uint64_t Length() override;
uint64_t Position() override;

// SeekForward will set a new stream size and 0 fill the buffer gap
void SeekForward(uint64_t offset) override;
// SeekBackward will set a new stream size and truncate the stream
void SeekBackward(uint64_t offset) override;
// Seek will set a new stream size (either by 0 filling or by truncating)
void Seek(uint64_t position) override;

// Get read access to the written data
MemoryReader GetReader();

protected:
void WriteImplementation(const void* buffer, std::size_t size) override;

private:
std::vector<uint8_t> streamBuffer;

using SizeType = decltype(streamBuffer)::size_type;
};
}
13 changes: 5 additions & 8 deletions test/Map/MapWriter.test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "Map/Map.h"
#include "Map/MapHeader.h"
#include "Stream/MemoryWriter.h"
#include "Stream/DynamicMemoryWriter.h"
#include "XFile.h"
#include <gtest/gtest.h>
#include <vector>
Expand All @@ -14,9 +14,8 @@ TEST(MapWriter, EmptyMap)
XFile::DeletePath(testFilename);

// Write to Memory
std::vector<char> buffer(1024);
Stream::MemoryWriter memoryWriter(buffer.data(), buffer.size() * sizeof(decltype(buffer)::value_type));
EXPECT_NO_THROW(Map().Write(memoryWriter));
Stream::DynamicMemoryWriter writer;
EXPECT_NO_THROW(Map().Write(writer));
}

TEST(MapWriter, BlankFilename)
Expand All @@ -25,11 +24,9 @@ TEST(MapWriter, BlankFilename)
}

TEST(MapWriter, AllowInvalidVersionTag) {
const std::string testFilename("Map/data/test.map");
Stream::DynamicMemoryWriter writer;
Map map;

map.SetVersionTag(MapHeader::MinMapVersion - 1);
EXPECT_NO_THROW(Map().Write(testFilename));

XFile::DeletePath(testFilename);
EXPECT_NO_THROW(Map().Write(writer));
}
1 change: 1 addition & 0 deletions test/OP2UtilityTest.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<ClCompile Include="Rect.test.cpp" />
<ClCompile Include="Sprite\ArtReader.test.cpp" />
<ClCompile Include="Sprite\ArtWriter.test.cpp" />
<ClCompile Include="Stream\DynamicMemoryWriter.test.cpp" />
<ClCompile Include="Stream\FileReader.test.cpp" />
<ClCompile Include="Stream\SliceReader.test.cpp" />
<ClCompile Include="Stream\FileWriter.test.cpp" />
Expand Down
3 changes: 3 additions & 0 deletions test/OP2UtilityTest.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
<Filter>Stream</Filter>
</ClCompile>
<ClCompile Include="Tag.test.cpp" />
<ClCompile Include="Stream\DynamicMemoryWriter.test.cpp">
<Filter>Stream</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
Expand Down
36 changes: 15 additions & 21 deletions test/Sprite/ArtWriter.test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "../src/Sprite/ArtFile.h"
#include "../src/XFile.h"
#include "Stream/MemoryWriter.h"
#include "Stream/DynamicMemoryWriter.h"
#include "XFile.h"
#include <gtest/gtest.h>
#include <string>
Expand All @@ -14,9 +14,8 @@ TEST(ArtWriter, Empty)
XFile::DeletePath(testFilename);

// Write to Memory
std::vector<char> buffer(1024);
Stream::MemoryWriter memoryWriter(buffer.data(), buffer.size() * sizeof(decltype(buffer)::value_type));
EXPECT_NO_THROW(ArtFile::Write(memoryWriter, ArtFile()));
Stream::DynamicMemoryWriter writer;
EXPECT_NO_THROW(ArtFile::Write(writer, ArtFile()));
}

TEST(ArtWriter, BlankFilename)
Expand All @@ -41,39 +40,35 @@ class SimpleArtFile : public ::testing::Test {

TEST_F(SimpleArtFile, Write_ScanLineByteWidth)
{
std::string filename = "Sprite/data/test.prt";
Stream::DynamicMemoryWriter writer;

// Check no throw if scanLine next 4 byte aligned
EXPECT_NO_THROW(ArtFile::Write(filename, artFile));
EXPECT_NO_THROW(ArtFile::Write(writer, artFile));

// Check throw if scanLine > width && < 4 byte aligned
artFile.imageMetas[0].scanLineByteWidth = 11;
EXPECT_THROW(ArtFile::Write(filename, artFile), std::runtime_error);
EXPECT_THROW(ArtFile::Write(writer, artFile), std::runtime_error);

// Check throw if scanLine > first 4 byte align
artFile.imageMetas[0].scanLineByteWidth = 16;
EXPECT_THROW(ArtFile::Write(filename, artFile), std::runtime_error);
EXPECT_THROW(ArtFile::Write(writer, artFile), std::runtime_error);

// Check throw if scanLine < width but still 4 byte aligned
artFile.imageMetas[0].scanLineByteWidth = 8;
EXPECT_THROW(ArtFile::Write(filename, artFile), std::runtime_error);

XFile::DeletePath(filename);
EXPECT_THROW(ArtFile::Write(writer, artFile), std::runtime_error);
}

TEST_F(SimpleArtFile, Write_PaletteIndexRange)
{
std::string filename = "Sprite/data/test.prt";
Stream::DynamicMemoryWriter writer;

// Check for no throw when ImageMeta.paletteIndex is within palette container's range
EXPECT_NO_THROW(ArtFile::Write(filename, artFile));
EXPECT_NO_THROW(ArtFile::Write(writer, artFile));

artFile.palettes.clear();

// Check for throw due to ImageMeta.paletteIndex outside of palette container's range
EXPECT_THROW(ArtFile::Write(filename, artFile), std::runtime_error);

XFile::DeletePath(filename);
EXPECT_THROW(ArtFile::Write(writer, artFile), std::runtime_error);
}

TEST_F(SimpleArtFile, Write_PaletteColors)
Expand All @@ -82,18 +77,17 @@ TEST_F(SimpleArtFile, Write_PaletteColors)
const uint8_t blue = 0;
artFile.palettes[0][0] = Color{ red, 0, blue, 0 };

std::string filename = "Sprite/data/test.prt";
Stream::DynamicMemoryWriter writer;

EXPECT_NO_THROW(ArtFile::Write(filename, artFile));
EXPECT_NO_THROW(ArtFile::Write(writer, artFile));

// Check ArtFile palette remains unchanged after write
EXPECT_EQ(red, artFile.palettes[0][0].red);
EXPECT_EQ(blue, artFile.palettes[0][0].blue);

// Check ArtFile palette written to disk properly
artFile = ArtFile::Read(filename);
auto reader = writer.GetReader();
artFile = ArtFile::Read(reader);
EXPECT_EQ(red, artFile.palettes[0][0].red);
EXPECT_EQ(blue, artFile.palettes[0][0].blue);

XFile::DeletePath(filename);
}
50 changes: 50 additions & 0 deletions test/Stream/DynamicMemoryWriter.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include "Stream/DynamicMemoryWriter.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <array>


TEST(DynamicMemoryWriter, LengthAutoExpands) {
const std::array<uint8_t, 4> data = {0, 1, 2, 3};
Stream::DynamicMemoryWriter writer;

// Initially stream has 0 length
EXPECT_EQ(0, writer.Length());

// Length grows as data is added
EXPECT_NO_THROW(writer.Write(data));
EXPECT_EQ(4, writer.Length());

// Seeking forward past the end expands the stream
EXPECT_NO_THROW(writer.SeekForward(1));
EXPECT_EQ(5, writer.Length());

// Additional data is added after the expansion gap
EXPECT_NO_THROW(writer.Write(data));
EXPECT_EQ(9, writer.Length());
}

TEST(DynamicMemoryWriter, ReadsBackStoredData) {
const std::array<uint8_t, 4> data = {0, 1, 2, 3};
Stream::DynamicMemoryWriter writer;

// Add data to stream
EXPECT_NO_THROW(writer.Write(data));
// Seeking forward 0 fills
EXPECT_NO_THROW(writer.SeekForward(1));
// Write more data after the gap
EXPECT_NO_THROW(writer.Write(data));

// Get stream to read back data
auto reader = writer.GetReader();
std::array<uint8_t, 4> readData;
uint8_t readDataByte;

// Read data back, and ensure it matches up
EXPECT_NO_THROW(reader.Read(readData));
EXPECT_EQ(data, readData);
EXPECT_NO_THROW(reader.Read(readDataByte));
EXPECT_EQ(0, readDataByte);
EXPECT_NO_THROW(reader.Read(readData));
EXPECT_EQ(data, readData);
}

0 comments on commit aafd60a

Please sign in to comment.