@@ -25,6 +25,26 @@ T identityCFunction(T value)
2525{
2626 return value;
2727}
28+
29+ int testGetter777 (lua_State* L)
30+ {
31+ lua_pushinteger (L, 777 );
32+ return 1 ;
33+ }
34+
35+ int testSetValueWithSelf (lua_State* L)
36+ {
37+ lua_pushvalue (L, 2 );
38+ lua_setglobal (L, " captured" );
39+ return 0 ;
40+ }
41+
42+ int testSetValueStatic (lua_State* L)
43+ {
44+ lua_pushvalue (L, 1 );
45+ lua_setglobal (L, " captured" );
46+ return 0 ;
47+ }
2848} // namespace
2949
3050struct LuaBridgeTest : TestBase
@@ -517,6 +537,173 @@ TEST_F(LuaBridgeTest, CallWithHandlerTypedReturnAndStackRestore)
517537 }
518538}
519539
540+ TEST_F (LuaBridgeTest, IndexMetamethodSimple_ObjectFallbackBranches)
541+ {
542+ // Exercise the table-based fallback branch of index_metamethod_simple<true>.
543+ auto callIndex = [this ](const char * key) -> bool
544+ {
545+ lua_newtable (L); // self
546+
547+ lua_newtable (L); // mt
548+ lua_newtable (L); // propget
549+ luabridge::lua_pushcfunction_x (L, &testGetter777, " testGetter777" );
550+ lua_setfield (L, -2 , " viaGetter" );
551+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropgetKey ());
552+ lua_setmetatable (L, -2 );
553+
554+ lua_newtable (L); // upvalue #1 (pg)
555+ lua_newtable (L); // upvalue #2 (mt)
556+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::index_metamethod_simple<true >, " index_metamethod_simple_object" , 2 );
557+
558+ lua_pushvalue (L, -2 ); // self
559+ lua_pushstring (L, key);
560+ const int code = lua_pcall (L, 2 , 1 , 0 );
561+
562+ lua_remove (L, -2 ); // remove self, keep result or error
563+ return code == LUABRIDGE_LUA_OK;
564+ };
565+
566+ // 1) Value found in self table branch.
567+ auto tmpSelf = luabridge::newTable (L);
568+ tmpSelf[" selfField" ] = 55 ;
569+ luabridge::setGlobal (L, tmpSelf, " tmpSelf" );
570+
571+ lua_getglobal (L, " tmpSelf" ); // self
572+ lua_newtable (L); // mt
573+ lua_newtable (L); // propget
574+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropgetKey ());
575+ lua_setmetatable (L, -2 );
576+ lua_newtable (L);
577+ lua_newtable (L);
578+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::index_metamethod_simple<true >, " index_metamethod_simple_object" , 2 );
579+ lua_pushvalue (L, -2 );
580+ lua_pushstring (L, " selfField" );
581+ ASSERT_EQ (LUABRIDGE_LUA_OK, lua_pcall (L, 2 , 1 , 0 ));
582+ ASSERT_TRUE (lua_isnumber (L, -1 ));
583+ ASSERT_EQ (55 , static_cast <int >(lua_tointeger (L, -1 )));
584+ lua_pop (L, 2 );
585+
586+ // 2) Value found in metatable branch.
587+ lua_newtable (L); // self
588+ lua_newtable (L); // mt
589+ lua_pushinteger (L, 66 );
590+ lua_setfield (L, -2 , " metaField" );
591+ lua_newtable (L);
592+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropgetKey ());
593+ lua_setmetatable (L, -2 );
594+ lua_newtable (L);
595+ lua_newtable (L);
596+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::index_metamethod_simple<true >, " index_metamethod_simple_object" , 2 );
597+ lua_pushvalue (L, -2 );
598+ lua_pushstring (L, " metaField" );
599+ ASSERT_EQ (LUABRIDGE_LUA_OK, lua_pcall (L, 2 , 1 , 0 ));
600+ ASSERT_TRUE (lua_isnumber (L, -1 ));
601+ ASSERT_EQ (66 , static_cast <int >(lua_tointeger (L, -1 )));
602+ lua_pop (L, 2 );
603+
604+ // 3) Value resolved via propget callable branch.
605+ ASSERT_TRUE (callIndex (" viaGetter" ));
606+ ASSERT_TRUE (lua_isnumber (L, -1 ));
607+ ASSERT_EQ (777 , static_cast <int >(lua_tointeger (L, -1 )));
608+ lua_pop (L, 1 );
609+
610+ // 4) Unknown key returns nil at final fallback.
611+ ASSERT_TRUE (callIndex (" doesNotExist" ));
612+ ASSERT_TRUE (lua_isnil (L, -1 ));
613+ lua_pop (L, 1 );
614+ }
615+
616+ TEST_F (LuaBridgeTest, NewIndexMetamethodSimple_FallbackBranches)
617+ {
618+ // Object variant fallback path (self is table).
619+ {
620+ lua_newtable (L); // self
621+ lua_newtable (L); // mt
622+ lua_newtable (L); // propset
623+ luabridge::lua_pushcfunction_x (L, &testSetValueWithSelf, " testSetValueWithSelf" );
624+ lua_setfield (L, -2 , " x" );
625+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropsetKey ());
626+ lua_setmetatable (L, -2 );
627+
628+ lua_newtable (L); // upvalue #1
629+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::newindex_metamethod_simple<true >, " newindex_metamethod_simple_object" , 1 );
630+ lua_pushvalue (L, -2 );
631+ lua_pushstring (L, " x" );
632+ lua_pushinteger (L, 123 );
633+ ASSERT_EQ (LUABRIDGE_LUA_OK, lua_pcall (L, 3 , 0 , 0 ));
634+ lua_pop (L, 1 );
635+
636+ auto captured = luabridge::getGlobal (L, " captured" );
637+ ASSERT_TRUE (captured.isNumber ());
638+ ASSERT_EQ (123 , captured.unsafe_cast <int >());
639+ }
640+
641+ // Object variant fallback error when propset table exists but key is missing.
642+ {
643+ lua_newtable (L); // self
644+ lua_newtable (L); // mt
645+ lua_newtable (L); // propset
646+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropsetKey ());
647+ lua_setmetatable (L, -2 );
648+
649+ lua_newtable (L); // upvalue #1
650+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::newindex_metamethod_simple<true >, " newindex_metamethod_simple_object" , 1 );
651+ lua_pushvalue (L, -2 );
652+ lua_pushstring (L, " missing" );
653+ lua_pushinteger (L, 1 );
654+ ASSERT_NE (LUABRIDGE_LUA_OK, lua_pcall (L, 3 , 0 , 0 ));
655+ auto err = lua_tostring (L, -1 );
656+ ASSERT_NE (nullptr , err);
657+ EXPECT_TRUE (std::string (err).find (" no writable member 'missing'" ) != std::string::npos);
658+ lua_pop (L, 2 );
659+ }
660+
661+ // Static variant fallback path (self is userdata to bypass simple table fast path).
662+ {
663+ lua_newuserdata (L, 1 ); // self
664+ lua_newtable (L); // mt
665+ lua_newtable (L); // propset
666+ luabridge::lua_pushcfunction_x (L, &testSetValueStatic, " testSetValueStatic" );
667+ lua_setfield (L, -2 , " x" );
668+ luabridge::lua_rawsetp_x (L, -2 , luabridge::detail::getPropsetKey ());
669+ lua_setmetatable (L, -2 );
670+
671+ lua_newtable (L); // upvalue #1
672+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::newindex_metamethod_simple<false >, " newindex_metamethod_simple_static" , 1 );
673+ lua_pushvalue (L, -2 );
674+ lua_pushstring (L, " x" );
675+ lua_pushinteger (L, 321 );
676+ ASSERT_EQ (LUABRIDGE_LUA_OK, lua_pcall (L, 3 , 0 , 0 ));
677+ lua_pop (L, 1 );
678+
679+ auto captured = luabridge::getGlobal (L, " captured" );
680+ ASSERT_TRUE (captured.isNumber ());
681+ ASSERT_EQ (321 , captured.unsafe_cast <int >());
682+ }
683+ }
684+
685+ TEST_F (LuaBridgeTest, ReadOnlyErrorAndArgumentDecodeRaiseLuaError)
686+ {
687+ // Explicitly exercise read_only_error -> raise_lua_error path.
688+ lua_pushstring (L, " locked" );
689+ luabridge::lua_pushcclosure_x (L, &luabridge::detail::read_only_error, " read_only_error" , 1 );
690+ ASSERT_NE (LUABRIDGE_LUA_OK, lua_pcall (L, 0 , 0 , 0 ));
691+ {
692+ auto err = lua_tostring (L, -1 );
693+ ASSERT_NE (nullptr , err);
694+ EXPECT_TRUE (std::string (err).find (" 'locked' is read-only" ) != std::string::npos);
695+ }
696+ lua_pop (L, 1 );
697+
698+ // Exercise argument decode error path in invocation wrappers (raise_lua_error at decode).
699+ luabridge::getGlobalNamespace (L)
700+ .addFunction (" expectInt" , +[](int ) { return 0 ; });
701+
702+ auto [ok, err] = runLuaCaptureError (" expectInt('not an int')" );
703+ EXPECT_FALSE (ok);
704+ EXPECT_TRUE (err.find (" Error decoding argument #1" ) != std::string::npos);
705+ }
706+
520707TEST_F (LuaBridgeTest, InvokePassingUnregisteredClassShouldThrowAndRestoreStack)
521708{
522709 class Unregistered {} unregistered;
0 commit comments