diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..84d328a7 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,70 @@ +name: Coverage + +on: + pull_request: + paths-ignore: + - 'docs/**' + push: + branches: + - main + paths-ignore: + - 'docs/**' + +jobs: + coverage-mac-clang: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + backends: [ V8, JavaScriptCore, Lua ] + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + key: build-dependencies-macos + path: | + build/ScriptXTestLibs + build/googletest-src + - name: prepare lcov + run: | + HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov + - name: Configure cmake + env: + SCRIPTX_TEST_FORCE_UPDATE_DEPS: ON + run: | + mkdir -p build && cd build + cmake ../test \ + -DSCRIPTX_BACKEND=${{ matrix.backends }} \ + -DDEVOPS_ENABLE_COVERAGE=ON \ + -DCMAKE_BUILD_TYPE=DEBUG + - name: Compile UnitTests + run: | + cd build + cmake --build . -j $(sysctl -n hw.ncpu) --target UnitTests + - name: Run UnitTests + run: | + cd build && ./UnitTests + - name: Generate Coverate Data + # https://clang.llvm.org/docs/SourceBasedCodeCoverage.html + run: | + cd build + lcov -c -d CMakeFiles/ -o all.info + lcov -e all.info -o coverage.info '*/ScriptX/src/*' '*/ScriptX/backend/*' + + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: './build/coverage.info' + flag-name: "Backend-${{ matrix.backends }}" + parallel: true + + coverage-finish: + needs: coverage-mac-clang + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/README-zh.md b/README-zh.md index 5f9ff803..df47106f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -4,7 +4,7 @@

全能的脚本引擎抽象层

--- -[![README-English](https://img.shields.io/badge/README-english-lightgreen)](README.md) [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0) [![UnitTests](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/tencent/ScriptX) ![Lines of code](https://img.shields.io/tokei/lines/github/tencent/ScriptX) ![GitHub top language](https://img.shields.io/github/languages/top/tencent/ScriptX) +[![README-English](https://img.shields.io/badge/README-english-lightgreen)](README.md) [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0) [![UnitTests](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/tencent/ScriptX) ![Lines of code](https://img.shields.io/tokei/lines/github/tencent/ScriptX) ![GitHub top language](https://img.shields.io/github/languages/top/tencent/ScriptX) [![Coverage Status](https://coveralls.io/repos/github/Tencent/ScriptX/badge.svg)](https://coveralls.io/github/Tencent/ScriptX) diff --git a/README.md b/README.md index 65463144..a98d5ac5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ --- -[![README中文](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-lightgreen)](README-zh.md) [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0) [![UnitTests](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/tencent/ScriptX) ![Lines of code](https://img.shields.io/tokei/lines/github/tencent/ScriptX) ![GitHub top language](https://img.shields.io/github/languages/top/tencent/ScriptX) +[![README中文](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-lightgreen)](README-zh.md) [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0) [![UnitTests](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/Tencent/ScriptX/actions/workflows/unit_tests.yml) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/tencent/ScriptX) ![Lines of code](https://img.shields.io/tokei/lines/github/tencent/ScriptX) ![GitHub top language](https://img.shields.io/github/languages/top/tencent/ScriptX) [![Coverage Status](https://coveralls.io/repos/github/Tencent/ScriptX/badge.svg)](https://coveralls.io/github/Tencent/ScriptX) ![ScriptX Architecture](docs/media/banner.webp) diff --git a/backend/JavaScriptCore/JscLocalReference.cc b/backend/JavaScriptCore/JscLocalReference.cc index 2af47146..0e853ccb 100644 --- a/backend/JavaScriptCore/JscLocalReference.cc +++ b/backend/JavaScriptCore/JscLocalReference.cc @@ -319,6 +319,7 @@ Local Local::asUnsupported() const { } bool Local::operator==(const script::Local& other) const { + if (isNull()) return other.isNull(); return JSValueIsStrictEqual(jsc_backend::currentEngineContextChecked(), const_cast(val_), const_cast(other.val_)); } diff --git a/backend/Lua/LuaEngine.cc b/backend/Lua/LuaEngine.cc index cd745d50..b51ec2c4 100644 --- a/backend/Lua/LuaEngine.cc +++ b/backend/Lua/LuaEngine.cc @@ -496,7 +496,7 @@ bool LuaEngine::isInstanceOf(lua_State* lua, int classIndex, int selfIndex) { } bool LuaEngine::isInstanceOf(lua_State* lua, const void* classDefine, int selfIndex) { - if (selfIndex == 0) { + if (selfIndex == 0 || classDefine == nullptr) { return false; } diff --git a/backend/Lua/LuaHelper.cc b/backend/Lua/LuaHelper.cc index b01bb8be..1b82ea0b 100644 --- a/backend/Lua/LuaHelper.cc +++ b/backend/Lua/LuaHelper.cc @@ -28,9 +28,11 @@ namespace { int luaErrorMessageHandler(lua_State* L) { auto msg = LuaEngine::make>(1); - auto error = Object::newObject(); - // set message - error.set("message", msg); + auto error = msg.isObject() ? msg.asObject() : Object::newObject(); + if (!msg.isObject() || error.has("message")) { + // set message + error.set("message", msg); + } // set stacktrace luaL_traceback(L, L, nullptr, 1); diff --git a/backend/Lua/LuaLocalReference.cc b/backend/Lua/LuaLocalReference.cc index cf82e679..00788a30 100644 --- a/backend/Lua/LuaLocalReference.cc +++ b/backend/Lua/LuaLocalReference.cc @@ -146,11 +146,11 @@ ValueKind Local::getKind() const { return ValueKind::kBoolean; } else if (type == LUA_TFUNCTION) { return ValueKind::kFunction; + } else if (isByteBuffer()) { + return ValueKind::kByteBuffer; } else if (type == LUA_TTABLE) { // lua don't have array type, the are all tables return ValueKind::kObject; - } else if (isByteBuffer()) { - return ValueKind::kByteBuffer; } else { return ValueKind::kUnsupported; } @@ -160,22 +160,32 @@ bool Local::isNull() const { return val_ == 0 || lua_isnoneornil(lua_backend::currentLua(), val_); } -bool Local::isString() const { return getKind() == ValueKind::kString; } +bool Local::isString() const { + return val_ != 0 && lua_type(lua_backend::currentLua(), val_) == LUA_TSTRING; +} -bool Local::isNumber() const { return getKind() == ValueKind::kNumber; } +bool Local::isNumber() const { + return val_ != 0 && lua_type(lua_backend::currentLua(), val_) == LUA_TNUMBER; +} -bool Local::isBoolean() const { return getKind() == ValueKind::kBoolean; } +bool Local::isBoolean() const { + return val_ != 0 && lua_type(lua_backend::currentLua(), val_) == LUA_TBOOLEAN; +} -bool Local::isFunction() const { return getKind() == ValueKind::kFunction; } +bool Local::isFunction() const { + return val_ != 0 && lua_type(lua_backend::currentLua(), val_) == LUA_TFUNCTION; +} -bool Local::isArray() const { return getKind() == ValueKind::kObject; } +bool Local::isArray() const { return isObject(); } bool Local::isByteBuffer() const { auto engine = lua_backend::currentEngine(); return engine->byteBufferDelegate_->isByteBuffer(engine, *this); } -bool Local::isObject() const { return getKind() == ValueKind::kObject; } +bool Local::isObject() const { + return val_ != 0 && lua_type(lua_backend::currentLua(), val_) == LUA_TTABLE; +} bool Local::isUnsupported() const { return getKind() == ValueKind::kUnsupported; } diff --git a/backend/V8/V8LocalReference.cc b/backend/V8/V8LocalReference.cc index 4c8f3b33..549a7663 100644 --- a/backend/V8/V8LocalReference.cc +++ b/backend/V8/V8LocalReference.cc @@ -26,43 +26,44 @@ namespace script { -#define REF_IMPL_BASIC_FUNC(ValueType) \ - Local::Local(const Local& copy) : val_(copy.val_) {} \ - Local::Local(Local&& from) noexcept : val_(from.val_) { \ - from.val_.Clear(); \ - } \ - Local::~Local() { \ - assert(val_.IsEmpty() || EngineScope::currentEngine() != nullptr); \ - } \ - Local& Local::operator=(const Local& from) { \ - if (&from != this) { \ - val_ = from.val_; \ - } \ - return *this; \ - } \ - Local& Local::operator=(Local&& move) noexcept { \ - if (&move != this) { \ - val_ = move.val_; \ - move.val_.Clear(); \ - } \ - return *this; \ - } \ - void Local::swap(Local& rhs) noexcept { \ - if (&rhs != this) { \ - std::swap(val_, rhs.val_); \ - } \ - } \ - bool Local::operator==(const script::Local& other) const { \ - return val_->StrictEquals(other.val_); \ +#define REF_IMPL_BASIC_FUNC(ValueType) \ + Local::Local(const Local& copy) : val_(copy.val_) {} \ + Local::Local(Local&& from) noexcept : val_(from.val_) { \ + from.val_.Clear(); \ + } \ + Local::~Local() { \ + assert(val_.IsEmpty() || EngineScope::currentEngine() != nullptr); \ + } \ + Local& Local::operator=(const Local& from) { \ + if (&from != this) { \ + val_ = from.val_; \ + } \ + return *this; \ + } \ + Local& Local::operator=(Local&& move) noexcept { \ + if (&move != this) { \ + val_ = move.val_; \ + move.val_.Clear(); \ + } \ + return *this; \ + } \ + void Local::swap(Local& rhs) noexcept { \ + if (&rhs != this) { \ + std::swap(val_, rhs.val_); \ + } \ } // if Local is created with null ref, it's an error -#define REF_IMPL_BASIC_NOT_VALUE(ValueType) \ - Local::Local(InternalLocalRef v8Local) : val_(v8Local) { \ - if (val_.IsEmpty() || val_->IsNullOrUndefined()) throw Exception("null reference"); \ - } \ - Local Local::describe() const { return asValue().describe(); } \ - std::string Local::describeUtf8() const { return asValue().describeUtf8(); } +#define REF_IMPL_BASIC_NOT_VALUE(ValueType) \ + Local::Local(InternalLocalRef v8Local) : val_(v8Local) { \ + if (val_.IsEmpty() || val_->IsNullOrUndefined()) throw Exception("null reference"); \ + } \ + Local Local::describe() const { return asValue().describe(); } \ + std::string Local::describeUtf8() const { return asValue().describeUtf8(); } \ + bool Local::operator==(const script::Local& other) const { \ + if (other.isNull()) return false; \ + return val_->StrictEquals(other.val_); \ + } #define REF_IMPL_TO_VALUE(ValueType) \ Local Local::asValue() const { return Local(val_.As()); } @@ -133,6 +134,11 @@ ValueKind Local::getKind() const { } } +bool Local::operator==(const script::Local& other) const { + if (isNull()) return other.isNull(); + return val_->StrictEquals(other.val_); +} + bool Local::isObject() const { return !isNull() && val_->IsObject(); } Local Local::asObject() const { diff --git a/backend/WebAssembly/WasmLocalReference.cc b/backend/WebAssembly/WasmLocalReference.cc index af472ae9..1a96fa92 100644 --- a/backend/WebAssembly/WasmLocalReference.cc +++ b/backend/WebAssembly/WasmLocalReference.cc @@ -199,6 +199,7 @@ Local Local::asUnsupported() const { } bool Local::operator==(const script::Local& other) const { + if (isNull()) return other.isNull(); return wasm_backend::Stack::equals(val_, other.val_); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0ca2cc43..b1888e75 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20) file(STRINGS ${CMAKE_CURRENT_LIST_DIR}/../VERSION SCRIPTX_VERSION) project(ScriptXUnitTests VERSION ${SCRIPTX_VERSION} LANGUAGES CXX) -option(DEVOPS_ENABLE_COVERAGE "enable coverage" OFF) +option(DEVOPS_ENABLE_COVERAGE "enable code coverage" OFF) enable_testing() add_executable(UnitTests) @@ -31,7 +31,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") endif () if (DEVOPS_ENABLE_COVERAGE) - # bk support gcov only + # devops support gcov only string(APPEND CMAKE_CXX_FLAGS_DEBUG " --coverage -g3") endif () endif () diff --git a/test/src/ByteBufferTest.cc b/test/src/ByteBufferTest.cc index 38ca7900..dde11757 100644 --- a/test/src/ByteBufferTest.cc +++ b/test/src/ByteBufferTest.cc @@ -44,30 +44,9 @@ TEST_F(ByteBufferTest, Type) { EXPECT_EQ(ByteBuffer::newByteBuffer(ptr, 8).byteLength(), 8); } -TEST_F(ByteBufferTest, Data) { - EngineScope engineScope(engine); - auto ret = engine->eval(TS().js(R"( - ab = new ArrayBuffer(8); - view = new Int8Array(ab); - view[0] = 1; - view[1] = 0; - view[2] = 2; - view[3] = 4; - ab; -)") - .lua(R"( -view = ByteBuffer(8) -view:writeInt8(1, 1) -view:writeInt8(2, 0) -view:writeInt8(3, 2) -view:writeInt8(4, 4) -return view - -)") - .select()); - - ASSERT_TRUE(ret.isByteBuffer()); - auto buffer = ret.asByteBuffer(); +void testByteBufferReadWrite(ScriptEngine* engine, const Local& buf) { + ASSERT_TRUE(buf.isByteBuffer()); + auto buffer = buf.asByteBuffer(); ASSERT_TRUE(buffer.byteLength() == 8); uint8_t* ptr = static_cast(buffer.getRawBytes()); ASSERT_TRUE(ptr != nullptr); @@ -92,8 +71,82 @@ return view:readInt8(5) == 2 and view:readInt8(6) == 0 and view:readInt8(7) == 4 .select()); ASSERT_TRUE(success.isBoolean()) << success.describeUtf8(); ASSERT_TRUE(success.asBoolean().value()); + +#ifdef SCRIPTX_BACKEND_LUA + // out of index + EXPECT_THROW({ engine->eval("view:readInt8(10)"); }, Exception); + EXPECT_THROW({ engine->eval("view:writeInt8(10, 0)"); }, Exception); + + // unaligned access + EXPECT_THROW({ engine->eval("view:readInt32(2)"); }, Exception); + EXPECT_THROW({ engine->eval("view:writeInt32(2, 0)"); }, Exception); + + // bad param + EXPECT_THROW({ engine->eval("ByteBuffer(-1)"); }, Exception); +#endif +} + +TEST_F(ByteBufferTest, Data) { + EngineScope engineScope(engine); + auto ret = engine->eval(TS().js(R"( + ab = new ArrayBuffer(8); + view = new Int8Array(ab); + view[0] = 1; + view[1] = 0; + view[2] = 2; + view[3] = 4; + ab; +)") + .lua(R"( +view = ByteBuffer(8) +view:writeInt8(1, 1) +view:writeInt8(2, 0) +view:writeInt8(3, 2) +view:writeInt8(4, 4) +return view + +)") + .select()); + testByteBufferReadWrite(engine, ret); } +#ifdef SCRIPTX_LANG_JAVASCRIPT + +TEST_F(ByteBufferTest, DataView) { + for (auto&& type : std::initializer_list>{ + {"Int8Array", ByteBuffer::Type::kInt8}, + {"Int16Array", ByteBuffer::Type::kInt16}, + {"Int32Array", ByteBuffer::Type::kInt32}, +#ifdef SCRIPTX_BACKEND_V8 + {"BigInt64Array", ByteBuffer::Type::kInt64}, +#endif + {"Float32Array", ByteBuffer::Type::KFloat32}, + {"Float64Array", ByteBuffer::Type::kFloat64}, + {"DataView", ByteBuffer::Type::kUnspecified}}) { + std::ostringstream code; + code << R"( + ab = new ArrayBuffer(8); + view = new Int8Array(ab); + view[0] = 1; + view[1] = 0; + view[2] = 2; + view[3] = 4; + )"; + code << "new " << type.first << "(ab);"; + + EngineScope engineScope(engine); + try { + auto ret = engine->eval(code.str()).asByteBuffer(); + EXPECT_EQ(ret.getType(), type.second); + testByteBufferReadWrite(engine, ret); + } catch (const Exception& e) { + FAIL() << e; + } + } +} + +#endif + TEST_F(ByteBufferTest, CreateShared) { EngineScope engineScope(engine); auto shared = std::shared_ptr(new uint8_t[8], std::default_delete()); diff --git a/test/src/ExceptionTest.cc b/test/src/ExceptionTest.cc index 206ddb51..5e8058b4 100644 --- a/test/src/ExceptionTest.cc +++ b/test/src/ExceptionTest.cc @@ -132,6 +132,41 @@ return exceptionStackTraceTest std::ostringstream() << Exception(); } +TEST_F(ExceptionTest, Cross) { + EngineScope engineScope(engine); + + Exception e("test"); + EXPECT_NE(e.message().find("test"), std::string::npos); + EXPECT_NE(std::string(e.what()).find("test"), std::string::npos); + + auto exception = e.exception(); + try { + EXPECT_FALSE(exception.isNull()); + auto throwIt = engine->eval(TS().js("function throwIt(e) { throw e; }; throwIt") + .lua("return function (e) error(e) end;") + .select()); + throwIt.asFunction().call({}, exception); + } catch (Exception& ex) { + EXPECT_NE(ex.message().find("test"), std::string::npos); +#ifdef SCRIPTX_LANG_JAVASCRIPT + EXPECT_TRUE(exception == ex.exception()); +#endif + +#ifdef SCRIPTX_LANG_LUA + auto exException = ex.exception(); + EXPECT_TRUE(exException.isObject()); + EXPECT_NE(exException.asObject().get("message").asString().toString().find( + exception.asString().toString()), + std::string::npos); +#endif + } + + { + Exception ex(String::newString("test")); + EXPECT_NE(ex.message().find("test"), std::string::npos); + } +} + #ifndef SCRIPTX_BACKEND_WEBASSEMBLY TEST_F(ExceptionTest, EngineScopeOnDestory) { bool executed = false; diff --git a/test/src/NativeTest.cc b/test/src/NativeTest.cc index 73485d17..73b212cf 100644 --- a/test/src/NativeTest.cc +++ b/test/src/NativeTest.cc @@ -966,6 +966,15 @@ class X { } // namespace +TEST_F(NativeTest, NativeFounction) { + EngineScope scope(engine); + auto func = Function::newFunction([](int) {}); + func.call({}, 0); + EXPECT_THROW({ func.call(); }, Exception); + EXPECT_THROW({ func.call({}, 1, 2); }, Exception); + EXPECT_THROW({ func.call({}, ""); }, Exception); +} + TEST_F(NativeTest, SelectOverloadedFunction) { auto o1 = script::selectOverloadedFunc(overload); auto o2 = script::selectOverloadedFunc(overload); diff --git a/test/src/ThreadPoolTest.cc b/test/src/ThreadPoolTest.cc index 10f68cd1..e804c36c 100644 --- a/test/src/ThreadPoolTest.cc +++ b/test/src/ThreadPoolTest.cc @@ -39,7 +39,8 @@ TEST(ThreadPool, Run) { tp.postMessage(msg); } - tp.shutdown(true); + tp.shutdown(false); + tp.awaitTermination(); EXPECT_EQ(max, i->load()); } @@ -49,7 +50,7 @@ TEST(ThreadPool, MultiThreadRun) { constexpr auto kProducerCount = 4; constexpr auto max = 1000; - ThreadPool tp(kWorkerCount, std::make_unique()); + ThreadPool tp(kWorkerCount); EXPECT_EQ(kWorkerCount, tp.workerCount()); auto i = std::make_unique(); diff --git a/test/src/UtilsTest.cc b/test/src/UtilsTest.cc index c675a19b..3029ee8d 100644 --- a/test/src/UtilsTest.cc +++ b/test/src/UtilsTest.cc @@ -35,6 +35,12 @@ TEST_F(UtilsTest, Log) { Logger::log("hello"); EXPECT_EQ(l.message, "hello"); + Logger::log(std::string("hello")); + EXPECT_EQ(l.message, "hello"); + + Logger::log(std::string_view("hello")); + EXPECT_EQ(l.message, "hello"); + Logger() << "hello " << 1; EXPECT_EQ(l.message, "hello 1"); diff --git a/test/src/ValueTest.cc b/test/src/ValueTest.cc index b6d5e807..4010e060 100644 --- a/test/src/ValueTest.cc +++ b/test/src/ValueTest.cc @@ -98,6 +98,7 @@ TEST_F(ValueTest, Object) { try { auto hello = String::newString("hello"); auto obj = Object::newObject(); + EXPECT_EQ(obj.asValue().getKind(), ValueKind::kObject); obj.set("hello", Number::newNumber(321)); EXPECT_TRUE(obj.has(hello)); @@ -166,9 +167,14 @@ TEST_F(ValueTest, String) { Local strVal = String::newString(string); EXPECT_FALSE(strVal.isNull()); EXPECT_TRUE(strVal.isString()); + EXPECT_EQ(strVal.getKind(), ValueKind::kString); auto str = String::newString(string); EXPECT_STREQ(string, str.toString().c_str()); +#ifdef __cpp_char8_t + EXPECT_EQ(u8"hello world", str.toU8string()); +#endif + EXPECT_STREQ(string, str.describeUtf8().c_str()); EXPECT_EQ(strVal, str); str = engine->eval(TS().js("'hello world'").lua("return 'hello world'").select()).asString(); @@ -494,6 +500,12 @@ TEST_F(ValueTest, ExceptionDeath) { TEST_F(ValueTest, Array) { EngineScope engineScope(engine); Local arr = Array::newArray(4); +#ifdef SCRIPTX_LANG_JAVASCRIPT + EXPECT_EQ(arr.asValue().getKind(), ValueKind::kArray); +#elif defined(SCRIPTX_LANG_LUA) + EXPECT_EQ(arr.asValue().getKind(), ValueKind::kObject); +#endif + // EXPECT_EQ(arr.size(), 4); EXPECT_TRUE(arr.get(0).isNull()); EXPECT_TRUE(arr.get(1).isNull()); @@ -527,15 +539,24 @@ TEST_F(ValueTest, Array) { EXPECT_TRUE(arr.get(2).isNull()); arr.set(3, 1); EXPECT_TRUE(arr.get(3).isNumber()); + + arr = Array::newArray(); + arr.add(Number::newNumber(42)); + EXPECT_EQ(arr.get(0).asNumber().toInt32(), 42); } TEST_F(ValueTest, Null) { EngineScope engineScope(engine); + EXPECT_EQ(Local().getKind(), ValueKind::kNull); + EXPECT_STREQ(Local().describeUtf8().c_str(), "null"); + #ifdef SCRIPTX_LANG_JAVASCRIPT // JS: null & undefined -> script::Null EXPECT_TRUE(engine->eval(String::newString(u8"null")).isNull()); + EXPECT_EQ(engine->eval(String::newString(u8"null")), Local()); EXPECT_TRUE(engine->eval(String::newString(u8"undefined")).isNull()); + EXPECT_EQ(engine->eval(String::newString(u8"undefined")), Local()); // script::Null -> JS: undefined auto key = String::newString(u8"ValueTest_Null_null"); @@ -546,6 +567,28 @@ TEST_F(ValueTest, Null) { #endif } +template +void testNumber(T value) { + auto num = Number::newNumber(value); + EXPECT_EQ(num.toInt32(), static_cast(value)); +#ifndef SCRIPTX_BACKEND_WEBASSEMBLY + EXPECT_EQ(num.toInt64(), static_cast(value)); +#endif + EXPECT_FLOAT_EQ(num.toFloat(), static_cast(value)); + EXPECT_FLOAT_EQ(num.toDouble(), static_cast(value)); +} + +TEST_F(ValueTest, Number) { + EngineScope engineScope(engine); + EXPECT_EQ(Number::newNumber(0).asValue().getKind(), ValueKind::kNumber); + EXPECT_EQ(Number::newNumber(42).describeUtf8(), "42"); + + testNumber(42); + testNumber(42); + testNumber(3.14); + testNumber(3.14); +} + TEST_F(ValueTest, Equals) { EngineScope engineScope(engine); auto n1 = Number::newNumber(1); @@ -556,6 +599,8 @@ TEST_F(ValueTest, Equals) { EXPECT_FALSE(n1 == n2); EXPECT_TRUE(n1 != n2); + EXPECT_TRUE(Local() == Local()); + auto s1 = String::newString("hello"); auto s2 = String::newString("hello"); auto s3 = String::newString("world"); @@ -564,4 +609,49 @@ TEST_F(ValueTest, Equals) { EXPECT_TRUE(s1 != s3); } +TEST_F(ValueTest, Kinds) { + auto test = [](const Local& v) { + ValueKind kind = v.getKind(); + if (kind == ValueKind::kNull) { + EXPECT_TRUE(v.isNull()); + } +#define CAST_TEST(TYPE) \ + if (kind != ValueKind::k##TYPE) { \ + EXPECT_THROW({ v.as##TYPE(); }, Exception); \ + } + CAST_TEST(String) + CAST_TEST(Number) + CAST_TEST(Boolean) + CAST_TEST(Function) + CAST_TEST(ByteBuffer) +#undef CAST_TEST + }; + + EngineScope engineScope(engine); + test({}); + test(String::newString("hello")); + test(Object::newObject()); + test(Number::newNumber(0)); + test(Boolean::newBoolean(false)); + test(Function::newFunction([]() {})); + test(Array::newArray()); + test(ByteBuffer::newByteBuffer(0)); +} + +TEST_F(ValueTest, KindNames) { + EXPECT_STREQ(valueKindName(ValueKind::kNull), "Null"); + EXPECT_STREQ(valueKindName(ValueKind::kString), "String"); + EXPECT_STREQ(valueKindName(ValueKind::kObject), "Object"); + EXPECT_STREQ(valueKindName(ValueKind::kNumber), "Number"); + EXPECT_STREQ(valueKindName(ValueKind::kBoolean), "Boolean"); + EXPECT_STREQ(valueKindName(ValueKind::kFunction), "Function"); + EXPECT_STREQ(valueKindName(ValueKind::kArray), "Array"); + EXPECT_STREQ(valueKindName(ValueKind::kByteBuffer), "ByteBuffer"); + EXPECT_STREQ(valueKindName(ValueKind::kUnsupported), "Unsupported"); + + std::ostringstream oss; + oss << ValueKind::kNull; + EXPECT_EQ(oss.str(), "Null"); +} + } // namespace script::test \ No newline at end of file