add shared_array
simpler version of epics::pvData::shared_vector<T> w/ offset+capacity tracking, or value conversion. Updated to use c++11 features (like std::type_index).
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Copyright - See the COPYRIGHT that is included with this distribution.
|
||||
* pvxs is distributed subject to a Software License Agreement found
|
||||
* in file LICENSE that is included with this distribution.
|
||||
*/
|
||||
#ifndef PVXS_SHAREDVECTOR_H
|
||||
#define PVXS_SHAREDVECTOR_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <algorithm>
|
||||
#include <ostream>
|
||||
|
||||
#include <pvxs/version.h>
|
||||
|
||||
namespace pvxs {
|
||||
|
||||
class Value;
|
||||
|
||||
template<typename E, class Enable = void> class shared_array;
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename T, typename Enable=void>
|
||||
struct sizeofx {
|
||||
static inline size_t op() { return sizeof(T); }
|
||||
};
|
||||
template<typename T>
|
||||
struct sizeofx<T, typename std::enable_if<std::is_void<T>{}>::type> {
|
||||
static inline size_t op() { return 1u; } // treat void* as pointer to bytes
|
||||
};
|
||||
|
||||
template<typename E>
|
||||
struct sa_default_delete {
|
||||
void operator()(E* e) const { delete[] e; }
|
||||
};
|
||||
|
||||
template<typename E>
|
||||
struct sa_base {
|
||||
protected:
|
||||
template<typename E1> friend struct sa_base;
|
||||
|
||||
std::shared_ptr<E> _data;
|
||||
size_t _size;
|
||||
public:
|
||||
|
||||
// shared_array()
|
||||
// shared_array(const shared_array&)
|
||||
// shared_array(shared_array&&)
|
||||
// shared_array(size_t, T)
|
||||
// shared_array(T*, size_t)
|
||||
// shared_array(T*, d, size_t)
|
||||
// shared_array(shared_ptr<T>, size_t)
|
||||
// shared_array(shared_ptr<T>, T*, size_t)
|
||||
|
||||
//! empty
|
||||
constexpr sa_base() :_size(0u) {}
|
||||
|
||||
// copyable
|
||||
sa_base(const sa_base&) = default;
|
||||
// movable
|
||||
inline sa_base(sa_base&& o) noexcept
|
||||
:_data(std::move(o._data)), _size(o._size)
|
||||
{
|
||||
o._size = 0;
|
||||
}
|
||||
sa_base& operator=(const sa_base&) =default;
|
||||
inline sa_base& operator=(sa_base&& o) noexcept
|
||||
{
|
||||
_data = std::move(o._data);
|
||||
_size = o._size;
|
||||
o._size = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// use existing alloc with delete[]
|
||||
template<typename A>
|
||||
sa_base(A* a, size_t len)
|
||||
:_data(a, sa_default_delete<E>()),_size(len)
|
||||
{}
|
||||
|
||||
// use existing alloc w/ custom deletor
|
||||
template<typename B>
|
||||
sa_base(E* a, B b, size_t len)
|
||||
:_data(a, b),_size(len)
|
||||
{}
|
||||
|
||||
// build around existing shared_ptr
|
||||
sa_base(const std::shared_ptr<E>& a, size_t len)
|
||||
:_data(a),_size(len)
|
||||
{}
|
||||
|
||||
// alias existing shared_ptr
|
||||
template<typename A>
|
||||
sa_base(const std::shared_ptr<A>& a, E* b, size_t len)
|
||||
:_data(a, b),_size(len)
|
||||
{}
|
||||
|
||||
void clear() noexcept {
|
||||
_data.reset();
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
void swap(sa_base& o) noexcept {
|
||||
std::swap(_data, o._data);
|
||||
std::swap(_size, o._data);
|
||||
}
|
||||
|
||||
inline size_t size() const { return _size; }
|
||||
inline bool empty() const noexcept { return _size==0; }
|
||||
|
||||
inline bool unique() const noexcept { return !_data || _data.use_count()<=1; }
|
||||
|
||||
E* data() const noexcept { return _data.get(); }
|
||||
|
||||
const std::shared_ptr<E>& dataPtr() const { return _data; }
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename E, class Enable>
|
||||
class shared_array : public detail::sa_base<E> {
|
||||
static_assert (!std::is_void<E>::value, "non-void specialization");
|
||||
|
||||
template<typename E1, class Enable1> friend class shared_array;
|
||||
|
||||
typedef detail::sa_base<E> base_t;
|
||||
typedef typename std::remove_const<E>::type _E_non_const;
|
||||
public:
|
||||
typedef E value_type;
|
||||
typedef E& reference;
|
||||
typedef typename std::add_const<E>::type& const_reference;
|
||||
typedef E* pointer;
|
||||
typedef typename std::add_const<E>::type* const_pointer;
|
||||
typedef E* iterator;
|
||||
typedef std::reverse_iterator<iterator> reverse_iterator;
|
||||
typedef typename std::add_const<E>::type* const_iterator;
|
||||
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
|
||||
typedef std::ptrdiff_t difference_type;
|
||||
typedef size_t size_type;
|
||||
|
||||
typedef E element_type;
|
||||
|
||||
constexpr shared_array() noexcept :base_t() {}
|
||||
|
||||
template<typename A>
|
||||
shared_array(std::initializer_list<A> L)
|
||||
:base_t(new _E_non_const[L.size()], L.size())
|
||||
{
|
||||
_E_non_const *raw = const_cast<_E_non_const*>(this->data());
|
||||
std::copy(L.begin(), L.end(), raw);
|
||||
}
|
||||
|
||||
//! @brief Allocate (with new[]) a new vector of size c
|
||||
explicit shared_array(size_t c)
|
||||
:base_t(new _E_non_const[c], c)
|
||||
{}
|
||||
|
||||
//! @brief Allocate (with new[]) a new vector of size c and fill with value e
|
||||
template<typename V>
|
||||
shared_array(size_t c, V e)
|
||||
:base_t(new _E_non_const[c], c)
|
||||
{
|
||||
std::fill_n((_E_non_const*)this->_data.get(), this->_size, e);
|
||||
}
|
||||
|
||||
// use existing alloc with delete[]
|
||||
shared_array(E* a, size_t len)
|
||||
:base_t(a, len)
|
||||
{}
|
||||
|
||||
// use existing alloc w/ custom deletor
|
||||
template<typename B>
|
||||
shared_array(E* a, B b, size_t len)
|
||||
:base_t(a, b, len)
|
||||
{}
|
||||
|
||||
// build around existing shared_ptr
|
||||
shared_array(const std::shared_ptr<E>& a, size_t len)
|
||||
:base_t(a, len)
|
||||
{}
|
||||
|
||||
// alias existing shared_array
|
||||
template<typename A>
|
||||
shared_array(const std::shared_ptr<A>& a, E* b, size_t len)
|
||||
:base_t(a, b, len)
|
||||
{}
|
||||
|
||||
size_t max_size() const noexcept {return ((size_t)-1)/sizeof(E);}
|
||||
|
||||
inline void reserve(size_t i) {}
|
||||
|
||||
void resize(size_t i) {
|
||||
if(!this->unique() || i!=this->_size) {
|
||||
shared_array o(i);
|
||||
std::copy_n(this->begin(), std::min(this->size(), i), o.begin());
|
||||
this->swap(o);
|
||||
}
|
||||
}
|
||||
|
||||
inline void make_unique() {
|
||||
this->resize(this->size());
|
||||
}
|
||||
|
||||
private:
|
||||
/* Hack alert.
|
||||
* For reasons of simplicity and efficiency, we want to use raw pointers for iteration.
|
||||
* However, shared_ptr::get() isn't defined when !_data, although practically it gives NULL.
|
||||
* Unfortunately, many of the MSVC (<= VS 2010) STL methods assert() that iterators are never NULL.
|
||||
* So we fudge here by abusing 'this' so that our iterators are always !NULL.
|
||||
*/
|
||||
inline E* base_ptr() const {
|
||||
#if defined(_MSC_VER) && _MSC_VER<=1600
|
||||
return this->_size ? this->_data.get() : (E*)(this-1);
|
||||
#else
|
||||
return this->_data.get();
|
||||
#endif
|
||||
}
|
||||
public:
|
||||
// STL iterators
|
||||
|
||||
inline iterator begin() const noexcept{return this->base_ptr();}
|
||||
inline const_iterator cbegin() const noexcept{return begin();}
|
||||
|
||||
inline iterator end() const noexcept{return this->base_ptr()+this->_size;}
|
||||
inline const_iterator cend() const noexcept{return end();}
|
||||
|
||||
inline reverse_iterator rbegin() const noexcept{return reverse_iterator(end());}
|
||||
inline const_reverse_iterator crbegin() const noexcept{return rbegin();}
|
||||
|
||||
inline reverse_iterator rend() const noexcept{return reverse_iterator(begin());}
|
||||
inline const_reverse_iterator crend() const noexcept{return rend();}
|
||||
|
||||
inline reference front() const noexcept{return (*this)[0];}
|
||||
inline reference back() const noexcept{return (*this)[this->m_count-1];}
|
||||
|
||||
//! @brief Member access
|
||||
//! @pre !empty() && i<size()
|
||||
//! Use sa.data() instead of &sa[0]
|
||||
inline reference operator[](size_t i) const noexcept {return this->_data.get()[i];}
|
||||
|
||||
//! @brief Member access
|
||||
//! @throws std::out_of_range if empty() || i>=size().
|
||||
reference at(size_t i) const
|
||||
{
|
||||
if(i>this->_size)
|
||||
throw std::out_of_range("Index out of bounds");
|
||||
return (*this)[i];
|
||||
}
|
||||
};
|
||||
|
||||
enum class ArrayType : uint8_t {
|
||||
Null = 0xff,
|
||||
Bool = 0x08,
|
||||
Int8 = 0x28,
|
||||
Int16 = 0x29,
|
||||
Int32 = 0x2a,
|
||||
Int64 = 0x2b,
|
||||
UInt8 = 0x2c,
|
||||
UInt16= 0x2d,
|
||||
UInt32= 0x2e,
|
||||
UInt64= 0x2f,
|
||||
Float = 0x4a,
|
||||
Double= 0x4b,
|
||||
String= 0x68,
|
||||
Value = 0x88, // also used for 0x89 and 0x8a
|
||||
};
|
||||
|
||||
PVXS_API
|
||||
std::ostream& operator<<(std::ostream& strm, ArrayType code);
|
||||
|
||||
namespace detail {
|
||||
template<typename T>
|
||||
struct CaptureCode;
|
||||
|
||||
#define CASE(TYPE, CODE) \
|
||||
template<> struct CaptureCode<TYPE> { static constexpr ArrayType code{ArrayType::CODE}; }
|
||||
CASE(bool, Bool);
|
||||
CASE(int8_t, Int8);
|
||||
CASE(int16_t, Int16);
|
||||
CASE(int32_t, Int32);
|
||||
CASE(int64_t, Int64);
|
||||
CASE(uint8_t, UInt8);
|
||||
CASE(uint16_t, UInt16);
|
||||
CASE(uint32_t, UInt32);
|
||||
CASE(uint64_t, UInt64);
|
||||
CASE(float, Float);
|
||||
CASE(double, Double);
|
||||
CASE(std::string, String);
|
||||
CASE(Value, Value);
|
||||
#undef CASE
|
||||
|
||||
} // namespace detail
|
||||
|
||||
|
||||
template<typename E>
|
||||
class shared_array<E, typename std::enable_if<std::is_void<E>{}>::type >
|
||||
: public detail::sa_base<E>
|
||||
{
|
||||
static_assert (std::is_void<E>::value, "void specialization");
|
||||
|
||||
template<typename E1, class Enable1> friend class shared_array;
|
||||
|
||||
typedef detail::sa_base<E> base_t;
|
||||
typedef typename std::remove_const<E>::type _E_non_const;
|
||||
|
||||
ArrayType _type;
|
||||
public:
|
||||
typedef E value_type;
|
||||
typedef E* pointer;
|
||||
typedef std::ptrdiff_t difference_type;
|
||||
typedef size_t size_type;
|
||||
|
||||
//! empty array, untyped
|
||||
constexpr shared_array() noexcept :base_t(), _type(ArrayType::Null) {}
|
||||
//! empty array, typed
|
||||
constexpr explicit shared_array(ArrayType code) noexcept :base_t(), _type(code) {}
|
||||
//! copy
|
||||
shared_array(const shared_array& o) = default;
|
||||
//! move
|
||||
inline shared_array(shared_array&& o) noexcept
|
||||
:base_t(std::move(o))
|
||||
,_type(o._type)
|
||||
{
|
||||
o._type = ArrayType::Null;
|
||||
}
|
||||
//! assign
|
||||
shared_array& operator=(const shared_array&) =default;
|
||||
//! move
|
||||
inline shared_array& operator=(shared_array&& o) noexcept
|
||||
{
|
||||
base_t::operator=(std::move(o));
|
||||
_type = o._type;
|
||||
o._type = ArrayType::Null;
|
||||
return *this;
|
||||
}
|
||||
|
||||
//! use existing alloc with delete[]
|
||||
shared_array(E* a, size_t len)
|
||||
:base_t(a, len)
|
||||
,_type(detail::CaptureCode<typename std::remove_cv<E>::type>::code)
|
||||
{}
|
||||
|
||||
//! use existing alloc w/ custom deletor
|
||||
template<typename B>
|
||||
shared_array(E* a, B b, size_t len)
|
||||
:base_t(a, b, len)
|
||||
,_type(detail::CaptureCode<typename std::remove_cv<E>::type>::code)
|
||||
{}
|
||||
|
||||
//! build around existing shared_ptr and length
|
||||
shared_array(const std::shared_ptr<E>& a, size_t len)
|
||||
:base_t(a, len)
|
||||
,_type(detail::CaptureCode<typename std::remove_cv<E>::type>::code)
|
||||
{}
|
||||
|
||||
//! alias existing shared_ptr and length
|
||||
template<typename A>
|
||||
shared_array(const std::shared_ptr<A>& a, E* b, size_t len)
|
||||
:base_t(a, b, len)
|
||||
,_type(detail::CaptureCode<typename std::remove_cv<A>::type>::code)
|
||||
{}
|
||||
|
||||
//! clear data and become untyped
|
||||
void clear() noexcept {
|
||||
base_t::clear();
|
||||
_type = ArrayType::Null;
|
||||
}
|
||||
|
||||
//! exchange
|
||||
void swap(shared_array& o) noexcept {
|
||||
base_t::swap(o);
|
||||
std::swap(_type, o._type);
|
||||
}
|
||||
|
||||
size_t max_size() const noexcept{return (size_t)-1;}
|
||||
|
||||
inline ArrayType original_type() const { return _type; }
|
||||
};
|
||||
|
||||
// non-const -> const
|
||||
template <typename SRC>
|
||||
static inline
|
||||
shared_array<typename std::add_const<typename SRC::value_type>::type>
|
||||
freeze(SRC&& src)
|
||||
{
|
||||
typedef typename SRC::value_type FROM;
|
||||
typedef typename std::add_const<FROM>::type TO;
|
||||
|
||||
if(!src.unique())
|
||||
throw std::logic_error("Can't freeze non-unique shared_array");
|
||||
|
||||
// cast data pointer to const
|
||||
TO* data = src.data();
|
||||
|
||||
shared_array<TO> ret(src.dataPtr(), data, src.size());
|
||||
|
||||
// c++20 provides a move()-able alternative to the aliasing constructor.
|
||||
// until this stops being the future, we consume the src ref. and
|
||||
// inc. + dec. the ref counter...
|
||||
auto temp(std::move(src));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// change type, while keeping same const
|
||||
template<typename TO, typename FROM,
|
||||
typename std::enable_if< std::is_const<TO>{} == std::is_const<FROM>{}, int >::type=0>
|
||||
static inline
|
||||
shared_array<TO>
|
||||
shared_array_static_cast(const shared_array<FROM>& src)
|
||||
{
|
||||
size_t newsize = src.size()*detail::sizeofx<FROM>::op()/detail::sizeofx<TO>::op();
|
||||
return shared_array<TO>(src.dataPtr(), static_cast<TO*>(src.data()), newsize);
|
||||
}
|
||||
|
||||
template<typename E, typename std::enable_if<!std::is_void<E>{}, int>::type =0>
|
||||
std::ostream& operator<<(std::ostream& strm, const shared_array<E>& arr)
|
||||
{
|
||||
strm<<'{'<<arr.size()<<"}[";
|
||||
for(size_t i=0; i<arr.size(); i++) {
|
||||
if(i>10) {
|
||||
strm<<"...";
|
||||
break;
|
||||
}
|
||||
strm<<arr[i];
|
||||
if(i+1<arr.size())
|
||||
strm<<", ";
|
||||
}
|
||||
strm<<']';
|
||||
return strm;
|
||||
}
|
||||
|
||||
PVXS_API
|
||||
std::ostream& operator<<(std::ostream& strm, const shared_array<const void>& arr);
|
||||
|
||||
PVXS_API
|
||||
std::ostream& operator<<(std::ostream& strm, const shared_array<void>& arr);
|
||||
|
||||
} // namespace pvxs
|
||||
|
||||
#endif // PVXS_SHAREDVECTOR_H
|
||||
@@ -13,6 +13,7 @@
|
||||
*/
|
||||
|
||||
#include <sstream>
|
||||
#include <functional>
|
||||
|
||||
#include <pvxs/version.h>
|
||||
#include <pvxs/util.h>
|
||||
@@ -41,6 +42,11 @@ public:
|
||||
|
||||
explicit operator bool() const { return result==Pass; }
|
||||
|
||||
testCase& setPass(bool v) {
|
||||
result = v ? Pass : Fail;
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline testCase& operator<<(const T& v) {
|
||||
msg<<v;
|
||||
@@ -99,6 +105,21 @@ testCase testNotEq(const char *sLHS, const LHS& lhs, const char *sRHS, const RHS
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<class Exception, typename FN>
|
||||
testCase testThrows(FN fn)
|
||||
{
|
||||
testCase ret(false);
|
||||
try {
|
||||
fn();
|
||||
ret<<"Unexpected success - ";
|
||||
}catch(Exception& e){
|
||||
ret.setPass(true)<<"Expected exception \""<<e.what()<<"\" - ";
|
||||
}catch(std::exception& e){
|
||||
ret<<"Unexpected exception "<<typeid(e).name()<<" \""<<e.what()<<"\" - ";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace pvxs
|
||||
|
||||
#define testEq(LHS, RHS) ::pvxs::detail::testEq(#LHS, LHS, #RHS, RHS)
|
||||
|
||||
@@ -22,6 +22,10 @@ TESTPROD += testudp
|
||||
testudp_SRCS += testudp.cpp
|
||||
TESTS += testudp
|
||||
|
||||
TESTPROD += testshared
|
||||
testshared_SRCS += testshared.cpp
|
||||
TESTS += testshared
|
||||
|
||||
TESTPROD += dummyserv
|
||||
dummyserv_SRCS += dummyserv.cpp
|
||||
# not a unittest
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright - See the COPYRIGHT that is included with this distribution.
|
||||
* pvxs is distributed subject to a Software License Agreement found
|
||||
* in file LICENSE that is included with this distribution.
|
||||
*/
|
||||
|
||||
#include <typeinfo>
|
||||
|
||||
#include <pvxs/sharedArray.h>
|
||||
|
||||
#include <pvxs/unittest.h>
|
||||
#include <epicsUnitTest.h>
|
||||
#include <testMain.h>
|
||||
|
||||
namespace {
|
||||
using namespace pvxs;
|
||||
|
||||
template<typename E>
|
||||
void testEmpty()
|
||||
{
|
||||
testDiag("%s", __func__);
|
||||
|
||||
shared_array<E> v;
|
||||
|
||||
testOk1(v.unique());
|
||||
testOk1(v.empty());
|
||||
|
||||
testEq(v.size(), 0u);
|
||||
}
|
||||
|
||||
template<typename I>
|
||||
void testInt()
|
||||
{
|
||||
testDiag("%s w/ %s", __func__, typeid(I).name());
|
||||
|
||||
shared_array<I> X(2, 5);
|
||||
testOk1(X.unique());
|
||||
testOk1(!X.empty());
|
||||
if(testEq(X.size(), 2u)) {
|
||||
testEq(X[0], 5);
|
||||
testEq(X[1], 5);
|
||||
}
|
||||
|
||||
shared_array<I> Y(X);
|
||||
testOk1(!X.unique());
|
||||
testOk1(!Y.unique());
|
||||
testEq(X.size(), Y.size());
|
||||
|
||||
X.clear();
|
||||
testOk1(X.unique());
|
||||
testOk1(Y.unique());
|
||||
testEq(X.size(), 0u);
|
||||
testEq(Y.size(), 2u);
|
||||
|
||||
X = std::move(Y);
|
||||
testOk1(X.unique());
|
||||
testOk1(Y.unique());
|
||||
testEq(X.size(), 2u);
|
||||
testEq(Y.size(), 0u);
|
||||
|
||||
shared_array<I> Z(std::move(X));
|
||||
testOk1(X.unique());
|
||||
testOk1(Y.unique());
|
||||
testOk1(Z.unique());
|
||||
testEq(X.size(), 0u);
|
||||
testEq(Y.size(), 0u);
|
||||
testEq(Z.size(), 2u);
|
||||
|
||||
// copy empty
|
||||
shared_array<I> Q(Y);
|
||||
testOk1(Y.unique());
|
||||
testOk1(Q.unique());
|
||||
testEq(Y.size(), 0u);
|
||||
testEq(Q.size(), 0u);
|
||||
}
|
||||
|
||||
template<typename Void, typename I>
|
||||
void testVoid()
|
||||
{
|
||||
testDiag("%s", __func__);
|
||||
|
||||
shared_array<I> X(2);
|
||||
|
||||
shared_array<Void> Y(shared_array_static_cast<Void>(X));
|
||||
testOk1(!X.unique());
|
||||
testOk1(!Y.unique());
|
||||
testEq(X.size(), 2u);
|
||||
testEq(Y.size(), 8u);
|
||||
testEq(Y.original_type(), ArrayType::UInt32); // never const uint32_t
|
||||
}
|
||||
|
||||
void testFreeze()
|
||||
{
|
||||
testDiag("%s", __func__);
|
||||
|
||||
shared_array<uint32_t> X(2, 5);
|
||||
shared_array<const uint32_t> Y(freeze(std::move(X)));
|
||||
testOk1(X.unique());
|
||||
testOk1(Y.unique());
|
||||
testEq(X.size(), 0u);
|
||||
testEq(Y.size(), 2u);
|
||||
}
|
||||
|
||||
void testFreezeError()
|
||||
{
|
||||
testDiag("%s", __func__);
|
||||
|
||||
shared_array<uint32_t> X(2, 5), Z(X);
|
||||
testOk1(!X.unique());
|
||||
testThrows<std::logic_error>([&X]() {
|
||||
shared_array<const uint32_t> Y(freeze(std::move(X)));
|
||||
})<<"Attempt to freeze() non-unique";
|
||||
}
|
||||
|
||||
void testComplex()
|
||||
{
|
||||
testDiag("%s", __func__);
|
||||
|
||||
shared_array<std::unique_ptr<uint32_t>> X(2, nullptr);
|
||||
|
||||
X[0] = decltype (X)::value_type{new uint32_t(4u)};
|
||||
testEq(*X[0], 4u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MAIN(testshared)
|
||||
{
|
||||
testPlan(81);
|
||||
testEmpty<void>();
|
||||
testEmpty<const void>();
|
||||
testEmpty<int32_t>();
|
||||
testEmpty<const int32_t>();
|
||||
testInt<int32_t>();
|
||||
testInt<const int32_t>();
|
||||
testVoid<void, uint32_t>();
|
||||
testVoid<const void, const uint32_t>();
|
||||
testFreeze();
|
||||
testFreezeError();
|
||||
testComplex();
|
||||
return testDone();
|
||||
}
|
||||
Reference in New Issue
Block a user