mirror of
https://github.com/libSDL2pp/libSDL2pp.git
synced 2025-09-12 16:57:00 -04:00
Merge branch 'rwops-improvements'
This commit is contained in:
commit
95f43d8478
@ -71,14 +71,15 @@ SET(LIBRARY_SOURCES
|
||||
|
||||
SET(LIBRARY_HEADERS
|
||||
SDL2pp/Audio.hh
|
||||
SDL2pp/ContainerRWops.hh
|
||||
SDL2pp/Exception.hh
|
||||
SDL2pp/ExtraRWops.hh
|
||||
SDL2pp/Point.hh
|
||||
SDL2pp/RWops.hh
|
||||
SDL2pp/Rect.hh
|
||||
SDL2pp/Renderer.hh
|
||||
SDL2pp/SDL.hh
|
||||
SDL2pp/SDL2pp.hh
|
||||
SDL2pp/StreamRWops.hh
|
||||
SDL2pp/Texture.hh
|
||||
SDL2pp/Wav.hh
|
||||
SDL2pp/Window.hh
|
||||
|
@ -19,20 +19,41 @@
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#ifndef SDL2PP_EXTRARWOPS_HH
|
||||
#define SDL2PP_EXTRARWOPS_HH
|
||||
#ifndef SDL2PP_CONTAINERRWOPS_HH
|
||||
#define SDL2PP_CONTAINERRWOPS_HH
|
||||
|
||||
#include <SDL2pp/RWops.hh>
|
||||
#include <SDL2pp/Exception.hh>
|
||||
|
||||
#include <type_traits>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace SDL2pp {
|
||||
|
||||
template<typename C>
|
||||
template <class C>
|
||||
class ContainerRWops : public CustomRWops {
|
||||
protected:
|
||||
C& container_;
|
||||
size_t position_;
|
||||
|
||||
private:
|
||||
template <class CC>
|
||||
typename std::enable_if<!std::is_const<CC>::value, size_t>::type WriteHelper(const void* ptr, size_t size, size_t maxnum) {
|
||||
if (position_ + size * maxnum > container_.size())
|
||||
container_.resize(position_ + size * maxnum);
|
||||
|
||||
std::copy(reinterpret_cast<const unsigned char*>(ptr), reinterpret_cast<const unsigned char*>(ptr) + size * maxnum, container_.begin() + position_);
|
||||
|
||||
position_ += size * maxnum;
|
||||
|
||||
return maxnum;
|
||||
}
|
||||
|
||||
template <class CC>
|
||||
typename std::enable_if<std::is_const<CC>::value, size_t>::type WriteHelper(const void*, size_t, size_t) {
|
||||
SDL_SetError("Can't write to read-only container");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public:
|
||||
ContainerRWops(C& container) : container_(container), position_(0) {
|
||||
}
|
||||
@ -54,7 +75,7 @@ public:
|
||||
position_ = container_.size() + offset;
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unexpected whence value for WritableMemRWops::Seek");
|
||||
throw std::logic_error("Unexpected whence value for ContainerRWops::Seek");
|
||||
}
|
||||
return position_;
|
||||
}
|
||||
@ -73,69 +94,7 @@ public:
|
||||
}
|
||||
|
||||
virtual size_t Write(const void* ptr, size_t size, size_t maxnum) override {
|
||||
if (position_ + size * maxnum > container_.size())
|
||||
container_.resize(position_ + size * maxnum);
|
||||
|
||||
std::copy(reinterpret_cast<const unsigned char*>(ptr), reinterpret_cast<const unsigned char*>(ptr) + size * maxnum, container_.begin() + position_);
|
||||
|
||||
position_ += size * maxnum;
|
||||
|
||||
return maxnum;
|
||||
}
|
||||
|
||||
virtual int Close() override {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename C>
|
||||
class ConstContainerRWops : public CustomRWops {
|
||||
protected:
|
||||
const C& container_;
|
||||
size_t position_;
|
||||
|
||||
public:
|
||||
ConstContainerRWops(const C& container) : container_(container), position_(0) {
|
||||
}
|
||||
|
||||
ConstContainerRWops(const ConstContainerRWops<C>&) = default;
|
||||
ConstContainerRWops& operator=(const ConstContainerRWops<C>&) = delete;
|
||||
ConstContainerRWops(ConstContainerRWops<C>&&) noexcept = default;
|
||||
ConstContainerRWops& operator=(ConstContainerRWops<C>&&) = delete;
|
||||
|
||||
virtual Sint64 Seek(Sint64 offset, int whence) override {
|
||||
switch (whence) {
|
||||
case RW_SEEK_SET:
|
||||
position_ = offset;
|
||||
break;
|
||||
case RW_SEEK_CUR:
|
||||
position_ = position_ + offset;
|
||||
break;
|
||||
case RW_SEEK_END:
|
||||
position_ = container_.size() + offset;
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unexpected whence value for WritableMemRWops::Seek");
|
||||
}
|
||||
return position_;
|
||||
}
|
||||
|
||||
virtual size_t Read(void* ptr, size_t size, size_t maxnum) override {
|
||||
if (position_ + size > container_.size())
|
||||
return 0;
|
||||
|
||||
int toread = std::min((container_.size() - position_), maxnum * size);
|
||||
|
||||
std::copy(container_.begin() + position_, container_.begin() + position_ + toread, reinterpret_cast<unsigned char*>(ptr));
|
||||
|
||||
position_ += toread;
|
||||
|
||||
return toread / size;
|
||||
}
|
||||
|
||||
virtual size_t Write(const void*, size_t, size_t) override {
|
||||
SDL_SetError("Can't write to read-only container");
|
||||
return 0;
|
||||
return WriteHelper<C>(ptr, size, maxnum);
|
||||
}
|
||||
|
||||
virtual int Close() override {
|
@ -192,8 +192,57 @@ Sint64 RWops::Size() {
|
||||
Sint64 old_pos = Tell();
|
||||
Sint64 size = Seek(0, SEEK_END);
|
||||
Sint64 back_pos = Seek(old_pos, SEEK_SET);
|
||||
(void)back_pos; // silence unused variable warning on release build
|
||||
assert(back_pos == old_pos);
|
||||
return size;
|
||||
}
|
||||
|
||||
Uint16 RWops::ReadBE16() {
|
||||
return SDL_ReadBE16(rwops_);
|
||||
}
|
||||
|
||||
Uint32 RWops::ReadBE32() {
|
||||
return SDL_ReadBE32(rwops_);
|
||||
}
|
||||
|
||||
Uint64 RWops::ReadBE64() {
|
||||
return SDL_ReadBE64(rwops_);
|
||||
}
|
||||
|
||||
Uint16 RWops::ReadLE16() {
|
||||
return SDL_ReadLE16(rwops_);
|
||||
}
|
||||
|
||||
Uint32 RWops::ReadLE32() {
|
||||
return SDL_ReadLE32(rwops_);
|
||||
}
|
||||
|
||||
Uint64 RWops::ReadLE64() {
|
||||
return SDL_ReadLE64(rwops_);
|
||||
}
|
||||
|
||||
size_t RWops::WriteBE16(Uint16 value) {
|
||||
return SDL_WriteBE16(rwops_, value);
|
||||
}
|
||||
|
||||
size_t RWops::WriteBE32(Uint32 value) {
|
||||
return SDL_WriteBE32(rwops_, value);
|
||||
}
|
||||
|
||||
size_t RWops::WriteBE64(Uint64 value) {
|
||||
return SDL_WriteBE64(rwops_, value);
|
||||
}
|
||||
|
||||
size_t RWops::WriteLE16(Uint16 value) {
|
||||
return SDL_WriteLE16(rwops_, value);
|
||||
}
|
||||
|
||||
size_t RWops::WriteLE32(Uint32 value) {
|
||||
return SDL_WriteLE32(rwops_, value);
|
||||
}
|
||||
|
||||
size_t RWops::WriteLE64(Uint64 value) {
|
||||
return SDL_WriteLE64(rwops_, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -113,6 +113,19 @@ public:
|
||||
size_t Write(const void* ptr, size_t size, size_t num);
|
||||
Sint64 Tell();
|
||||
Sint64 Size();
|
||||
|
||||
Uint16 ReadBE16();
|
||||
Uint32 ReadBE32();
|
||||
Uint64 ReadBE64();
|
||||
Uint16 ReadLE16();
|
||||
Uint32 ReadLE32();
|
||||
Uint64 ReadLE64();
|
||||
size_t WriteBE16(Uint16 value);
|
||||
size_t WriteBE32(Uint32 value);
|
||||
size_t WriteBE64(Uint64 value);
|
||||
size_t WriteLE16(Uint16 value);
|
||||
size_t WriteLE32(Uint32 value);
|
||||
size_t WriteLE64(Uint64 value);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -33,7 +33,8 @@
|
||||
#include <SDL2pp/Rect.hh>
|
||||
#include <SDL2pp/Point.hh>
|
||||
#include <SDL2pp/RWops.hh>
|
||||
#include <SDL2pp/ExtraRWops.hh>
|
||||
#include <SDL2pp/ContainerRWops.hh>
|
||||
#include <SDL2pp/StreamRWops.hh>
|
||||
#include <SDL2pp/Wav.hh>
|
||||
|
||||
#endif
|
||||
|
159
SDL2pp/StreamRWops.hh
Normal file
159
SDL2pp/StreamRWops.hh
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
libSDL2pp - C++11 bindings/wrapper for SDL2
|
||||
Copyright (C) 2014 Dmitry Marakasov <amdmi3@amdmi3.ru>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#ifndef SDL2PP_STREAMRWOPS_HH
|
||||
#define SDL2PP_STREAMRWOPS_HH
|
||||
|
||||
#include <SDL2pp/RWops.hh>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <type_traits>
|
||||
|
||||
namespace SDL2pp {
|
||||
|
||||
template <class S>
|
||||
class StreamRWops : public CustomRWops {
|
||||
// since STL streams have different pointers for reading and writing,
|
||||
// supporting both at the same time is impossible
|
||||
static_assert(!(std::is_base_of<std::istream, S>::value && std::is_base_of<std::ostream, S>::value), "StreamRWops does not support reading and writing at the same time");
|
||||
|
||||
protected:
|
||||
S& stream_;
|
||||
|
||||
private:
|
||||
template <class SS>
|
||||
typename std::enable_if<std::is_base_of<std::istream, SS>::value && !std::is_base_of<std::ostream, SS>::value, void>::type SeekHelper(off_t off, std::ios_base::seekdir dir) {
|
||||
stream_.seekg(off, dir);
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<!std::is_base_of<std::istream, SS>::value && std::is_base_of<std::ostream, SS>::value, void>::type SeekHelper(off_t off, std::ios_base::seekdir dir) {
|
||||
stream_.seekp(off, dir);
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<std::is_base_of<std::istream, SS>::value && !std::is_base_of<std::ostream, SS>::value, off_t>::type TellHelper() {
|
||||
return stream_.tellg();
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<!std::is_base_of<std::istream, SS>::value && std::is_base_of<std::ostream, SS>::value, off_t>::type TellHelper() {
|
||||
return stream_.tellp();
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<std::is_base_of<std::istream, SS>::value, size_t>::type ReadHelper(void* ptr, size_t size, size_t maxnum) {
|
||||
stream_.read(static_cast<char*>(ptr), size * maxnum);
|
||||
size_t nread = stream_.gcount();
|
||||
|
||||
// eof is OK
|
||||
if (stream_.rdstate() == (std::ios_base::eofbit | std::ios_base::failbit))
|
||||
stream_.clear();
|
||||
|
||||
if (nread != size * maxnum) {
|
||||
// short read
|
||||
char* pos = static_cast<char*>(ptr);
|
||||
pos += nread;
|
||||
|
||||
int count = nread % size;
|
||||
|
||||
// put partially read object back into the stream
|
||||
while (--count >= 0)
|
||||
stream_.putback(*--pos);
|
||||
}
|
||||
|
||||
return nread / size;
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<!std::is_base_of<std::istream, SS>::value, size_t>::type ReadHelper(void*, size_t, size_t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<std::is_base_of<std::ostream, SS>::value, size_t>::type WriteHelper(const void* ptr, size_t size, size_t maxnum) {
|
||||
stream_.write(static_cast<const char*>(ptr), size * maxnum);
|
||||
// XXX: there seem to be no reliable way to tell how much
|
||||
// was actually written
|
||||
if (stream_.rdstate() & std::ios_base::badbit)
|
||||
return 0;
|
||||
return maxnum;
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<!std::is_base_of<std::ostream, SS>::value, size_t>::type WriteHelper(const void*, size_t, size_t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<std::is_base_of<std::ostream, SS>::value, int>::type CloseHelper() {
|
||||
stream_.flush();
|
||||
return stream_.rdstate() & std::ios_base::badbit;
|
||||
}
|
||||
|
||||
template <class SS>
|
||||
typename std::enable_if<!std::is_base_of<std::ostream, SS>::value, int>::type CloseHelper() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public:
|
||||
StreamRWops(S& stream) : stream_(stream) {
|
||||
}
|
||||
|
||||
StreamRWops(const StreamRWops<S>&) = default;
|
||||
StreamRWops& operator=(const StreamRWops<S>&) = delete;
|
||||
StreamRWops(StreamRWops<S>&&) noexcept = default;
|
||||
StreamRWops& operator=(StreamRWops<S>&&) = delete;
|
||||
|
||||
virtual Sint64 Seek(Sint64 offset, int whence) override {
|
||||
switch (whence) {
|
||||
case RW_SEEK_SET:
|
||||
SeekHelper<S>(offset, std::ios_base::beg);
|
||||
break;
|
||||
case RW_SEEK_CUR:
|
||||
SeekHelper<S>(offset, std::ios_base::cur);
|
||||
break;
|
||||
case RW_SEEK_END:
|
||||
SeekHelper<S>(offset, std::ios_base::end);
|
||||
break;
|
||||
default:
|
||||
throw std::logic_error("Unexpected whence value for StreamRWops::Seek");
|
||||
}
|
||||
return TellHelper<S>();
|
||||
}
|
||||
|
||||
virtual size_t Read(void* ptr, size_t size, size_t maxnum) override {
|
||||
return ReadHelper<S>(ptr, size, maxnum);
|
||||
}
|
||||
|
||||
virtual size_t Write(const void* ptr, size_t size, size_t maxnum) override {
|
||||
return WriteHelper<S>(ptr, size, maxnum);
|
||||
}
|
||||
|
||||
virtual int Close() override {
|
||||
return CloseHelper<S>();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -8,7 +8,8 @@ SET(HEADER_TESTS
|
||||
header_rect
|
||||
header_renderer
|
||||
header_rwops
|
||||
header_extrarwops
|
||||
header_containerrwops
|
||||
header_streamrwops
|
||||
header_sdl
|
||||
header_sdl2pp
|
||||
header_texture
|
||||
|
5
tests/header_containerrwops.cc
Normal file
5
tests/header_containerrwops.cc
Normal file
@ -0,0 +1,5 @@
|
||||
#include <SDL2pp/ContainerRWops.hh>
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
#include <SDL2pp/ExtraRWops.hh>
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
5
tests/header_streamrwops.cc
Normal file
5
tests/header_streamrwops.cc
Normal file
@ -0,0 +1,5 @@
|
||||
#include <SDL2pp/StreamRWops.hh>
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include <SDL2pp/Exception.hh>
|
||||
#include <SDL2pp/ExtraRWops.hh>
|
||||
#include <SDL2pp/ContainerRWops.hh>
|
||||
#include <SDL2pp/StreamRWops.hh>
|
||||
#include <SDL2pp/RWops.hh>
|
||||
|
||||
#include "testing.h"
|
||||
@ -118,7 +119,7 @@ BEGIN_TEST()
|
||||
{
|
||||
const std::vector<char> buffer = { 'a', 'b', 'c', 'd' };
|
||||
|
||||
RWops rw((ConstContainerRWops<std::vector<char>>(buffer)));
|
||||
RWops rw((ContainerRWops<const std::vector<char>>(buffer)));
|
||||
|
||||
{
|
||||
// Read via C++
|
||||
@ -133,12 +134,90 @@ BEGIN_TEST()
|
||||
}
|
||||
|
||||
{
|
||||
// Write
|
||||
// Write to const container fails
|
||||
char buf[4] = {0};
|
||||
|
||||
EXPECT_TRUE(rw.Write(buf, 1, 4) == 0);
|
||||
EXPECT_TRUE(rw.Write(buf, 4, 1) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Write to non-const container
|
||||
std::vector<char> vec;
|
||||
|
||||
RWops rw((ContainerRWops<std::vector<char>>(vec)));
|
||||
|
||||
char buf[4] = {'a', 'b', 'c', 'd'};
|
||||
|
||||
EXPECT_TRUE(rw.Write(buf, 1, 4) == 4);
|
||||
EXPECT_TRUE(rw.Write(buf, 4, 1) == 1);
|
||||
|
||||
EXPECT_TRUE(rw.Seek(2, SEEK_SET) == 2);
|
||||
EXPECT_TRUE(rw.Write(buf, 2, 2) == 2);
|
||||
|
||||
EXPECT_TRUE(vec.size() == 8);
|
||||
EXPECT_TRUE(std::string(vec.data(), 8) == "ababcdcd");
|
||||
}
|
||||
}
|
||||
|
||||
// Test for StreamRWops
|
||||
{
|
||||
{
|
||||
// write test
|
||||
|
||||
std::stringstream test;
|
||||
RWops rw((StreamRWops<std::ostream>(test)));
|
||||
|
||||
char buf[4] = { 'a', 'b', 'c', 'd' };
|
||||
EXPECT_TRUE(rw.Write(buf, 1, 4) == 4);
|
||||
|
||||
EXPECT_TRUE(rw.Seek(0, RW_SEEK_CUR) == 4);
|
||||
|
||||
EXPECT_TRUE(rw.Seek(2, RW_SEEK_SET) == 2);
|
||||
|
||||
EXPECT_TRUE(rw.Write(buf, 1, 4) == 4);
|
||||
|
||||
EXPECT_EQUAL(test.str(), "ababcd");
|
||||
}
|
||||
|
||||
{
|
||||
// read test
|
||||
|
||||
std::stringstream test("abcdef");
|
||||
RWops rw((StreamRWops<std::istream>(test)));
|
||||
|
||||
char buf[4];
|
||||
EXPECT_EQUAL(rw.Read(buf, 1, 4), 4UL);
|
||||
|
||||
EXPECT_EQUAL(std::string(buf, 4), "abcd");
|
||||
|
||||
EXPECT_EQUAL(rw.Seek(0, RW_SEEK_CUR), 4);
|
||||
|
||||
EXPECT_EQUAL(rw.Seek(2, RW_SEEK_SET), 2);
|
||||
|
||||
EXPECT_EQUAL(rw.Read(buf, 1, 4), 4UL);
|
||||
|
||||
EXPECT_EQUAL(std::string(buf, 4), "cdef");
|
||||
|
||||
// short read
|
||||
EXPECT_EQUAL(rw.Seek(4, RW_SEEK_SET), 4);
|
||||
|
||||
EXPECT_EQUAL(rw.Read(buf, 1, 4), 2UL);
|
||||
|
||||
EXPECT_EQUAL(std::string(buf, 2), "ef");
|
||||
|
||||
// short object read
|
||||
EXPECT_EQUAL(rw.Seek(4, RW_SEEK_SET), 4);
|
||||
|
||||
EXPECT_EQUAL(rw.Read(buf, 4, 1), 0UL);
|
||||
|
||||
EXPECT_EQUAL(rw.Seek(0, RW_SEEK_CUR), 4);
|
||||
|
||||
// read end
|
||||
EXPECT_EQUAL(rw.Read(buf, 1, 2), 2UL);
|
||||
|
||||
EXPECT_EQUAL(std::string(buf, 2), "ef");
|
||||
}
|
||||
}
|
||||
|
||||
// SDL file read test
|
||||
@ -154,6 +233,36 @@ BEGIN_TEST()
|
||||
|
||||
rw.Close();
|
||||
}
|
||||
|
||||
// Fixed width reads/writes
|
||||
{
|
||||
std::vector<char> data, outdata;
|
||||
for (int i = 0; i < 28; i++)
|
||||
data.push_back(i);
|
||||
|
||||
RWops rw((ContainerRWops<std::vector<char>>(data)));
|
||||
|
||||
EXPECT_EQUAL(rw.ReadBE16(), 0x0001U);
|
||||
EXPECT_EQUAL(rw.ReadLE16(), 0x0302U);
|
||||
EXPECT_EQUAL(rw.ReadBE32(), 0x04050607U);
|
||||
EXPECT_EQUAL(rw.ReadLE32(), 0x0B0A0908U);
|
||||
EXPECT_EQUAL(rw.ReadBE64(), 0x0C0D0E0F10111213ULL);
|
||||
EXPECT_EQUAL(rw.ReadLE64(), 0x1B1A191817161514ULL);
|
||||
|
||||
RWops rw1((ContainerRWops<std::vector<char>>(outdata)));
|
||||
|
||||
EXPECT_EQUAL(rw1.WriteBE16(0x0001U), 1U);
|
||||
EXPECT_EQUAL(rw1.WriteLE16(0x0302U), 1U);
|
||||
EXPECT_EQUAL(rw1.WriteBE32(0x04050607U), 1U);
|
||||
EXPECT_EQUAL(rw1.WriteLE32(0x0B0A0908U), 1U);
|
||||
EXPECT_EQUAL(rw1.WriteBE64(0x0C0D0E0F10111213ULL), 1U);
|
||||
EXPECT_EQUAL(rw1.WriteLE64(0x1B1A191817161514ULL), 1U);
|
||||
|
||||
EXPECT_EQUAL(data.size(), outdata.size());
|
||||
|
||||
EXPECT_TRUE(data == outdata);
|
||||
}
|
||||
|
||||
HANDLE_EXCEPTION(Exception& e)
|
||||
std::cerr << "unexpected SDL exception was thrown during the test: " << e.what() << ": " << e.GetSDLError() << std::endl;
|
||||
END_TEST()
|
||||
|
Loading…
x
Reference in New Issue
Block a user