├── .gitignore ├── examples └── love2d │ ├── main.lua │ └── core.lua ├── test-suite ├── tests │ ├── data_change │ │ ├── test4.lua │ │ ├── test3.lua │ │ ├── test1.lua │ │ ├── test2.lua │ │ ├── test5.lua │ │ ├── test9.lua │ │ ├── test10.lua │ │ ├── test8.lua │ │ ├── test6.lua │ │ └── test7.lua │ ├── new_data │ │ ├── test1.lua │ │ ├── test2.lua │ │ ├── test5.lua │ │ ├── test3.lua │ │ ├── test6.lua │ │ ├── test4.lua │ │ └── test7.lua │ ├── test13.lua │ ├── syntax_error │ │ ├── test1.lua │ │ ├── test3.lua │ │ ├── test2.lua │ │ └── test4.lua │ ├── test1.lua │ ├── getinfo │ │ ├── test4.lua │ │ ├── test5.lua │ │ ├── test2.lua │ │ ├── test3.lua │ │ └── test1.lua │ ├── same_file_multi_loads │ │ └── test1.lua │ ├── test2.lua │ ├── multiple_return_values │ │ ├── test2.lua │ │ ├── test3.lua │ │ └── test1.lua │ ├── timestamp_and_cache │ │ ├── test1.lua │ │ ├── test3.lua │ │ └── test2.lua │ ├── nested_loads │ │ ├── test1.lua │ │ └── test2.lua │ ├── test15.lua │ ├── test7.lua │ ├── anon │ │ ├── test2.lua │ │ ├── test3.lua │ │ ├── test1.lua │ │ ├── test4.lua │ │ └── test5.lua │ ├── test11.lua │ ├── test3.lua │ ├── test12.lua │ ├── test4.lua │ ├── global_state │ │ ├── test2.lua │ │ ├── test6.lua │ │ ├── test10.lua │ │ ├── test3.lua │ │ ├── test1.lua │ │ ├── test8.lua │ │ ├── test9.lua │ │ ├── test4.lua │ │ ├── test5.lua │ │ └── test7.lua │ ├── test8.lua │ ├── test16.lua │ ├── test5.lua │ ├── env │ │ └── test1.lua │ ├── test6.lua │ ├── several_routes │ │ └── test1.lua │ ├── test17.lua │ ├── test9.lua │ ├── test10.lua │ └── test14.lua ├── utils.lua ├── test_setup.lua └── run_tests.lua ├── .travis.yml ├── LICENSE ├── README.md └── lua_reload.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | testfile*.lua 3 | log.txt 4 | -------------------------------------------------------------------------------- /examples/love2d/main.lua: -------------------------------------------------------------------------------- 1 | LuaReload = dofile("../../lua_reload.lua") 2 | LuaReload.Inject() 3 | LuaReload.SetPrintReloadingLogs(false) 4 | 5 | dofile("core.lua") 6 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { 5 | data = false 6 | } 7 | ]=] 8 | 9 | local t = DoFileString(file1) 10 | assert(t.data == false) 11 | 12 | t.data = true 13 | 14 | ReloadFileString(file1) 15 | 16 | assert(t.data == true) 17 | -------------------------------------------------------------------------------- /examples/love2d/core.lua: -------------------------------------------------------------------------------- 1 | function love.load() 2 | love.graphics.setBackgroundColor(0.1, 0.1, 0.1, 1) 3 | end 4 | 5 | function love.update(dt) 6 | LuaReload.Monitor() 7 | end 8 | 9 | function love.draw() 10 | love.graphics.setColor(0.8, 0.8, 0.8) 11 | love.graphics.print("Welcome", 32, 32) 12 | end 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | - LUA="lua 5.2" 6 | - LUA="lua 5.3" 7 | - LUA="luajit 2.0" 8 | - LUA="luajit 2.1" 9 | 10 | before_install: 11 | - pip install hererocks 12 | - hererocks lua_install --$LUA -r latest 13 | - source lua_install/bin/activate 14 | 15 | install: 16 | - luarocks install luafilesystem 17 | 18 | script: 19 | - cd test-suite && lua run_tests.lua 20 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = {} 5 | function t.Func() 6 | end 7 | return t 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local t = { 12 | data = 10 13 | } 14 | function t.Func() 15 | end 16 | return t 17 | ]=] 18 | 19 | local t = DoFileString(file1) 20 | 21 | assert(t.data == nil) 22 | 23 | ReloadFileString(file2) 24 | 25 | assert(t.data == 10) 26 | -------------------------------------------------------------------------------- /test-suite/tests/test13.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { value = 5 } 5 | ]=] 6 | 7 | local file2 = [=[ 8 | return { value = 10 } 9 | ]=] 10 | 11 | local func = DoFileString(file1) 12 | 13 | func = nil 14 | collectgarbage() 15 | 16 | local cache = LuaReload.GetFileCache() 17 | local file = cache[ GetTestFilename() ] 18 | for k, v in pairs(file.returnValues) do 19 | error("return values are still present!") 20 | end 21 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { 5 | data = false 6 | } 7 | ]=] 8 | 9 | local file2 = [=[ 10 | return { 11 | data = true 12 | } 13 | ]=] 14 | 15 | local t = DoFileString(file1) 16 | assert(t.data == false) 17 | 18 | ReloadFileString(file2) 19 | assert(t.data == true) 20 | 21 | ReloadFileString(file1) 22 | assert(t.data == false) 23 | 24 | ReloadFileString(file2) 25 | assert(t.data == true) 26 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local data = 1 5 | return function() 6 | return data 7 | end 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local data = 2 12 | return function() 13 | return data 14 | end 15 | ]=] 16 | 17 | local func = DoFileString(file1) 18 | assert(func() == 1) 19 | 20 | ReloadFileString(file2) 21 | assert(func() == 2) 22 | 23 | ReloadFileString(file1) 24 | assert(func() == 1) 25 | 26 | ReloadFileString(file2) 27 | assert(func() == 2) 28 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local data = false 5 | return function() 6 | return data 7 | end 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local data = true 12 | return function() 13 | return data 14 | end 15 | ]=] 16 | 17 | local func = DoFileString(file1) 18 | assert(func() == false) 19 | 20 | ReloadFileString(file2) 21 | assert(func() == true) 22 | 23 | ReloadFileString(file1) 24 | assert(func() == false) 25 | 26 | ReloadFileString(file2) 27 | assert(func() == true) 28 | -------------------------------------------------------------------------------- /test-suite/tests/syntax_error/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return function() 5 | return 5 6 | end 7 | ]=] 8 | 9 | local file2 = [=[ 10 | return function() 11 | syntax error is here!!! 12 | end 13 | ]=] 14 | 15 | local func = DoFileString(file1) 16 | 17 | assert(func() == 5) 18 | 19 | WriteFileString(file2) 20 | 21 | print("BELOW ERROR is expected:") 22 | local file, err = loadfile(GetTestFilename()) 23 | 24 | assert(file) 25 | assert(err == nil) 26 | assert(func() == 5) 27 | assert(file()() == 5) 28 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { 5 | data = "original" 6 | } 7 | ]=] 8 | 9 | local file2 = [=[ 10 | return { 11 | data = "new" 12 | } 13 | ]=] 14 | 15 | local file3 = [=[ 16 | return { 17 | data = "new2" 18 | } 19 | ]=] 20 | 21 | local t = DoFileString(file1) 22 | assert(t.data == "original") 23 | 24 | t.data = "current" 25 | ReloadFileString(file2) 26 | assert(t.data == "current") 27 | 28 | t.data = "new" 29 | ReloadFileString(file3) 30 | assert(t.data == "new2") 31 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test9.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { 5 | data = false 6 | } 7 | ]=] 8 | 9 | local file2 = [=[ 10 | return { 11 | data = true, 12 | func = function() end 13 | } 14 | ]=] 15 | 16 | local t = DoFileString(file1) 17 | assert(t.data == false) 18 | 19 | ReloadFileString(file2) 20 | assert(t.data == true) 21 | assert(t.func) 22 | 23 | ReloadFileString(file1) 24 | assert(t.data == false) 25 | assert(t.func == nil) 26 | 27 | ReloadFileString(file2) 28 | assert(t.data == true) 29 | assert(t.func) 30 | -------------------------------------------------------------------------------- /test-suite/tests/syntax_error/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | LuaReload.SetUseOldFileOnError(false) 3 | 4 | local file1 = [=[ 5 | return function() 6 | return 5 7 | end 8 | ]=] 9 | 10 | local file2 = [=[ 11 | return function() 12 | syntax error is here!!! 13 | end 14 | ]=] 15 | 16 | local func = DoFileString(file1) 17 | 18 | assert(func() == 5) 19 | 20 | WriteFileString(file2) 21 | 22 | print("BELOW ERROR is expected:") 23 | local status, err = pcall(dofile, GetTestFilename()) 24 | 25 | assert(status == false) 26 | assert(err) 27 | print("BELOW ERROR is expected:") 28 | print(err) 29 | -------------------------------------------------------------------------------- /test-suite/tests/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return function() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | ]=] 9 | 10 | local file2 = [=[ 11 | return function() 12 | log("> Hello, I'm a new version of the function, and I return 10!") 13 | return 10 14 | end 15 | ]=] 16 | 17 | local func = DoFileString(file1) 18 | local info = debug.getinfo(func, "S") 19 | log("HELLO", info.short_src) 20 | 21 | assert(func() == 5) 22 | 23 | ReloadFileString(file2) 24 | 25 | assert(func() == 10) 26 | -------------------------------------------------------------------------------- /test-suite/tests/getinfo/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function f() 5 | return 10 6 | end 7 | return f 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local iter = 0 12 | local function f() 13 | iter = iter + 1 14 | return iter 15 | end 16 | return f 17 | ]=] 18 | 19 | local f1 = DoFileString(file1) 20 | local f2 = DoFileString(file1) 21 | 22 | assert(f1() == 10) 23 | assert(f2() == 10) 24 | 25 | ReloadFileString(file2) 26 | 27 | assert(f1() == 1) 28 | assert(f1() == 2) 29 | assert(f1() == 3) 30 | 31 | assert(f2() == 1) 32 | assert(f2() == 2) 33 | -------------------------------------------------------------------------------- /test-suite/tests/syntax_error/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | LuaReload.SetUseOldFileOnError(false) 3 | 4 | local file1 = [=[ 5 | return function() 6 | return 5 7 | end 8 | ]=] 9 | 10 | local file2 = [=[ 11 | return function() 12 | syntax error is here!!! 13 | end 14 | ]=] 15 | 16 | local func = DoFileString(file1) 17 | 18 | assert(func() == 5) 19 | 20 | WriteFileString(file2) 21 | 22 | print("BELOW ERROR is expected:") 23 | local file, err = loadfile(GetTestFilename()) 24 | print("BELOW ERROR is expected:") 25 | print(err) 26 | 27 | assert(file == nil) 28 | assert(err) 29 | assert(func() == 5) 30 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | a = 100 6 | } 7 | function t.Func() 8 | return t.a 9 | end 10 | return t 11 | ]=] 12 | 13 | local file2 = [=[ 14 | local t = { 15 | a = 100 16 | } 17 | function t.Func() 18 | return t.a 19 | end 20 | return t 21 | ]=] 22 | 23 | local t = DoFileString(file1) 24 | 25 | assert(t.Func() == 100) 26 | t.a = 200 27 | assert(t.Func() == 200) 28 | 29 | ReloadFileString(file2) 30 | 31 | assert(t.Func() == 200) 32 | 33 | ReloadFileString(file2) 34 | 35 | assert(t.Func() == 200) 36 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | a = 10, 6 | b = 20 7 | } 8 | function t.Func() 9 | return t.a 10 | end 11 | return t 12 | ]=] 13 | 14 | local file2 = [=[ 15 | local t = { 16 | a = 100, 17 | b = 200 18 | } 19 | function t.Func() 20 | return t.b 21 | end 22 | return t 23 | ]=] 24 | 25 | local obj = DoFileString(file1) 26 | 27 | assert(obj.Func() == 10) 28 | 29 | obj.a = 1 30 | obj.b = 2 31 | 32 | assert(obj.Func() == 1) 33 | 34 | ReloadFileString(file2) 35 | 36 | assert(obj.Func() == 2) 37 | -------------------------------------------------------------------------------- /test-suite/tests/getinfo/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function f() 5 | return 10 6 | end 7 | return { f = f } 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local iter = 0 12 | local function f() 13 | iter = iter + 1 14 | return iter 15 | end 16 | return { f = f } 17 | ]=] 18 | 19 | local t1 = DoFileString(file1) 20 | local t2 = DoFileString(file1) 21 | 22 | assert(t1.f() == 10) 23 | assert(t2.f() == 10) 24 | 25 | ReloadFileString(file2) 26 | 27 | assert(t1.f() == 1) 28 | assert(t1.f() == 2) 29 | assert(t1.f() == 3) 30 | 31 | assert(t2.f() == 1) 32 | assert(t2.f() == 2) 33 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | } 6 | function t.Func() 7 | return 0 8 | end 9 | return t 10 | ]=] 11 | 12 | local file2 = [=[ 13 | local t = { 14 | a = 100 15 | } 16 | function t.Func() 17 | return 0 18 | end 19 | function t.GetA() 20 | return t.a 21 | end 22 | return t 23 | ]=] 24 | 25 | local t = DoFileString(file1) 26 | 27 | assert(t.Func() == 0) 28 | 29 | ReloadFileString(file2) 30 | 31 | assert(t.Func() == 0) 32 | assert(t.GetA() == 100) 33 | 34 | assert(t.a == 100) 35 | 36 | t.a = 200 37 | 38 | assert(t.GetA() == 200) 39 | -------------------------------------------------------------------------------- /test-suite/tests/same_file_multi_loads/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local iter = 0 5 | return function() 6 | iter = iter + 1 7 | return iter 8 | end 9 | ]=] 10 | 11 | local file2 = [=[ 12 | local iter = 0 13 | return function() 14 | iter = iter + 2 15 | return iter 16 | end 17 | ]=] 18 | 19 | local func1 = DoFileString(file1) 20 | local func2 = DoFileString(file1) 21 | 22 | assert(func1() == 1) 23 | assert(func1() == 2) 24 | assert(func1() == 3) 25 | 26 | assert(func2() == 1) 27 | assert(func2() == 2) 28 | 29 | ReloadFileString(file2) 30 | 31 | assert(func1() == 5) 32 | assert(func2() == 4) 33 | -------------------------------------------------------------------------------- /test-suite/tests/syntax_error/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return function() 5 | return 5 6 | end 7 | ]=] 8 | 9 | local file2 = [=[ 10 | return function() 11 | syntax error is here!!! 12 | end 13 | ]=] 14 | 15 | local func = DoFileString(file1) 16 | 17 | assert(func() == 5) 18 | 19 | WriteFileString(file2) 20 | 21 | LuaReload.SetErrorHandler(function(fileName, errorMessage, isReloading) 22 | error("ERROR catched: " .. errorMessage) 23 | end) 24 | 25 | local status, err = pcall(loadfile, GetTestFilename()) 26 | 27 | assert(status == false) 28 | assert(err) 29 | print("BELOW ERROR is expected:") 30 | print(err) 31 | -------------------------------------------------------------------------------- /test-suite/tests/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | return { 9 | func = localFunc 10 | } 11 | ]=] 12 | 13 | local file2 = [=[ 14 | local function localFunc() 15 | log("> Hello, I'm a new version of the function, and I return 10!") 16 | return 10 17 | end 18 | return { 19 | func = localFunc 20 | } 21 | ]=] 22 | 23 | local obj = DoFileString(file1) 24 | 25 | assert(obj.func() == 5) 26 | 27 | ReloadFileString(file2) 28 | 29 | assert(obj.func() == 10) 30 | -------------------------------------------------------------------------------- /test-suite/tests/multiple_return_values/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function f() 5 | log("> Hello, I'm the original version of the function1, and I return 5!") 6 | return 5 7 | end 8 | return "hi", nil, { 9 | f = f 10 | } 11 | ]=] 12 | 13 | local file2 = [=[ 14 | local function f() 15 | log("> Hello, I'm a new version of the function1, and I return 10!") 16 | return 10 17 | end 18 | return "hi", nil, { 19 | f = f 20 | } 21 | ]=] 22 | 23 | local a, b, c = DoFileString(file1) 24 | 25 | assert(c.f() == 5) 26 | 27 | ReloadFileString(file2) 28 | 29 | assert(c.f() == 10) 30 | -------------------------------------------------------------------------------- /test-suite/tests/timestamp_and_cache/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | -- assume we don't have get_timestamp functionality 4 | LuaReload.FileGetTimestamp = function(_) 5 | return 0 6 | end 7 | 8 | local file1 = [=[ 9 | return function() 10 | return 1 11 | end 12 | ]=] 13 | 14 | local file2 = [=[ 15 | return function() 16 | return 2 17 | end 18 | ]=] 19 | 20 | local func = DoFileString(file1) 21 | assert(func() == 1) 22 | 23 | ReloadFileString(file2) 24 | assert(func() == 1) 25 | 26 | LuaReload.SetEnableTimestampCheck(false) 27 | 28 | ReloadFileString(file2) 29 | assert(func() == 2) 30 | 31 | ReloadFileString(file1) 32 | assert(func() == 1) 33 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test10.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | data = 1 6 | } 7 | return t 8 | ]=] 9 | 10 | local file2 = [=[ 11 | local t = { 12 | data = 2, 13 | func = function() end 14 | } 15 | return t 16 | ]=] 17 | 18 | local file3 = [=[ 19 | local t = { 20 | data = 3, 21 | } 22 | t.func = function() return t.data end 23 | return t 24 | ]=] 25 | 26 | local t = DoFileString(file1) 27 | assert(t.data == 1) 28 | 29 | ReloadFileString(file2) 30 | assert(t.data == 2) 31 | assert(t.func) 32 | 33 | t.data = 100 34 | 35 | ReloadFileString(file3) 36 | assert(t.data == 100) 37 | assert(t.func() == 100) 38 | -------------------------------------------------------------------------------- /test-suite/tests/multiple_return_values/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function f() 5 | log("> Hello, I'm the original version of the function1, and I return 5!") 6 | return 5 7 | end 8 | return "hi", f, { 9 | f = f 10 | } 11 | ]=] 12 | 13 | local file2 = [=[ 14 | local function f() 15 | log("> Hello, I'm a new version of the function1, and I return 10!") 16 | return 10 17 | end 18 | return "hi", nil, { 19 | f = f 20 | } 21 | ]=] 22 | 23 | local a, b, c = DoFileString(file1) 24 | 25 | assert(b() == 5) 26 | assert(c.f() == 5) 27 | 28 | ReloadFileString(file2) 29 | 30 | assert(b() == 10) 31 | assert(c.f() == 10) 32 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test6.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | a = 10, 6 | b = 20 7 | } 8 | local m = {} 9 | function t.Func() 10 | return t.a 11 | end 12 | return t, m 13 | ]=] 14 | 15 | local file2 = [=[ 16 | local t = { 17 | a = 100, 18 | b = 200 19 | } 20 | local m = { 21 | v = 300 22 | } 23 | function t.Func() 24 | return m.v 25 | end 26 | return t, m 27 | ]=] 28 | 29 | local obj, m = DoFileString(file1) 30 | 31 | assert(obj.Func() == 10) 32 | 33 | ReloadFileString(file2) 34 | 35 | assert(obj.a == 100) 36 | assert(obj.b == 200) 37 | assert(m.v == 300) 38 | 39 | assert(obj.Func() == 300) 40 | -------------------------------------------------------------------------------- /test-suite/tests/nested_loads/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local fileNested = [=[ 4 | local fn = {} 5 | local base = 100 6 | function fn:Util() 7 | return base 8 | end 9 | return fn 10 | ]=] 11 | 12 | WriteFileString(fileNested, "nested") 13 | 14 | local file1 = [=[ 15 | local utils = dofile(GetTestFilename("nested")) 16 | return function() 17 | return utils:Util() + 1 18 | end 19 | ]=] 20 | 21 | local file2 = [=[ 22 | local utils = dofile(GetTestFilename("nested")) 23 | return function() 24 | return utils:Util() + 2 25 | end 26 | ]=] 27 | 28 | local func = DoFileString(file1) 29 | 30 | assert(func() == 101) 31 | 32 | ReloadFileString(file2) 33 | 34 | assert(func() == 102) 35 | -------------------------------------------------------------------------------- /test-suite/tests/test15.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local fn = {} 5 | function fn:Func() 6 | self.a = self.a or 10 7 | return self.a 8 | end 9 | local self = 100 10 | fn.Hello = function() 11 | return self 12 | end 13 | return fn 14 | ]=] 15 | 16 | local file2 = [=[ 17 | local fn = {} 18 | function fn:Func() 19 | self.a = self.a or 100 20 | return self.a + 1 21 | end 22 | local self = 200 23 | fn.Hello = function() 24 | return self 25 | end 26 | return fn 27 | ]=] 28 | 29 | local obj = DoFileString(file1) 30 | assert(obj:Func() == 10) 31 | assert(obj.Hello() == 100) 32 | ReloadFileString(file2) 33 | assert(obj:Func() == 11) 34 | assert(obj.Hello() == 200) 35 | -------------------------------------------------------------------------------- /test-suite/tests/test7.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local calls = 0 5 | return function() 6 | calls = calls + 1 7 | log("> Hello, you've called this function", calls, "times") 8 | return calls 9 | end 10 | ]=] 11 | 12 | local file2 = [=[ 13 | local calls = 0 14 | return function() 15 | calls = calls + 1 16 | log("> Hello, you've called this function", calls, "times, and I'm the new version of it") 17 | return calls, true 18 | end 19 | ]=] 20 | 21 | local func = DoFileString(file1) 22 | 23 | assert(func() == 1) 24 | assert(func() == 2) 25 | assert(select(2, func()) == nil) 26 | 27 | ReloadFileString(file2) 28 | 29 | assert(func() == 4) 30 | assert(func() == 5) 31 | assert(select(2, func()) == true) 32 | -------------------------------------------------------------------------------- /test-suite/tests/timestamp_and_cache/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | -- assume we don't have get_timestamp functionality 4 | LuaReload.FileGetTimestamp = function(_) 5 | return 0 6 | end 7 | 8 | local file1 = [=[ 9 | return function() 10 | return 1 11 | end 12 | ]=] 13 | 14 | local file2 = [=[ 15 | return function() 16 | return 2 17 | end 18 | ]=] 19 | 20 | local func = DoFileString(file1) 21 | assert(func() == 1) 22 | 23 | -- TODO remove when vanilla Lua doesn't load new version of files 24 | -- by default (see loadFileNew) 25 | local isVanillaLua = type(jit) ~= "table" 26 | 27 | local func = DoFileString(file2) 28 | assert(isVanillaLua or func() == 1) 29 | 30 | LuaReload.SetUseCache(false) 31 | 32 | local func = DoFileString(file2) 33 | assert(func() == 2) 34 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test8.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | local data = "original" 6 | function module.MakeAnon() 7 | return function() 8 | return data 9 | end 10 | end 11 | return module 12 | ]=] 13 | 14 | local file2 = [=[ 15 | local module = {} 16 | local data = "new" 17 | function module.MakeAnon() 18 | return function() 19 | return data 20 | end 21 | end 22 | return module 23 | ]=] 24 | 25 | local module = DoFileString(file1) 26 | local anon = module.MakeAnon() 27 | assert(anon() == "original") 28 | 29 | module = nil 30 | collectgarbage() 31 | collectgarbage() 32 | assert(anon() == "original") 33 | 34 | ReloadFileString(file2) 35 | assert(anon() == "new") 36 | -------------------------------------------------------------------------------- /test-suite/tests/anon/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | function module.Func() 6 | return 100 7 | end 8 | function module.GetAnon() 9 | return function() 10 | return module.Func() 11 | end 12 | end 13 | return module 14 | ]=] 15 | 16 | local file2 = [=[ 17 | local module = {} 18 | function module.Func() 19 | return 200 20 | end 21 | function module.GetAnon() 22 | return function() 23 | return module.Func() 24 | end 25 | end 26 | return module 27 | ]=] 28 | 29 | Module = DoFileString(file1) 30 | func = Module.GetAnon() 31 | 32 | assert(func() == 100) 33 | 34 | collectgarbage() 35 | collectgarbage() 36 | 37 | ReloadFileString(file2) 38 | 39 | assert(func() == 200) 40 | -------------------------------------------------------------------------------- /test-suite/tests/test11.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | 6 | local module2 = {} 7 | function module2:Func() 8 | self.a = 20 9 | end 10 | 11 | function module:Func() 12 | module2:Func() 13 | self.a = 10 14 | return function() 15 | self.a = 10 16 | end 17 | end 18 | 19 | return module 20 | ]=] 21 | 22 | local file2 = [=[ 23 | local module = {} 24 | 25 | local module2 = {} 26 | function module2:Func() 27 | end 28 | 29 | function module:Func() 30 | module2:Func() 31 | end 32 | 33 | return module 34 | ]=] 35 | 36 | local obj = DoFileString(file1) 37 | 38 | local a = obj:Func() 39 | 40 | --assert(obj.func() == 5) 41 | 42 | ReloadFileString(file2) 43 | 44 | --assert(obj.func() == 10) 45 | -------------------------------------------------------------------------------- /test-suite/tests/multiple_return_values/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return function() 5 | log("> Hello, I'm the original version of the function1, and I return 5!") 6 | return 5 7 | end, function() 8 | log("> Hello, I'm the original version of the function1, and I return 50!") 9 | return 50 10 | end 11 | ]=] 12 | 13 | local file2 = [=[ 14 | return function() 15 | log("> Hello, I'm a new version of the function1, and I return 10!") 16 | return 10 17 | end, function() 18 | log("> Hello, I'm a new version of the function2, and I return 100!") 19 | return 100 20 | end 21 | ]=] 22 | 23 | local func, func2 = DoFileString(file1) 24 | 25 | assert(func() == 5) 26 | assert(func2() == 50) 27 | 28 | ReloadFileString(file2) 29 | 30 | assert(func() == 10) 31 | assert(func2() == 100) 32 | -------------------------------------------------------------------------------- /test-suite/tests/nested_loads/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local fileNested = [=[ 4 | local fn = {} 5 | local base = 100 6 | function fn:Util() 7 | return base 8 | end 9 | return fn 10 | ]=] 11 | 12 | local fileNested2 = [=[ 13 | local fn = {} 14 | local base = 200 15 | function fn:Util() 16 | return base 17 | end 18 | return fn 19 | ]=] 20 | 21 | WriteFileString(fileNested, "nested") 22 | 23 | local file1 = [=[ 24 | local utils = dofile(GetTestFilename("nested")) 25 | return function() 26 | return utils:Util() + 1 27 | end 28 | ]=] 29 | 30 | local file2 = [=[ 31 | local utils = dofile(GetTestFilename("nested")) 32 | return function() 33 | return utils:Util() + 2 34 | end 35 | ]=] 36 | 37 | local func = DoFileString(file1) 38 | 39 | assert(func() == 101) 40 | 41 | WriteFileString(fileNested2, "nested") 42 | ReloadFileString(file2) 43 | 44 | assert(func() == 202) 45 | -------------------------------------------------------------------------------- /test-suite/tests/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | local function localFuncRemoved() 9 | log("> Hello, I'm the function defined in the original file which will be removed!") 10 | return true 11 | end 12 | return { 13 | func = localFunc, 14 | funcRemoved = localFuncRemoved 15 | } 16 | ]=] 17 | 18 | local file2 = [=[ 19 | local function localFunc() 20 | log("> Hello, I'm a new version of the function, and I return 10!") 21 | return 10 22 | end 23 | return { 24 | func = localFunc 25 | } 26 | ]=] 27 | 28 | local obj = DoFileString(file1) 29 | 30 | assert(obj.func() == 5) 31 | assert(obj.funcRemoved ~= nil) 32 | 33 | ReloadFileString(file2) 34 | 35 | assert(obj.func() == 10) 36 | assert(obj.funcRemoved == nil) 37 | -------------------------------------------------------------------------------- /test-suite/tests/test12.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | 6 | local module2 = {} 7 | function module2:Func() 8 | self.a = 20 9 | end 10 | 11 | function module:Func() 12 | module2:Func() 13 | self.a = 10 14 | return function() 15 | self.a = 10 16 | end 17 | end 18 | 19 | return module 20 | ]=] 21 | 22 | local file2 = [=[ 23 | local module = {} 24 | 25 | local module2 = {} 26 | function module2:Func() 27 | self.a = 20 28 | end 29 | 30 | function module:Func() 31 | module2:Func() 32 | self.a = 10 33 | return function() 34 | self.a = 10 35 | end 36 | end 37 | 38 | return module 39 | ]=] 40 | 41 | local obj = DoFileString(file1) 42 | 43 | local a = obj:Func() 44 | 45 | --assert(obj.func() == 5) 46 | 47 | ReloadFileString(file2) 48 | 49 | --assert(obj.func() == 10) 50 | -------------------------------------------------------------------------------- /test-suite/tests/timestamp_and_cache/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | -- assume we don't have get_timestamp functionality 4 | LuaReload.FileGetTimestamp = function(_) 5 | return 0 6 | end 7 | 8 | -- disable reloading for all files 9 | LuaReload.ShouldReload = function() 10 | return false 11 | end 12 | 13 | local file1 = [=[ 14 | return function() 15 | return 1 16 | end 17 | ]=] 18 | 19 | local file2 = [=[ 20 | return function() 21 | return 2 22 | end 23 | ]=] 24 | 25 | local func = DoFileString(file1) 26 | assert(func() == 1) 27 | 28 | -- TODO remove when vanilla Lua doesn't load new version of files 29 | -- by default (see loadFileNew) 30 | local isVanillaLua = type(jit) ~= "table" 31 | 32 | local func2 = DoFileString(file2) 33 | assert(isVanillaLua or func2() == 1) 34 | 35 | LuaReload.SetEnableTimestampCheck(false) 36 | 37 | local func3 = DoFileString(file2) 38 | assert(func() == 1) 39 | assert(isVanillaLua or func2() == 1) 40 | assert(func3() == 2) 41 | -------------------------------------------------------------------------------- /test-suite/tests/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | local tRemoved = { 9 | funcRemoved = function() 10 | log("> Hello, I'm the function defined in the original file which will be removed!") 11 | return true 12 | end 13 | } 14 | return { 15 | func = localFunc, 16 | tRemoved = tRemoved 17 | } 18 | ]=] 19 | 20 | local file2 = [=[ 21 | local function localFunc() 22 | log("> Hello, I'm a new version of the function, and I return 10!") 23 | return 10 24 | end 25 | return { 26 | func = localFunc 27 | } 28 | ]=] 29 | 30 | local obj = DoFileString(file1) 31 | 32 | assert(obj.func() == 5) 33 | assert(obj.tRemoved.funcRemoved ~= nil) 34 | 35 | ReloadFileString(file2) 36 | 37 | assert(obj.func() == 10) 38 | assert(obj.tRemoved == nil) 39 | -------------------------------------------------------------------------------- /test-suite/tests/getinfo/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function f2() 5 | end 6 | return function() 7 | return 0, f2 8 | end 9 | ]=] 10 | 11 | local file2 = [=[ 12 | local iter = 0 13 | local function f2() 14 | iter = iter + 1 15 | end 16 | return function() 17 | return iter, f2 18 | end 19 | ]=] 20 | 21 | local file3 = [=[ 22 | local iter = 0 23 | local function f2() 24 | iter = iter + 2 25 | end 26 | return function() 27 | return iter, f2 28 | end 29 | ]=] 30 | 31 | local f = DoFileString(file1) 32 | local iter, f2 = f() 33 | assert(iter == 0) 34 | f2() 35 | assert(f() == 0) 36 | 37 | ReloadFileString(file2) 38 | 39 | f2() 40 | assert(f() == 1) 41 | 42 | local iter, f2 = f() 43 | assert(iter == 1) 44 | f2() 45 | assert(f() == 2) 46 | 47 | DoFileString(file3) 48 | 49 | f2() 50 | assert(f() == 4) 51 | 52 | local iter, f2 = f() 53 | assert(iter == 4) 54 | f2() 55 | assert(f() == 6) 56 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = { 5 | } 6 | function t.New() 7 | local obj = {} 8 | setmetatable(obj, { __index = t }) 9 | obj.myClass = t 10 | return obj 11 | end 12 | function t:Func() 13 | return 0 14 | end 15 | return t 16 | ]=] 17 | 18 | local file2 = [=[ 19 | local t = { 20 | a = 100 21 | } 22 | function t.New() 23 | local obj = {} 24 | setmetatable(obj, { __index = t }) 25 | obj.myClass = t 26 | return obj 27 | end 28 | function t:Func() 29 | return 0 30 | end 31 | function t:GetA() 32 | return t.a 33 | end 34 | return t 35 | ]=] 36 | 37 | local obj = DoFileString(file1).New() 38 | 39 | assert(obj:Func() == 0) 40 | 41 | ReloadFileString(file2) 42 | 43 | assert(obj:Func() == 0) 44 | assert(obj:GetA() == 100) 45 | 46 | assert(obj.a == 100) 47 | 48 | obj.myClass.a = 200 49 | 50 | assert(obj:GetA() == 200) 51 | -------------------------------------------------------------------------------- /test-suite/tests/new_data/test7.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | return { 5 | basic = { 6 | f = function() 7 | return 1 8 | end 9 | }, 10 | advanced = { 11 | f = function() 12 | return 2 13 | end 14 | } 15 | } 16 | ]=] 17 | 18 | local file2 = [=[ 19 | return { 20 | basic = { 21 | f = function() 22 | return 10 23 | end, 24 | f2 = function() 25 | return 30 26 | end 27 | }, 28 | advanced = { 29 | f = function() 30 | return 20 31 | end 32 | }, 33 | f = function() 34 | return 40 35 | end 36 | } 37 | ]=] 38 | 39 | local t = DoFileString(file1) 40 | 41 | assert(t.basic.f() == 1) 42 | assert(t.advanced.f() == 2) 43 | 44 | ReloadFileString(file2) 45 | 46 | assert(t.basic.f() == 10) 47 | assert(t.advanced.f() == 20) 48 | assert(t.basic.f2() == 30) 49 | assert(t.f() == 40) -------------------------------------------------------------------------------- /test-suite/tests/anon/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | module.__index = module 6 | function module:new() 7 | return setmetatable({}, module) 8 | end 9 | function module:Func() 10 | return 100 11 | end 12 | function module:GetAnon() 13 | return function() 14 | return self:Func() 15 | end 16 | end 17 | return module 18 | ]=] 19 | 20 | local file2 = [=[ 21 | local module = {} 22 | module.__index = module 23 | function module:new() 24 | return setmetatable({}, module) 25 | end 26 | function module:Func() 27 | return 200 28 | end 29 | function module:GetAnon() 30 | return function() 31 | return self:Func() 32 | end 33 | end 34 | return module 35 | ]=] 36 | 37 | Module = DoFileString(file1) 38 | obj = Module:new() 39 | func = obj:GetAnon() 40 | 41 | assert(func() == 100) 42 | 43 | collectgarbage() 44 | collectgarbage() 45 | 46 | ReloadFileString(file2) 47 | 48 | assert(func() == 200) 49 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test2.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local file1 = [=[ 6 | GlobalModule = {} 7 | local iter = 0 8 | function GlobalModule.Func() 9 | log("> Hello, I'm the original version of the function, and I return 1000 + iter", iter, debug.getinfo(1).func) 10 | iter = iter + 1 11 | return 1000 + iter 12 | end 13 | print("XXX GlobalModule: ", GlobalModule) 14 | ]=] 15 | 16 | local file2 = [=[ 17 | GlobalModule = {} 18 | local iter = 0 19 | function GlobalModule.Func() 20 | log("> Hello, I'm the new version of the function, and I return 2000 + iter", iter, debug.getinfo(1).func) 21 | iter = iter + 1 22 | return 2000 + iter 23 | end 24 | print("XXX GlobalModule new: ", GlobalModule) 25 | ]=] 26 | 27 | local iter = DoFileString(file1) 28 | 29 | assert(GlobalModule.Func() == 1001) 30 | assert(GlobalModule.Func() == 1002) 31 | 32 | ReloadFileString(file2) 33 | 34 | assert(GlobalModule.Func() == 2003) 35 | assert(GlobalModule.Func() == 2004) 36 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test6.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | local data = "original" 6 | function module:Get() 7 | return data 8 | end 9 | function module:Set( value ) 10 | data = value 11 | end 12 | return module 13 | ]=] 14 | 15 | local file2 = [=[ 16 | local module = {} 17 | local data = "new" 18 | function module:Get() 19 | return data 20 | end 21 | function module:Set( value ) 22 | data = value 23 | end 24 | return module 25 | ]=] 26 | 27 | local file3 = [=[ 28 | local module = {} 29 | local data = "new2" 30 | function module:Get() 31 | return data 32 | end 33 | function module:Set( value ) 34 | data = value 35 | end 36 | return module 37 | ]=] 38 | 39 | local module = DoFileString(file1) 40 | assert(module:Get() == "original") 41 | 42 | module:Set("current") 43 | ReloadFileString(file2) 44 | assert(module:Get() == "current") 45 | 46 | module:Set("new") 47 | ReloadFileString(file3) 48 | assert(module:Get() == "new2") 49 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test6.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | -- TODO we do not update data in global tables yet, so this test fails 6 | --[[ 7 | local file1 = [=[ 8 | GlobalModule = { 9 | iter = 0 10 | } 11 | function GlobalModule:Func() 12 | log("> Hello, I'm the original version of the function, and I return 1000 + iter", self.iter, debug.getinfo(1).func) 13 | self.iter = self.iter + 1 14 | return 1000 + self.iter 15 | end 16 | ]=] 17 | 18 | local file2 = [=[ 19 | GlobalModule = { 20 | iter = 0 21 | } 22 | function GlobalModule:Func() 23 | log("> Hello, I'm the new version of the function, and I return 2000 + iter", self.iter, debug.getinfo(1).func) 24 | self.iter = self.iter + 1 25 | return 2000 + self.iter 26 | end 27 | ]=] 28 | 29 | local iter = DoFileString(file1) 30 | 31 | assert(GlobalModule:Func() == 1001) 32 | assert(GlobalModule:Func() == 1002) 33 | 34 | ReloadFileString(file2) 35 | 36 | assert(GlobalModule:Func() == 2003) 37 | assert(GlobalModule:Func() == 2004) 38 | ]] 39 | -------------------------------------------------------------------------------- /test-suite/tests/getinfo/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local t = {} 5 | local function f1() 6 | end 7 | local function f2() 8 | end 9 | return f1, f2, t 10 | ]=] 11 | 12 | local file2 = [=[ 13 | local iter = 0 14 | local t = {} 15 | local function f1() 16 | iter = iter + 1 17 | return iter 18 | end 19 | local function f2() 20 | iter = iter + 1 21 | return iter 22 | end 23 | return f1, f2, t 24 | ]=] 25 | 26 | local file3 = [=[ 27 | local iter = 0 28 | local t = {} 29 | local function f1() 30 | iter = iter + 1 31 | return iter, t.a 32 | end 33 | local function f2() 34 | iter = iter + 1 35 | return iter, t.a 36 | end 37 | return f1, f2, t 38 | ]=] 39 | 40 | local f1, f2, t = DoFileString(file1) 41 | t.a = 100 42 | 43 | DoFileString(file1) 44 | collectgarbage() 45 | collectgarbage() 46 | ReloadFileString(file2) 47 | 48 | assert(f1() == 1) 49 | assert(f2() == 2) 50 | 51 | DoFileString(file3) 52 | 53 | AssertCall({ 3, 100 }, f1()) 54 | AssertCall({ 4, 100 }, f2()) 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Klymenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test10.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local file1 = [=[ 6 | GlobalModule = {} 7 | GlobalModule.constValue = 100 8 | local iter = 0 9 | function GlobalModule.Func() 10 | log("> Hello, I'm the original version of the function, and I return 1000 + iter", iter, debug.getinfo(1).func) 11 | iter = iter + 1 12 | return 1000 + iter 13 | end 14 | print("XXX GlobalModule: ", GlobalModule) 15 | ]=] 16 | 17 | local file2 = [=[ 18 | GlobalModule = {} 19 | GlobalModule.constValue = 200 20 | local iter = 0 21 | function GlobalModule.Func() 22 | log("> Hello, I'm the new version of the function, and I return 2000 + iter", iter, debug.getinfo(1).func) 23 | iter = iter + 1 24 | return 2000 + iter 25 | end 26 | print("XXX GlobalModule new: ", GlobalModule) 27 | ]=] 28 | 29 | local iter = DoFileString(file1) 30 | 31 | assert(GlobalModule.Func() == 1001) 32 | assert(GlobalModule.Func() == 1002) 33 | 34 | ReloadFileString(file2) 35 | 36 | assert(GlobalModule.Func() == 2003) 37 | assert(GlobalModule.Func() == 2004) 38 | -------------------------------------------------------------------------------- /test-suite/tests/test8.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local calls = 0 5 | local function Call() 6 | calls = calls + 1 7 | log("> Hello, you've called Call()", calls, "times") 8 | return calls, false 9 | end 10 | return { 11 | Call = Call 12 | } 13 | ]=] 14 | 15 | local file2 = [=[ 16 | local calls = 0 17 | local function Call() 18 | calls = calls + 1 19 | log("> Hello, you've called Call()", calls, "times, and I'm the new version of it") 20 | return calls, true 21 | end 22 | local function PrintCalls() 23 | log("> You've called Call()", calls, "times, and I'm PrintCalls()") 24 | return calls 25 | end 26 | return { 27 | Call = Call, 28 | PrintCalls = PrintCalls 29 | } 30 | ]=] 31 | 32 | local obj = DoFileString(file1) 33 | 34 | local calls, isNew = obj.Call() 35 | assert(calls == 1) 36 | assert(not isNew) 37 | 38 | ReloadFileString(file2) 39 | 40 | local calls, isNew = obj.Call() 41 | assert(calls == 2) 42 | assert(isNew) 43 | 44 | assert(obj.PrintCalls) 45 | local calls = obj.PrintCalls() 46 | assert(calls == 2) 47 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test3.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local file1 = [=[ 6 | GlobalModule = GlobalModule or {} 7 | GlobalModule.subModule = GlobalModule.subModule or {} 8 | local iter = 0 9 | function GlobalModule.subModule.Func() 10 | log("> Hello, I'm the original version of the function, and I return 1000 + iter", iter, debug.getinfo(1).func) 11 | iter = iter + 1 12 | return 1000 + iter 13 | end 14 | ]=] 15 | 16 | local file2 = [=[ 17 | GlobalModule = GlobalModule or {} 18 | GlobalModule.subModule = GlobalModule.subModule or {} 19 | local iter = 0 20 | function GlobalModule.subModule.Func() 21 | log("> Hello, I'm the new version of the function, and I return 2000 + iter", iter, debug.getinfo(1).func) 22 | iter = iter + 1 23 | return 2000 + iter 24 | end 25 | ]=] 26 | 27 | local iter = DoFileString(file1) 28 | 29 | assert(GlobalModule.subModule.Func() == 1001) 30 | assert(GlobalModule.subModule.Func() == 1002) 31 | 32 | ReloadFileString(file2) 33 | 34 | assert(GlobalModule.subModule.Func() == 2003) 35 | assert(GlobalModule.subModule.Func() == 2004) 36 | -------------------------------------------------------------------------------- /test-suite/tests/getinfo/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local iter = 0 5 | return { 6 | f1 = function() 7 | iter = iter + 1 8 | return iter 9 | end, 10 | f2 = function() 11 | log("f2 called, which uses global log") 12 | iter = iter + 1 13 | return iter 14 | end 15 | } 16 | ]=] 17 | 18 | local file2 = [=[ 19 | local iter = 0 20 | local const = 0 21 | return { 22 | setConst = function(value) 23 | const = value 24 | end, 25 | f1 = function() 26 | iter = iter + 2 27 | return iter, const 28 | end, 29 | f2 = function() 30 | log("f2 called, which uses global log") 31 | iter = iter + 2 32 | return iter, const 33 | end 34 | } 35 | ]=] 36 | 37 | local t = DoFileString(file1) 38 | 39 | assert(t.f1() == 1) 40 | assert(t.f1() == 2) 41 | assert(t.f2() == 3) 42 | assert(t.f2() == 4) 43 | 44 | ReloadFileString(file2) 45 | 46 | AssertCall({6, 0}, t.f1()) 47 | AssertCall({8, 0}, t.f2()) 48 | t.setConst(10) 49 | AssertCall({10, 10}, t.f2()) 50 | AssertCall({12, 10}, t.f1()) 51 | -------------------------------------------------------------------------------- /test-suite/tests/test16.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local fn = { 5 | a = 0 6 | } 7 | function fn:GetFunc1() 8 | return function() 9 | self.a = self.a + 1 10 | return self.a 11 | end 12 | end 13 | function fn:GetFunc2() 14 | return function() 15 | self.a = self.a + 1 16 | return self.a 17 | end 18 | end 19 | return fn 20 | ]=] 21 | 22 | local file2 = [=[ 23 | local fn = { 24 | a = 0 25 | } 26 | function fn:GetFunc1() 27 | return function() 28 | self.a = self.a + 2 29 | return self.a 30 | end 31 | end 32 | function fn:GetFunc2() 33 | return function() 34 | self.a = self.a + 2 35 | return self.a 36 | end 37 | end 38 | return fn 39 | ]=] 40 | 41 | local obj = DoFileString(file1) 42 | local f1 = obj:GetFunc1() 43 | local f2 = obj:GetFunc2() 44 | assert(f1() == 1) 45 | assert(f2() == 2) 46 | 47 | ReloadFileString(file2) 48 | 49 | assert(f1() == 3) 50 | assert(f2() == 4) 51 | local _f1 = obj:GetFunc1() 52 | local _f2 = obj:GetFunc2() 53 | assert(_f1() == 6) 54 | assert(_f2() == 8) 55 | -------------------------------------------------------------------------------- /test-suite/tests/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | local function localFuncRemoved() 9 | log("> Hello, I'm the function defined in the original file which will be removed!") 10 | return true 11 | end 12 | return { 13 | func = localFunc, 14 | funcRemoved = localFuncRemoved 15 | } 16 | ]=] 17 | 18 | local file2 = [=[ 19 | local function localFunc() 20 | log("> Hello, I'm a new version of the function, and I return 10!") 21 | return 10 22 | end 23 | local function localFuncAdded() 24 | log("> Hello, I'm a new function defined in the new versin of the file!") 25 | return true 26 | end 27 | return { 28 | func = localFunc, 29 | funcAdded = localFuncAdded 30 | } 31 | ]=] 32 | 33 | local obj = DoFileString(file1) 34 | 35 | assert(obj.func() == 5) 36 | assert(obj.funcRemoved ~= nil) 37 | assert(obj.funcAdded == nil) 38 | 39 | ReloadFileString(file2) 40 | 41 | assert(obj.func() == 10) 42 | assert(obj.funcRemoved == nil) 43 | assert(obj.funcAdded ~= nil) 44 | -------------------------------------------------------------------------------- /test-suite/tests/anon/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function Func() 5 | return 100 6 | end 7 | return function() 8 | return function() 9 | return Func() 10 | end 11 | end 12 | ]=] 13 | 14 | local file2 = [=[ 15 | local function Func() 16 | return 200 17 | end 18 | return function() 19 | return function() 20 | return Func() 21 | end 22 | end 23 | ]=] 24 | 25 | local fn = LoadFileString(file1) 26 | local original = fn() 27 | local func = original() 28 | 29 | 30 | local getfenv = getfenv or function(func) 31 | local i = 1 32 | while true do 33 | local name, val = debug.getupvalue(func, i) 34 | print("getupvalue", name, val) 35 | if name == "_ENV" then 36 | return val 37 | elseif not name then 38 | break 39 | end 40 | i = i + 1 41 | end 42 | return nil 43 | end 44 | 45 | print(getfenv(fn)) 46 | print(getfenv(original)) 47 | print(getfenv(func)) 48 | 49 | fn = nil 50 | original = nil 51 | 52 | assert(func() == 100) 53 | collectgarbage() 54 | collectgarbage() 55 | 56 | ReloadFileString(file2) 57 | 58 | assert(func() == 200) 59 | -------------------------------------------------------------------------------- /test-suite/tests/env/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | -- TODO: we do not update envs properly, so this test fails 4 | 5 | --[[ 6 | local file1 = [=[ 7 | local f = function() 8 | log("> Hello, I'm the original version of the function, and I return 5!", global) 9 | return 5 10 | end 11 | setfenv(f, { 12 | f = function() 13 | log("> Hello, I'm the original version of the function, and I return 15!") 14 | return 15 15 | end, 16 | log = log, 17 | global = 100 18 | }) 19 | return f 20 | ]=] 21 | 22 | local file2 = [=[ 23 | local f = function() 24 | log("> Hello, I'm a new version of the function, and I return 10!", global) 25 | return 10 26 | end 27 | setfenv(f, { 28 | f = function() 29 | log("> Hello, I'm a new version of the function, and I return 20!") 30 | return 20 31 | end, 32 | log = log, 33 | global = 100 34 | }) 35 | return f 36 | ]=] 37 | 38 | local func = DoFileString(file1) 39 | 40 | assert(func() == 5) 41 | local env = getfenv(func) 42 | assert(env.f() == 15) 43 | 44 | ReloadFileString(file2) 45 | 46 | assert(getfenv(func) == env) 47 | assert(func() == 10) 48 | assert(env.f() == 20) 49 | ]] 50 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local file1 = [=[ 6 | local iter = 0 7 | local function LocalFunc() 8 | log("> [OLD] iter", iter, debug.getinfo(1).func) 9 | iter = iter + 1 10 | return iter 11 | end 12 | function GlobalFunc() 13 | log("> Hello, I'm the original version of the function, and I return 1000 + iter!", debug.getinfo(1).func) 14 | return 1000 + LocalFunc() 15 | end 16 | return LocalFunc 17 | ]=] 18 | 19 | local file2 = [=[ 20 | local iter = 0 21 | local function LocalFunc() 22 | log("> [NEW] iter", iter, debug.getinfo(1).func) 23 | iter = iter + 1 24 | return 200 + iter 25 | end 26 | function GlobalFunc() 27 | local r = 2000 + LocalFunc() 28 | log("> Hello, I'm the new version of the function, and I return 2000 + 200 + iter!", r, debug.getinfo(1).func) 29 | return r 30 | end 31 | ]=] 32 | 33 | local iter = DoFileString(file1) 34 | 35 | assert(GlobalFunc() == 1001) 36 | assert(GlobalFunc() == 1002) 37 | assert(iter() == 3) 38 | assert(GlobalFunc() == 1004) 39 | 40 | ReloadFileString(file2) 41 | 42 | assert(GlobalFunc() == 2205) 43 | assert(GlobalFunc() == 2206) 44 | assert(iter() == 207) 45 | assert(GlobalFunc() == 2208) 46 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test8.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local file1 = [=[ 6 | local iter = 0 7 | local function LocalFunc() 8 | log("> [OLD] iter", iter, debug.getinfo(1).func) 9 | iter = iter + 1 10 | return iter 11 | end 12 | function _G.GlobalFunc() 13 | log("> Hello, I'm the original version of the function, and I return 1000 + iter!", debug.getinfo(1).func) 14 | return 1000 + LocalFunc() 15 | end 16 | return LocalFunc 17 | ]=] 18 | 19 | local file2 = [=[ 20 | local iter = 0 21 | local function LocalFunc() 22 | log("> [NEW] iter", iter, debug.getinfo(1).func) 23 | iter = iter + 1 24 | return 200 + iter 25 | end 26 | function _G.GlobalFunc() 27 | local r = 2000 + LocalFunc() 28 | log("> Hello, I'm the new version of the function, and I return 2000 + 200 + iter!", r, debug.getinfo(1).func) 29 | return r 30 | end 31 | ]=] 32 | 33 | local iter = DoFileString(file1) 34 | 35 | assert(GlobalFunc() == 1001) 36 | assert(GlobalFunc() == 1002) 37 | assert(iter() == 3) 38 | assert(GlobalFunc() == 1004) 39 | 40 | ReloadFileString(file2) 41 | 42 | assert(GlobalFunc() == 2205) 43 | assert(GlobalFunc() == 2206) 44 | assert(iter() == 207) 45 | assert(GlobalFunc() == 2208) 46 | -------------------------------------------------------------------------------- /test-suite/tests/test6.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | log("> Hello, I'm the original version of the function, and I return 5!") 6 | return 5 7 | end 8 | local function localFuncRemoved() 9 | log("> Hello, I'm the function defined in the original file which will be removed!") 10 | return true 11 | end 12 | return function() 13 | return { 14 | func = localFunc, 15 | funcRemoved = localFuncRemoved 16 | } 17 | end 18 | ]=] 19 | 20 | local file2 = [=[ 21 | local function localFunc() 22 | log("> Hello, I'm a new version of the function, and I return 10!") 23 | return 10 24 | end 25 | local function localFuncAdded() 26 | log("> Hello, I'm a new function defined in the new versin of the file!") 27 | return true 28 | end 29 | return function() 30 | return { 31 | func = localFunc, 32 | funcAdded = localFuncAdded 33 | } 34 | end 35 | ]=] 36 | 37 | local obj = DoFileString(file1) 38 | 39 | assert(obj().func() == 5) 40 | assert(obj().funcRemoved ~= nil) 41 | assert(obj().funcAdded == nil) 42 | 43 | ReloadFileString(file2) 44 | 45 | assert(obj().func() == 10) 46 | assert(obj().funcRemoved == nil) 47 | assert(obj().funcAdded ~= nil) 48 | -------------------------------------------------------------------------------- /test-suite/tests/several_routes/test1.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local function localFunc() 5 | return 5 6 | end 7 | local function memberFunc1() 8 | log("> Hello, I'm the original version of the memberFunc1, and I return 15!") 9 | return 10 + localFunc() 10 | end 11 | local function memberFunc2() 12 | log("> Hello, I'm the original version of the memberFunc2, and I return 25!") 13 | return 20 + localFunc() 14 | end 15 | return { 16 | memberFunc1 = memberFunc1, 17 | memberFunc2 = memberFunc2 18 | }, 1000 19 | ]=] 20 | 21 | local file2 = [=[ 22 | local function localFunc() 23 | return 10 24 | end 25 | local function memberFunc1() 26 | -- reference to the localFunc is removed here, so we are forced to follow a second route 27 | -- (return values/1/memberFunc2/upvalue localFunc) 28 | log("> Hello, I'm the new version of the memberFunc1, and I return 100!") 29 | return 100 30 | end 31 | local memberFunc2 = nil 32 | return { 33 | memberFunc1 = memberFunc1, 34 | memberFunc2 = memberFunc2 35 | }, {} 36 | ]=] 37 | 38 | local obj = DoFileString(file1) 39 | local memberFunc2 = obj.memberFunc2 40 | assert(obj.memberFunc1() == 15) 41 | assert(obj.memberFunc2() == 25) 42 | assert(memberFunc2() == 25) 43 | 44 | ReloadFileString(file2) 45 | 46 | assert(obj.memberFunc1() == 100) 47 | assert(memberFunc2 == nil) 48 | -------------------------------------------------------------------------------- /test-suite/tests/test17.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local fn = {} 5 | local a = 0 6 | function fn:GetA() 7 | a = a + 1 8 | return a 9 | end 10 | function fn:GetFunc1() 11 | local a = 10 12 | return function() 13 | a = a + 1 14 | return a 15 | end 16 | end 17 | function fn:GetFunc2() 18 | local a = 100 19 | return function() 20 | a = a + 1 21 | return a 22 | end 23 | end 24 | return fn 25 | ]=] 26 | 27 | local file2 = [=[ 28 | local fn = {} 29 | local a = 0 30 | function fn:GetA() 31 | a = a + 1 32 | return a 33 | end 34 | function fn:GetFunc1() 35 | local a = 20 36 | return function() 37 | a = a + 1 38 | return a 39 | end 40 | end 41 | function fn:GetFunc2() 42 | local a = 200 43 | return function() 44 | a = a + 1 45 | return a 46 | end 47 | end 48 | return fn 49 | ]=] 50 | 51 | local obj = DoFileString(file1) 52 | local f1 = obj:GetFunc1() 53 | local f2 = obj:GetFunc2() 54 | assert(obj:GetA() == 1) 55 | assert(f1() == 11) 56 | assert(f2() == 101) 57 | 58 | ReloadFileString(file2) 59 | 60 | assert(obj:GetA() == 2) 61 | assert(f1() == 12) 62 | assert(f2() == 102) 63 | local _f1 = obj:GetFunc1() 64 | local _f2 = obj:GetFunc2() 65 | assert(_f1() == 21) 66 | assert(_f2() == 201) 67 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test9.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | -- SKIPPED dut to not supporting crazy things like `getfenv(print)` 6 | --[[ 7 | local file1 = [=[ 8 | local iter = 0 9 | local function LocalFunc() 10 | log("> [OLD] iter", iter, debug.getinfo(1).func) 11 | iter = iter + 1 12 | return iter 13 | end 14 | local g = getfenv(print) 15 | function g.GlobalFunc() 16 | log("> Hello, I'm the original version of the function, and I return 1000 + iter!", debug.getinfo(1).func) 17 | return 1000 + LocalFunc() 18 | end 19 | return LocalFunc 20 | ]=] 21 | 22 | local file2 = [=[ 23 | local iter = 0 24 | local function LocalFunc() 25 | log("> [NEW] iter", iter, debug.getinfo(1).func) 26 | iter = iter + 1 27 | return 200 + iter 28 | end 29 | local g = getfenv(print) 30 | function g.GlobalFunc() 31 | local r = 2000 + LocalFunc() 32 | log("> Hello, I'm the new version of the function, and I return 2000 + 200 + iter!", r, debug.getinfo(1).func) 33 | return r 34 | end 35 | ]=] 36 | 37 | local iter = DoFileString(file1) 38 | 39 | assert(GlobalFunc() == 1001) 40 | assert(GlobalFunc() == 1002) 41 | assert(iter() == 3) 42 | assert(GlobalFunc() == 1004) 43 | 44 | ReloadFileString(file2) 45 | 46 | assert(GlobalFunc() == 2205) 47 | assert(GlobalFunc() == 2206) 48 | assert(iter() == 207) 49 | assert(GlobalFunc() == 2208) 50 | --]] 51 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local fileBaseModule = [===[ 6 | Module = {} 7 | function Module:CreateModule(modulePath) 8 | local parent = Module 9 | -- split by dot 10 | for module in string.gmatch(modulePath, [=[[^\.]+]=]) do 11 | parent[module] = parent[module] or {} 12 | parent = parent[module] 13 | end 14 | return parent 15 | end 16 | ]===] 17 | 18 | local file1 = [=[ 19 | local module = Module:CreateModule("first.second.third") 20 | local iter = 0 21 | function module.Func() 22 | log("> Hello, I'm the original version of the function, and I return 1000 + iter", iter, debug.getinfo(1).func) 23 | iter = iter + 1 24 | return 1000 + iter 25 | end 26 | ]=] 27 | 28 | local file2 = [=[ 29 | local module = Module:CreateModule("first.second.third") 30 | local iter = 0 31 | function module.Func() 32 | log("> Hello, I'm the new version of the function, and I return 2000 + iter", iter, debug.getinfo(1).func) 33 | iter = iter + 1 34 | return 2000 + iter 35 | end 36 | ]=] 37 | 38 | DoFileString(fileBaseModule, "base_module") 39 | 40 | local iter = DoFileString(file1) 41 | 42 | assert(Module.first.second.third.Func() == 1001) 43 | assert(Module.first.second.third.Func() == 1002) 44 | 45 | ReloadFileString(file2) 46 | 47 | assert(Module.first.second.third.Func() == 2003) 48 | assert(Module.first.second.third.Func() == 2004) 49 | -------------------------------------------------------------------------------- /test-suite/tests/data_change/test7.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | return module 6 | ]=] 7 | 8 | local file2 = [=[ 9 | local module = { 10 | data = "original" 11 | } 12 | return module 13 | ]=] 14 | 15 | local file3 = [=[ 16 | local module = { 17 | data = "original" 18 | } 19 | function module.New() 20 | return setmetatable({}, { __index = module }) 21 | end 22 | function module:Get() 23 | return self.data 24 | end 25 | function module:Set(value) 26 | self.data = value 27 | end 28 | return module 29 | ]=] 30 | 31 | local file4 = [=[ 32 | local module = { 33 | data = "new" 34 | } 35 | function module.New() 36 | return setmetatable({}, { __index = module }) 37 | end 38 | function module:Get() 39 | return self.data 40 | end 41 | function module:Set(value) 42 | self.data = value 43 | end 44 | return module 45 | ]=] 46 | 47 | local module = DoFileString(file1) 48 | 49 | ReloadFileString(file2) 50 | assert(module.data == "original") 51 | 52 | ReloadFileString(file3) 53 | local obj1 = module.New() 54 | local obj2 = module.New() 55 | assert(obj1:Get() == "original") 56 | assert(obj2:Get() == "original") 57 | 58 | obj1:Set("current") 59 | ReloadFileString(file4) 60 | assert(obj1:Get() == "current") 61 | assert(obj2:Get() == "new") 62 | 63 | module.data = "current2" 64 | ReloadFileString(file4) 65 | assert(obj1:Get() == "current") 66 | assert(obj2:Get() == "current2") 67 | -------------------------------------------------------------------------------- /test-suite/tests/anon/test4.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file0 = [=[ 4 | Module = {} 5 | Module.__index = Module 6 | function Module:new() 7 | return setmetatable({}, Module) 8 | end 9 | function Module:GetAnon() 10 | return function() 11 | return 100 12 | end 13 | end 14 | ]=] 15 | 16 | local file1 = [=[ 17 | Module = {} 18 | Module.__index = Module 19 | function Module:new() 20 | return setmetatable({}, Module) 21 | end 22 | function Module:Func() 23 | return 200 24 | end 25 | function Module:GetAnon() 26 | return function() 27 | return self:Func() 28 | end 29 | end 30 | ]=] 31 | 32 | local file2 = [=[ 33 | Module = {} 34 | Module.__index = Module 35 | function Module:new() 36 | return setmetatable({}, Module) 37 | end 38 | function Module:Func() 39 | return 300 40 | end 41 | function Module:GetAnon() 42 | return function() 43 | return self:Func() 44 | end 45 | end 46 | ]=] 47 | 48 | DoFileString(file0) 49 | local obj = Module:new() 50 | local func = obj:GetAnon() 51 | 52 | assert(func() == 100) 53 | 54 | collectgarbage() 55 | collectgarbage() 56 | 57 | ReloadFileString(file1) 58 | --[[ 59 | assert(func() == 100) 60 | local obj2 = Module:new() 61 | local func2 = obj2:GetAnon() 62 | assert(func2() == 200) 63 | 64 | ReloadFileString(file2) 65 | 66 | assert(func() == 100) 67 | assert(func2() == 300) 68 | assert(obj2:Func() == 300) 69 | assert(obj:Func() == 300) 70 | --]] 71 | -------------------------------------------------------------------------------- /test-suite/tests/anon/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file0 = [=[ 4 | local Module = {} 5 | Module.__index = Module 6 | function Module:new() 7 | return setmetatable({}, Module) 8 | end 9 | function Module:GetAnon() 10 | return function() 11 | return 100 12 | end 13 | end 14 | return Module 15 | ]=] 16 | 17 | local file1 = [=[ 18 | local Module = {} 19 | Module.__index = Module 20 | function Module:new() 21 | return setmetatable({}, Module) 22 | end 23 | function Module:Func() 24 | return 200 25 | end 26 | function Module:GetAnon() 27 | return function() 28 | return self:Func() 29 | end 30 | end 31 | return Module 32 | ]=] 33 | 34 | local file2 = [=[ 35 | local Module = {} 36 | Module.__index = Module 37 | function Module:new() 38 | return setmetatable({}, Module) 39 | end 40 | function Module:Func() 41 | return 300 42 | end 43 | function Module:GetAnon() 44 | return function() 45 | return self:Func() 46 | end 47 | end 48 | return Module 49 | ]=] 50 | 51 | local Module = DoFileString(file0) 52 | local obj = Module:new() 53 | local func = obj:GetAnon() 54 | 55 | assert(func() == 100) 56 | 57 | collectgarbage() 58 | collectgarbage() 59 | 60 | ReloadFileString(file1) 61 | --[[ 62 | assert(func() == 100) 63 | local obj2 = Module:new() 64 | local func2 = obj2:GetAnon() 65 | assert(func2() == 200) 66 | 67 | ReloadFileString(file2) 68 | 69 | assert(func() == 100) 70 | assert(func2() == 300) 71 | assert(obj2:Func() == 300) 72 | assert(obj:Func() == 300)]] 73 | -------------------------------------------------------------------------------- /test-suite/tests/test9.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | local iter = 0 6 | 7 | function module.Func() 8 | iter = iter + 1 9 | log("[OLD] iter has been increased, it's", iter, "now") 10 | return iter, false 11 | end 12 | 13 | function module.GetNestedFunc() 14 | return function() 15 | iter = iter + 1 16 | log("[OLD] iter has been increased, it's", iter, "now (this is a closure returned from GetNestedFunc which can't be updated)") 17 | return iter, false 18 | end 19 | end 20 | 21 | return module 22 | ]=] 23 | 24 | local file2 = [=[ 25 | local module = {} 26 | local iter = 0 27 | 28 | function module.Func() 29 | iter = iter + 1 30 | log("[NEW] iter has been increased, it's", iter, "now") 31 | return iter, true 32 | end 33 | 34 | function module.GetNestedFunc() 35 | return function() 36 | iter = iter + 1 37 | log("[NEW] iter has been increased, it's", iter, "now (this is a closure retrieved from the new version of the file)") 38 | return iter, true 39 | end 40 | end 41 | 42 | return module 43 | ]=] 44 | 45 | local obj = DoFileString(file1) 46 | local NestedFunc = obj.GetNestedFunc() 47 | 48 | AssertCall({ 1, false }, obj.Func()) 49 | AssertCall({ 2, false }, NestedFunc()) 50 | 51 | ReloadFileString(file2) 52 | local NestedFuncNew = obj.GetNestedFunc() 53 | log("*** File has been reloaded ***") 54 | 55 | AssertCall({ 3, true }, obj.Func()) 56 | log("Anonymous function expected to be OLD:") 57 | AssertCall({ 4, false }, NestedFunc()) 58 | AssertCall({ 5, true }, NestedFuncNew()) 59 | AssertCall({ 6, true }, obj.Func()) 60 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test5.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | local fileBaseModule = [===[ 6 | Module = {} 7 | function Module:CreateModule(modulePath) 8 | local parent = Module 9 | -- split by dot 10 | for module in string.gmatch(modulePath, [=[[^\.]+]=]) do 11 | parent[module] = parent[module] or {} 12 | parent = parent[module] 13 | end 14 | return parent 15 | end 16 | ]===] 17 | 18 | local file1 = [=[ 19 | local module = Module:CreateModule("first.second.third") 20 | local iter = 0 21 | local function LocalFunc() 22 | log("> [OLD] iter", iter, debug.getinfo(1).func) 23 | iter = iter + 1 24 | return iter 25 | end 26 | function module.Func() 27 | log("> Hello, I'm the original version of the function, and I return 1000 + iter!", debug.getinfo(1).func) 28 | return 1000 + LocalFunc() 29 | end 30 | return LocalFunc 31 | ]=] 32 | 33 | local file2 = [=[ 34 | local module = Module:CreateModule("first.second.third") 35 | local iter = 0 36 | local function LocalFunc() 37 | log("> [NEW] iter", iter, debug.getinfo(1).func) 38 | iter = iter + 1 39 | return 200 + iter 40 | end 41 | function module.Func() 42 | local r = 2000 + LocalFunc() 43 | log("> Hello, I'm the new version of the function, and I return 2000 + 200 + iter!", r, debug.getinfo(1).func) 44 | return r 45 | end 46 | ]=] 47 | 48 | DoFileString(fileBaseModule, "base_module") 49 | 50 | local iter = DoFileString(file1) 51 | 52 | assert(Module.first.second.third.Func() == 1001) 53 | assert(Module.first.second.third.Func() == 1002) 54 | assert(iter() == 3) 55 | assert(Module.first.second.third.Func() == 1004) 56 | 57 | ReloadFileString(file2) 58 | 59 | assert(Module.first.second.third.Func() == 2205) 60 | assert(Module.first.second.third.Func() == 2206) 61 | assert(iter() == 207) 62 | assert(Module.first.second.third.Func() == 2208) 63 | -------------------------------------------------------------------------------- /test-suite/tests/global_state/test7.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | LuaReload.SetHandleGlobalModules(true) 4 | 5 | list = {} 6 | 7 | local file1 = [=[ 8 | Module = {} 9 | Module.__index = Module 10 | function Module:new(value) 11 | local o = { 12 | value = value 13 | } 14 | setmetatable(o, Module) 15 | return o 16 | end 17 | function Module:GetFunc() 18 | table.insert(list, function(value) 19 | log("[OLD] self.value is", self.value, "value is", value) 20 | return self.value + value 21 | end) 22 | end 23 | ]=] 24 | 25 | local file2 = [=[ 26 | Module = {} 27 | Module.__index = Module 28 | function Module:new(value) 29 | local o = { 30 | value = value 31 | } 32 | setmetatable(o, Module) 33 | return o 34 | end 35 | function Module:Compute(value) 36 | log("[NEW] self.value is", self.value, "value is", value) 37 | return self.value + value + 1000 38 | end 39 | function Module:GetFunc() 40 | table.insert(list, function(value) 41 | return self:Compute(value) 42 | end) 43 | end 44 | ]=] 45 | 46 | local file3 = [=[ 47 | Module = {} 48 | Module.__index = Module 49 | function Module:new(value) 50 | local o = { 51 | value = value 52 | } 53 | setmetatable(o, Module) 54 | return o 55 | end 56 | function Module:Compute(value) 57 | log("[NEW2] self.value is", self.value, "value is", value) 58 | return self.value + value + 2000 59 | end 60 | function Module:GetFunc() 61 | table.insert(list, function(value) 62 | return self:Compute(value) 63 | end) 64 | end 65 | ]=] 66 | 67 | DoFileString(file1) 68 | local obj = Module:new(1) 69 | obj:GetFunc() 70 | obj = nil 71 | assert(list[1](100) == 101) 72 | table.remove(list, 1) 73 | 74 | ReloadFileString(file2) 75 | 76 | local obj = Module:new(1) 77 | obj:GetFunc() 78 | obj = nil 79 | 80 | assert(list[1](100) == 1101) 81 | 82 | ReloadFileString(file3) 83 | 84 | assert(list[1](100) == 2101) 85 | 86 | -------------------------------------------------------------------------------- /test-suite/utils.lua: -------------------------------------------------------------------------------- 1 | function log(...) 2 | local num = select("#", ...) 3 | local arg = {...} 4 | local msg = "" 5 | for i = 1, num do 6 | msg = msg .. tostring(arg[i]) .. " " 7 | end 8 | print(msg) 9 | end 10 | 11 | function Error(...) 12 | local num = select("#", ...) 13 | local arg = {...} 14 | local msg = "" 15 | for i = 1, num do 16 | msg = msg .. tostring(arg[i]) .. " " 17 | end 18 | error(msg) 19 | end 20 | 21 | function ErrorLevel(level, ...) 22 | local num = select("#", ...) 23 | local arg = {...} 24 | local msg = "" 25 | for i = 1, num do 26 | msg = msg .. tostring(arg[i]) .. " " 27 | end 28 | error(msg, level) 29 | end 30 | 31 | function CopyFile(filename1, filename2) 32 | local file1 = io.open(filename1, "r") 33 | local data = file1:read("*a") 34 | file1:close() 35 | 36 | local file2 = io.open(filename2, "w+") 37 | file2:write(data) 38 | file2:close() 39 | end 40 | 41 | function AssertCall(returnValues, ...) 42 | for i = 1, select("#", ...) do 43 | local expected = returnValues[i] 44 | local actual = select(i, ...) 45 | if expected ~= actual then 46 | ErrorLevel(3, "Value no.", i, "is expected to be [", expected, "] but it is [", actual, "]") 47 | end 48 | end 49 | return true 50 | end 51 | 52 | function AssertCallTable(returnValues, ...) 53 | for i = 1, select("#", ...) do 54 | local t_expected = returnValues[i] 55 | local t_actual = select(i, ...) 56 | for k, v in pairs(t_expected) do 57 | local expected = t_expected[k] 58 | local actual = t_actual[k] 59 | if expected ~= actual then 60 | Error("Value at [", k, "] in returned table no.", i, 61 | "is expected to be [", expected, "] but it is [", actual, "]") 62 | end 63 | end 64 | for k, v in pairs(t_actual) do 65 | local expected = t_expected[k] 66 | local actual = t_actual[k] 67 | if expected ~= actual then 68 | Error("Value at [", k, "] in returned table no.", i, 69 | "is expected to be [", expected, "] but it is [", actual, "]") 70 | end 71 | end 72 | end 73 | return true 74 | end 75 | -------------------------------------------------------------------------------- /test-suite/tests/test10.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file1 = [=[ 4 | local module = {} 5 | local iter = 0 6 | local garbage = { exists = true, data = 0 } -- ensure this variable is removed when all those functions are removed 7 | 8 | function module.Func() 9 | garbage.data = garbage.data + 1 -- make this function use garbage, so garbage will be removed only when this function is removed 10 | iter = iter + 1 11 | log("[OLD] iter has been increased, it's", iter, "now") 12 | return iter, false 13 | end 14 | 15 | function module.GetNestedFunc() 16 | garbage.data = garbage.data + 1 -- make this function use garbage, so garbage will be removed only when this function is removed 17 | return function() 18 | iter = iter + 1 19 | log("[OLD] iter has been increased, it's", iter, "now (this is a closure returned from GetNestedFunc which can't be updated)") 20 | return iter, false 21 | end 22 | end 23 | 24 | function module.GarbageTest() 25 | log("[OLD] GarbageTest is called, and it uses upvalue garbage, garbage.exists is", garbage.exists) 26 | return garbage 27 | end 28 | 29 | return module 30 | ]=] 31 | 32 | local file2 = [=[ 33 | local module = {} 34 | local iter = 0 35 | 36 | function module.FuncNew() 37 | iter = iter + 1 38 | log("[NEW] iter has been increased, it's", iter, "now") 39 | return iter, true 40 | end 41 | 42 | return module 43 | ]=] 44 | 45 | local obj = DoFileString(file1) 46 | local NestedFunc = obj.GetNestedFunc() 47 | 48 | AssertCall({ 1, false }, obj.Func()) 49 | AssertCall({ 2, false }, NestedFunc()) 50 | 51 | local garbage = obj.GarbageTest() 52 | assert(garbage.exists) 53 | 54 | -- store a week reference to garbage, it should be removed 55 | local ref = setmetatable({}, { __mode = "v" }) 56 | table.insert(ref, garbage) 57 | garbage = nil 58 | 59 | obj.Func = nil 60 | obj.GetNestedFunc = nil 61 | obj.GarbageTest = nil 62 | collectgarbage() 63 | collectgarbage() 64 | 65 | -- ensure "garbage" table was removed 66 | assert(#ref == 0) 67 | 68 | ReloadFileString(file2) 69 | log("*** File has been reloaded ***") 70 | 71 | assert(obj.Func == nil) 72 | assert(obj.GetNestedFunc == nil) 73 | AssertCall({ 3, true }, obj.FuncNew()) 74 | log("Anonymous function expected to be OLD:") 75 | AssertCall({ 4, false }, NestedFunc()) 76 | AssertCall({ 5, true }, obj.FuncNew()) 77 | -------------------------------------------------------------------------------- /test-suite/test_setup.lua: -------------------------------------------------------------------------------- 1 | -- LuaReload is left global intentionally 2 | LuaReload = dofile("../lua_reload.lua") 3 | LuaReload.SetPrintReloadingLogs(true) 4 | LuaReload.SetLogReferencesSteps(false) 5 | LuaReload.SetTraverseGlobals(true) 6 | LuaReload.SetTraverseRegistry(true) 7 | LuaReload.SetStoreReferencePath(true) 8 | LuaReload.Inject() 9 | 10 | dofile("utils.lua") 11 | 12 | local filesTimestamp = {} 13 | local timestamp = 0 14 | 15 | function GetTestFilename(postfix) 16 | return "testfile" .. (postfix and "_" .. postfix or "") .. ".lua" 17 | end 18 | 19 | function WriteString(text, filename) 20 | local file2 = io.open(filename, "w+") 21 | file2:write(text) 22 | file2:close() 23 | 24 | -- update the timestamp 25 | timestamp = timestamp + 1 26 | filesTimestamp[ filename ] = timestamp 27 | end 28 | 29 | function WriteFileString(text, postfix) 30 | local filename = GetTestFilename(postfix) 31 | WriteString(text, filename) 32 | end 33 | 34 | function DoFileString(text, postfix) 35 | local filename = GetTestFilename(postfix) 36 | WriteString(text, filename) 37 | return dofile(filename) 38 | end 39 | 40 | function LoadFileString(text, postfix) 41 | local filename = GetTestFilename(postfix) 42 | WriteString(text, filename) 43 | return loadfile(filename) 44 | end 45 | 46 | function ReloadFileString(text, postfix) 47 | WriteFileString(text, postfix) 48 | local fn = GetTestFilename(postfix) 49 | LuaReload.ReloadFile(fn) 50 | end 51 | 52 | function WriteFunction(func, filename) 53 | local file2 = io.open(filename, "w+") 54 | file2:write(string.dump(func)) 55 | file2:close() 56 | 57 | -- update the timestamp 58 | timestamp = timestamp + 1 59 | filesTimestamp[ filename ] = timestamp 60 | end 61 | 62 | function WriteFileFunc(func, postfix) 63 | local filename = GetTestFilename(postfix) 64 | WriteFunction(func, filename) 65 | end 66 | 67 | function LoadFileFunc(func, postfix) 68 | local filename = GetTestFilename(postfix) 69 | WriteFunction(func, filename) 70 | return loadfile(filename) 71 | end 72 | 73 | function DoFileFunc(func, postfix) 74 | local filename = GetTestFilename(postfix) 75 | WriteFunction(func, filename) 76 | return dofile(filename) 77 | end 78 | 79 | function ReloadFileFunc(func, postfix) 80 | WriteFileFunc(func, postfix) 81 | local fn = GetTestFilename(postfix) 82 | LuaReload.ReloadFile(fn) 83 | end 84 | 85 | LuaReload.FileGetTimestamp = function(filename) 86 | return filesTimestamp[ filename ] 87 | end -------------------------------------------------------------------------------- /test-suite/run_tests.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Run this file with LuaJIT to run the test suite. 3 | Note: LuaFileSystem is required, download from https://keplerproject.github.io/luafilesystem/ 4 | 5 | To run a specific test, or tests from a specific folder, 6 | provide the name of the file or the folder as an argument, e.g. 7 | > luajit run_tests.lua tests/env 8 | ]] 9 | dofile("utils.lua") 10 | require("lfs") 11 | 12 | local tests = {} 13 | local function traverse(dir) 14 | for entry in lfs.dir(dir) do 15 | if entry ~= "." and entry ~= ".." then 16 | entry = dir .. "/" .. entry 17 | local attr = lfs.attributes(entry) 18 | if attr.mode == "directory" then 19 | traverse(entry) 20 | elseif attr.mode == "file" and entry:sub(-3, -1) == "lua" then 21 | table.insert(tests, entry) 22 | end 23 | end 24 | end 25 | end 26 | 27 | local target = ({...})[1] or "tests" 28 | local attr = lfs.attributes(target) 29 | if not attr then 30 | Error(target, "doesn't exist") 31 | end 32 | 33 | if attr.mode == "file" then 34 | table.insert(tests, target) 35 | else 36 | traverse(target) 37 | end 38 | 39 | table.sort(tests) 40 | 41 | local succeded = {} 42 | local failed = {} 43 | local total = #tests 44 | 45 | local setfenv = setfenv 46 | if not setfenv then 47 | setfenv = function(chunk, env) 48 | local i = 1 49 | while true do 50 | local name = debug.getupvalue(chunk, i) 51 | if name == "_ENV" then 52 | debug.upvaluejoin(chunk, i, function() return env end, 1) 53 | break 54 | elseif not name then 55 | break 56 | end 57 | i = i + 1 58 | end 59 | return chunk 60 | end 61 | end 62 | 63 | local function MakeEnv() 64 | local env = {} 65 | env._G = env 66 | env.loadfile = function(...) 67 | local func, err = loadfile(...) 68 | if func then 69 | setfenv(func, env) 70 | end 71 | return func, err 72 | end 73 | env.dofile = function(...) 74 | local func = loadfile(...) 75 | if func then 76 | setfenv(func, env) 77 | else 78 | return dofile(...) 79 | end 80 | return func() 81 | end 82 | setmetatable(env, { __index = _G }) 83 | return env 84 | end 85 | 86 | for i = 1, total do 87 | log("\n\n\n*** Running test no.", i, "from file", tests[i], "***") 88 | local success, message 89 | local test, err = loadfile(tests[i]) 90 | 91 | if test then 92 | setfenv(test, MakeEnv()) 93 | success, message = xpcall(test, debug.traceback) 94 | else 95 | success, message = false, err 96 | end 97 | 98 | if success then 99 | table.insert(succeded, { file = tests[i] }) 100 | log("*** TEST SUCCEDED ***") 101 | else 102 | table.insert(failed, { file = tests[i], error = message }) 103 | log("Error:\n" .. tostring(message)) 104 | log("*** TEST FAILED ***") 105 | end 106 | end 107 | 108 | if #failed > 0 then 109 | log("\nfailed:") 110 | for _, entry in ipairs(failed) do 111 | log(" " .. entry.file .. " error: " .. (entry.error:match("([^\r\n]+)"))) 112 | end 113 | end 114 | 115 | log("\n\nsucceded:", #succeded, "\nfailed:", #failed, "\ntotal:", total) 116 | 117 | -- remove temporary files 118 | for entry in lfs.dir(".") do 119 | if entry ~= "." and entry ~= ".." then 120 | local attr = lfs.attributes(entry) 121 | if attr.mode == "file" and entry:find("testfile") == 1 then 122 | os.remove(entry) 123 | end 124 | end 125 | end 126 | 127 | if #failed > 0 then 128 | os.exit(1) 129 | end 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua Hot Reload library 2 | [![Build Status](https://travis-ci.org/anton-kl/lua-hot-reload.svg?branch=master)](https://travis-ci.org/anton-kl/lua-hot-reload) 3 | ![Lua](https://img.shields.io/badge/Lua-5.2%2C%205.3%2C%20JIT-blue) 4 | 5 | A single file module that allows you to hot reload Lua code in your project. 6 | 7 | Note: this project is **in the beta stage**, it may contain bugs. 8 | 9 | ## Requirements 10 | - LuaJIT or Lua 5.2+ 11 | - Optionally [LuaFileSystem](https://keplerproject.github.io/luafilesystem/) 12 | 13 | As for Lua5.1, the support is very limited, unless you provide `upvalueid` and `upvaluejoin` functions via a [patch](http://lua-users.org/lists/lua-l/2010-01/msg00914.html). 14 | 15 | LuaFileSystem is used in the integrated filesystem pooling loop which informs your game about file changes. In case you use Love2D, `love.filesystem` will be used. Note though, that filesystem pooling is slow, so for best performance you should use some filesystem watching library (e.g. [efsw](https://bitbucket.org/SpartanJ/efsw) in case of C or C++) 16 | 17 | ## Features 18 | 19 | 1. You can change the code of any file-scoped function, every instance of this function in the game will be updated immediately. Data links (upvalues) will be preserved, it will also work if a file has been loaded several times. 20 | 21 | 2. You can change values of the file-scoped variables, if they value didn't change since you loaded a file. Even if a file just returns a table with data - you can edit this table in a file and this table should be updated. 22 | 23 | ## Integration 24 | 25 | #### Step 1 26 | Add `lua_reload.lua` file to your project 27 | 28 | #### Step 2 29 | Load it and call the `Inject` function. You should do this as early as possible, since only files loaded after this call will be available for hot reloading. 30 | ```lua 31 | local luaReload = require("lua_reload") 32 | luaReload.Inject() 33 | ``` 34 | 35 | #### Step 3 36 | When you want to reload a certain file, just call the `ReloadFile` function: 37 | ```lua 38 | luaReload.ReloadFile(filename) 39 | ``` 40 | 41 | #### Step 4 42 | Hot reloading works best when you game detects file changes on the disk and reloads them automatically. The best option would be to integrate filesystem watching library, but for Love2D and projects that can use LuaFileSystem - there is an integrated filesystem pooling loop for fast setup. Call `luaReload.Monitor()` function several times per second, and luaReload will automatically reload any changed files. For Love2D the code could look like this: 43 | ```lua 44 | function love.update(dt) 45 | luaReload.Monitor() 46 | end 47 | ``` 48 | 49 | ## Limitations 50 | 51 | ### Execution of the file shouldn't have side effects 52 | 53 | This library doesn't parse the file, it uses debug library to get functions and data from the file. In order for a file to be reloaded, it has to be executed, so its execution should not break the game. 54 | 55 | ### Functions made inside other functions can't be updated after they are created 56 | 57 | If a function is created inside another function, each instance of it is unique. Only new instances of this function will have new code. If you want to update all instances of such a function in real time, you should make it a local file-scoped function. 58 | 59 | ### You shouldn't rename functions 60 | 61 | If you rename a function, or if you change the way how it can be retrieved from a file (e.g. it was in a table and you made it a local function), the library will think that old function has been removed and a new function has been added. 62 | 63 | ### Modifying global functions and data is WIP 64 | 65 | You can execute `LuaReload.SetHandleGlobalModules(true)` to enable reloading of the files which return nothing and only contain global functions, but this feature is limited and WIP. 66 | 67 | ## Test suite 68 | To run the test suite: 69 | 70 | 1. Install [LuaFileSystem](https://keplerproject.github.io/luafilesystem/) 71 | 2. Go to `test-suite` folder and execute `luajit run_tests.lua` 72 | 73 | To run a specific test, or tests from a specific folder, provide the name of the file or the folder as an argument, e.g. `luajit run_tests.lua tests/env` 74 | 75 | You can also install [inspect](https://luarocks.org/modules/kikito/inspect) for additional upvalue logs. 76 | -------------------------------------------------------------------------------- /test-suite/tests/test14.lua: -------------------------------------------------------------------------------- 1 | dofile("test_setup.lua") 2 | 3 | local file = [=[ 4 | local fn = {} 5 | local steps = 0 6 | function fn:Step() 7 | steps = steps + 1 8 | return 1, steps 9 | end 10 | return fn 11 | ]=] 12 | 13 | local obj = DoFileString(file) 14 | AssertCall({ 1, 1 }, obj:Step()) 15 | 16 | local file = [=[ 17 | local fn = {} 18 | local steps = 0 19 | function fn:Step() 20 | steps = steps + 1 21 | return 2, steps 22 | end 23 | function fn:GetSteps() 24 | return 2, steps 25 | end 26 | return fn 27 | ]=] 28 | 29 | DoFileString(file) 30 | AssertCall({ 2, 2 }, obj:Step()) 31 | AssertCall({ 2, 2 }, obj:GetSteps()) 32 | 33 | local file = [=[ 34 | local fn = {} 35 | local steps = 0 36 | local const = 10 37 | fn.const = 100 38 | function fn:Step() 39 | steps = steps + 1 40 | return 3, steps, const 41 | end 42 | function fn:GetSteps() 43 | return 3, steps 44 | end 45 | function fn:GetConst() 46 | return 3, const, fn.const 47 | end 48 | return fn 49 | ]=] 50 | 51 | DoFileString(file) 52 | AssertCall({ 3, 3, 10 }, obj:Step()) 53 | AssertCall({ 3, 3 }, obj:GetSteps()) 54 | AssertCall({ 3, 10, 100 }, obj:GetConst()) 55 | 56 | local file = [=[ 57 | local fn = {} 58 | local steps = 0 59 | local const = 10 60 | fn.const = 100 61 | local module = { 62 | GetSteps = function() 63 | return 4, steps 64 | end 65 | } 66 | function fn:Step() 67 | steps = steps + 1 68 | return 4, steps, const 69 | end 70 | function fn:GetSteps() 71 | return 4, steps, module.GetSteps() 72 | end 73 | function fn:GetConst() 74 | return 4, const, fn.const 75 | end 76 | return fn 77 | ]=] 78 | 79 | DoFileString(file) 80 | AssertCall({ 4, 4, 10 }, obj:Step()) 81 | AssertCall({ 4, 4, 4, 4 }, obj:GetSteps()) 82 | AssertCall({ 4, 10, 100 }, obj:GetConst()) 83 | 84 | local file = [=[ 85 | local fn = {} 86 | local steps = 0 87 | local const = 10 88 | fn.const = 100 89 | local module = { 90 | GetSteps = function() 91 | return steps 92 | end 93 | } 94 | function fn:Step() 95 | steps = steps + 1 96 | return { steps = steps, const = const } 97 | end 98 | function fn:GetSteps() 99 | return { steps = steps, moduleSteps = module.GetSteps() } 100 | end 101 | function fn:GetConst() 102 | return { const = const, fnConst = fn.const } 103 | end 104 | function fn:ReturnTable() 105 | return { a = 1, b = 2 } 106 | end 107 | return fn 108 | ]=] 109 | 110 | DoFileString(file) 111 | AssertCallTable({{ steps = 5, const = 10 }}, obj:Step()) 112 | AssertCallTable({{ steps = 5, moduleSteps = 5 }}, obj:GetSteps()) 113 | AssertCallTable({{ const = 10, fnConst = 100 }}, obj:GetConst()) 114 | print(obj) 115 | print(getmetatable(obj)) 116 | AssertCallTable({{ a = 1, b = 2 }}, obj:ReturnTable()) 117 | 118 | local file = [=[ 119 | local fn = {} 120 | local steps = 0 121 | local const = 10 122 | fn.const = 100 123 | local module = { 124 | GetSteps = function() 125 | return steps 126 | end 127 | } 128 | function fn:Step() 129 | steps = steps + 1 130 | return { steps = steps, const = const } 131 | end 132 | function fn:GetSteps() 133 | return { steps = steps, moduleSteps = module.GetSteps() } 134 | end 135 | function fn:GetConst() 136 | return { const = const, fnConst = fn.const } 137 | end 138 | function fn:ReturnFunc1() 139 | return function() 140 | return self:GetSteps() 141 | end 142 | end 143 | function fn:ReturnFunc2() 144 | return function() 145 | return self:GetSteps() 146 | end 147 | end 148 | return fn 149 | ]=] 150 | 151 | DoFileString(file) 152 | local f1 = obj:ReturnFunc1() 153 | local f2 = obj:ReturnFunc2() 154 | AssertCallTable({{ steps = 6, const = 10 }}, obj:Step()) 155 | AssertCallTable({{ steps = 6, moduleSteps = 6 }}, obj:GetSteps()) 156 | AssertCallTable({{ const = 10, fnConst = 100 }}, obj:GetConst()) 157 | 158 | local file = [=[ 159 | local fn = {} 160 | local steps = 0 161 | local const = 10 162 | fn.const = 100 163 | local module = { 164 | GetSteps = function() 165 | return steps 166 | end 167 | } 168 | function fn:Step() 169 | steps = steps + 1 170 | return { steps = steps, const = const } 171 | end 172 | function fn:GetSteps() 173 | return { steps = steps, moduleSteps = module.GetSteps() } 174 | end 175 | function fn:GetConst() 176 | return { const = const, fnConst = fn.const } 177 | end 178 | function fn:ReturnFunc1() 179 | return function() 180 | return self:GetSteps() 181 | end 182 | end 183 | function fn:ReturnFunc2() 184 | return function() 185 | return self:GetSteps() 186 | end 187 | end 188 | return fn 189 | ]=] 190 | 191 | DoFileString(file) 192 | AssertCallTable({{ steps = 7, const = 10 }}, obj:Step()) 193 | AssertCallTable({{ steps = 7, moduleSteps = 7 }}, obj:GetSteps()) 194 | AssertCallTable({{ const = 10, fnConst = 100 }}, obj:GetConst()) 195 | -------------------------------------------------------------------------------- /lua_reload.lua: -------------------------------------------------------------------------------- 1 | -- This code requires major refactoring. 2 | -- Also see TODOs 3 | local module = {} 4 | 5 | -- if "inspect.lua" is accessible - load it 6 | local exists, inspect = pcall( require, "inspect" ) 7 | if not exists then inspect = nil end 8 | 9 | -- original function are set in the Inject() function 10 | local setfenvOriginal 11 | local loadfileOriginal 12 | local requireOriginal 13 | 14 | local fileCache = {} 15 | local fileIndexCounter = 1 16 | 17 | local traverseRegistry = true 18 | local traverseGlobals = true 19 | local trackGlobals = false 20 | local globalsAccessed = {} 21 | local handleGlobalModules = false 22 | 23 | -- logging related flags 24 | local log = true 25 | local logCacheAccess = true 26 | local logReferencesSteps = false 27 | local storeReferencePath = false 28 | local logUpvalues = false 29 | local logGlobalAccess = true 30 | local logTrace = false 31 | 32 | -- if error happened during loading of the file, 33 | -- but the old working version of the file is available, use it 34 | local useOldFileOnError = true 35 | local retrieveUpvaluesFromNestedFunctions = true 36 | -- getinfo as an additional source of information about functions, is used only 37 | -- for Lua5.2+ where we can't set ENV for every function (functions which 38 | -- doesn't use globals doesn't have ENVs) 39 | local useGetInfo = _ENV ~= nil 40 | -- In vanilla Lua, if a chunk creates a function, which doesn't have any upvalues 41 | -- (e.g. just return a result of some calculations), then even if execute this 42 | -- chunk several times, we may get exactly the same function, down to its address. 43 | -- This make it practically impossible to determine if two functions are coming 44 | -- from different versions of the file, or not. Thus each time we are about 45 | -- to execute the chunk, we should make it from scratch first. Example of the issue: 46 | --[[ 47 | function Func() 48 | return function() return 100 end 49 | end 50 | print(Func() == Func()) -- true on vanilla Lua 51 | ]] 52 | local loadFileFromSource = type(jit) ~= 'table' 53 | -- set to false to always assume file has been changed when loading/reloading 54 | local enableTimestampCheck = true 55 | -- set to false to always load file from the disk ignoring cache 56 | local useCache = true 57 | 58 | local Reload -- defined below 59 | local reloading = false 60 | local toBeReload = {} 61 | local loadedFilesList = {} 62 | local returnValuesMt = { __mode = "v" } 63 | local customErrorHandler = nil 64 | local functionSource = setmetatable({}, { __mode = "k" }) 65 | 66 | local visitedGlobal = {} 67 | local queuePreallocationSize = 40000 68 | local visitedPreallocationSize = 40000 69 | 70 | local debug = debug 71 | local getinfo = debug.getinfo 72 | local getlocal = debug.getlocal 73 | local getupvalue = debug.getupvalue 74 | local upvalueid = debug.upvalueid 75 | local setlocal = debug.setlocal 76 | local setupvalue = debug.setupvalue 77 | local upvaluejoin = debug.upvaluejoin 78 | 79 | -- default handler just prints the error message, but you may want 80 | -- to abort executing if isReloading is set to false, which means 81 | -- that this is the first time we load this file, so we can't use 82 | -- an older version of it 83 | local function HandleLoadError(fileName, errorMessage, isReloading) 84 | if customErrorHandler then 85 | customErrorHandler(fileName, errorMessage, isReloading) 86 | else 87 | print("ERROR during loading of the " .. fileName .. ": " .. errorMessage .. ". " .. debug.traceback()) 88 | end 89 | end 90 | 91 | local reloadTimes = 0 92 | local function Log(...) 93 | local num = select("#", ...) 94 | local arg = {...} 95 | local prefix = "[LUAR]" .. (reloading and "[#" .. reloadTimes .. "]" or "") .. " " 96 | local msg = prefix 97 | for i = 1, num do 98 | msg = msg .. tostring(arg[i]) .. " " 99 | end 100 | print( (msg:gsub("\n", "\n" .. prefix)) ) 101 | end 102 | local function Error(...) 103 | local num = select("#", ...) 104 | local arg = {...} 105 | local msg = "" 106 | for i = 1, num do 107 | msg = msg .. tostring(arg[i]) .. " " 108 | end 109 | error(msg) 110 | end 111 | 112 | local _GOriginal = _G 113 | local envMetatable = { 114 | __index = function(t, k) 115 | if trackGlobals then 116 | globalsAccessed[k] = true 117 | if log and logGlobalAccess then Log(" get global", k, "value: [", _GOriginal[k], "]") end 118 | end 119 | return _GOriginal[k] 120 | end, 121 | __newindex = function(t, k, v) 122 | if trackGlobals then 123 | globalsAccessed[k] = true 124 | if log and logGlobalAccess then Log(" set global", k, "from: [", _GOriginal[k], "] to: [", v, "]") end 125 | end 126 | _GOriginal[k] = v 127 | end 128 | } 129 | 130 | local function GetFuncDesc(func) 131 | local info = getinfo(func) 132 | return info.func, "defined in", info.short_src, "at", info.linedefined, "-", info.lastlinedefined 133 | end 134 | 135 | -- see https://leafo.net/guides/setfenv-in-lua52-and-above.html 136 | local setfenv = setfenv or function(func, env) 137 | local i = 1 138 | while true do 139 | local name = getupvalue(func, i) 140 | if name == "_ENV" then 141 | upvaluejoin(func, i, function() return env end, 1) 142 | break 143 | elseif not name then 144 | break 145 | end 146 | i = i + 1 147 | end 148 | return func 149 | end 150 | 151 | local getfenv = getfenv or function(func) 152 | local i = 1 153 | while true do 154 | local name, val = getupvalue(func, i) 155 | if name == "_ENV" then 156 | return val 157 | elseif not name then 158 | break 159 | end 160 | i = i + 1 161 | end 162 | end 163 | 164 | local setfenvNew = function(target, env) 165 | local oldEnv = getfenv(target) 166 | local _sourceFileIndex = rawget(oldEnv, "_sourceFileIndex") 167 | local _sourceFileName = rawget(oldEnv, "_sourceFileName") 168 | local _sourceFileIndex_current = rawget(env, "_sourceFileIndex") 169 | local _sourceFileName_current = rawget(env, "_sourceFileName") 170 | if (_sourceFileIndex_current ~= nil and _sourceFileIndex_current ~= _sourceFileIndex) 171 | or (_sourceFileName_current ~= nil and _sourceFileName_current ~= _sourceFileName) 172 | then 173 | -- TODO add a test case for this 174 | error("You set the environment of two functions defined in a different files to the same table.\n" .. 175 | "Unfortunately, this is not supported. In the environment of a functions, we store file name\n" .. 176 | "and file index - and without this information it is impossible to update a function properly.\n" .. 177 | "we could use getinfo().short_src, but it is slower, pollutes stacktraces, and more trickier\n" .. 178 | "to include \"file index\" in. Consider doing shallow-copy of the table, and use it as an environment.") 179 | end 180 | rawset(env, "_sourceFileIndex", _sourceFileIndex) 181 | rawset(env, "_sourceFileName", _sourceFileName) 182 | setfenvOriginal(target, env) 183 | end 184 | 185 | local function StoreReturnValues(file, fileIndex, ...) 186 | local number = select("#", ...) 187 | for i = 1, number do 188 | local value = select(i, ...) 189 | local vtype = type(value) 190 | if vtype == "table" or vtype == "function" then 191 | file.returnValues[ { 192 | fileIndex = fileIndex, 193 | valueIndex = i 194 | } ] = value 195 | end 196 | end 197 | return ... 198 | end 199 | 200 | local function UpdateReturnValues(file, fileIndex, ...) 201 | -- Tables created in new versions of a file never get references in the game, 202 | -- instead of copying references to tables - we just transfer the data from 203 | -- new tables to respective old tables (we match them based on return values 204 | -- order and some other factors). But new functions are obviously get 205 | -- referenced in the game, and we update references everywhere in the game, 206 | -- we don't do this for returnValues tables (we explicitly skip them while 207 | -- searching for references, this is why we have to manually remove old 208 | -- functions references in the returnValues table) 209 | local returnValues = file.returnValues 210 | for data, value in pairs(returnValues) do 211 | if log then Log("Analyze return value no.", data.valueIndex, "for fileIndex", fileIndex) end 212 | if data.fileIndex == fileIndex 213 | and type(value) == "function" 214 | then 215 | local newValue = select(data.valueIndex, ...) 216 | if log then Log(" Update this old function from", returnValues[data], "to", newValue) end 217 | returnValues[data] = newValue 218 | end 219 | end 220 | return ... 221 | end 222 | 223 | local function Pack(...) 224 | return { n = select("#", ...), ... } 225 | end 226 | 227 | local function SetupChunkEnv(chunk, fileName, fileIndex) 228 | local env = { 229 | _sourceFileIndex = fileIndex, 230 | _sourceFileName = fileName 231 | } 232 | setfenvOriginal(chunk, setmetatable(env, envMetatable)) 233 | end 234 | 235 | local function ScheduleReload(fileName) 236 | local trace = logTrace and debug.traceback() or "" 237 | if not toBeReload[fileName] then 238 | if log then Log("SCHEDULE", fileName, "to be reloaded", trace) end 239 | toBeReload[fileName] = true 240 | else 241 | if log then Log(fileName, "is already scheduled to be reloaded", trace) end 242 | end 243 | end 244 | 245 | local function ReloadFile(fileName) 246 | if reloading then 247 | ScheduleReload(fileName) 248 | return false 249 | end 250 | local file = fileCache[fileName] 251 | assert(file, "DEV ERROR: Reloading a file which doesn't exist in the cache") 252 | 253 | local chunk, errorMessage = loadfileOriginal(fileName) 254 | 255 | if not chunk then 256 | HandleLoadError(fileName, errorMessage, true) 257 | return false, errorMessage 258 | end 259 | 260 | reloading = true 261 | Reload(fileName, file.chunk, chunk, file.returnValues) 262 | reloading = false 263 | 264 | file.chunk = chunk 265 | file.timestamp = module.FileGetTimestamp(fileName) 266 | 267 | return true 268 | end 269 | 270 | local function ReloadScheduledFiles(files) 271 | if reloading then return end 272 | 273 | -- reload all pending files 274 | while next(toBeReload) do 275 | local fileName, _ = next(toBeReload) 276 | toBeReload[fileName] = nil 277 | 278 | ReloadFile(fileName) 279 | end 280 | end 281 | 282 | local loadfileInternal = function(fileName) 283 | local file = fileCache[fileName] 284 | local errorMessage 285 | local timestamp = module.FileGetTimestamp(fileName) 286 | 287 | if file and (not enableTimestampCheck or timestamp > file.timestamp) and module.ShouldReload(fileName) then 288 | if reloading then 289 | -- schedule to reload 290 | ScheduleReload(fileName) 291 | else 292 | -- reload 293 | local _ 294 | _, errorMessage = ReloadFile(fileName) 295 | -- reload any files that may have been scheduled for reloading 296 | -- during reloading of the above file 297 | ReloadScheduledFiles() 298 | end 299 | elseif file and enableTimestampCheck and useCache and timestamp == file.timestamp then 300 | -- load from cache (if timestamp check is disabled always load from the file) 301 | if logCacheAccess then Log("Loading", fileName, "from cache") end 302 | else 303 | -- load from file 304 | if log then 305 | local trace = logTrace and debug.traceback() or "" 306 | Log("Loading", fileName, trace) 307 | end 308 | table.insert(loadedFilesList, fileName) 309 | local chunk 310 | chunk, errorMessage = loadfileOriginal(fileName) 311 | if not chunk then 312 | HandleLoadError(fileName, errorMessage, false) 313 | else 314 | file = { 315 | chunk = chunk, 316 | timestamp = timestamp, 317 | returnValues = setmetatable({}, returnValuesMt) 318 | } 319 | fileCache[fileName] = file 320 | end 321 | end 322 | 323 | return file, errorMessage 324 | end 325 | 326 | local loadfileNew = function(fileName) 327 | -- load the file into the cache (or get from the cache) 328 | -- also reload it automatically if it has changed 329 | local file, errorMessage = loadfileInternal(fileName) 330 | 331 | if file and (not errorMessage or useOldFileOnError) then 332 | -- we do not return chunk directly, since each time it is executed, 333 | -- we want to setup a new ENV for it, so we return a function which 334 | -- does exactly that: executes the chunk, and setups the proper ENV 335 | return function(...) 336 | local fileIndex = fileIndexCounter 337 | fileIndexCounter = fileIndexCounter + 1 338 | 339 | local chunk = file.chunk 340 | if loadFileFromSource then 341 | if errorMessage then 342 | local fileNameTmp = fileName .. "RELOADTMP" 343 | os.rename(fileName, fileNameTmp) 344 | 345 | local handle = io.open(fileName, "w+") 346 | handle:write(string.dump(file.chunk, false)) 347 | io.close(handle) 348 | 349 | chunk = loadfileOriginal(fileName) 350 | 351 | os.remove(fileName) 352 | os.rename(fileNameTmp, fileName) 353 | else 354 | -- TODO if file is scheduled to reload, we may load a newer 355 | -- version of the file here, so the file will exists in 356 | -- different versions in the system, which is wrong 357 | chunk = loadfileOriginal(fileName) 358 | end 359 | end 360 | SetupChunkEnv(chunk, fileName, fileIndex) 361 | if reloading then 362 | -- during reloading we are may load files, but their return 363 | -- values should not be referenced in the game, so we avoid 364 | -- storing them because they won't be used 365 | -- TODO return values may still be written into global variables 366 | -- how should we handle this? 367 | return chunk(...) 368 | else 369 | return StoreReturnValues(file, fileIndex, chunk(...)) 370 | end 371 | end 372 | else 373 | -- if file loading failed, and we didn't abort yet, 374 | -- assume user code will handle nil and error message properly 375 | return nil, errorMessage 376 | end 377 | end 378 | 379 | local function dofileNew(fileName) 380 | local func, msg = loadfileNew(fileName) 381 | assert(func, msg) 382 | return func() 383 | end 384 | 385 | local function requireNew(modname) 386 | local function DoesFileExist(fileName) 387 | local file = io.open(fileName, "r") 388 | if file ~= nil then 389 | io.close(file) 390 | return true 391 | else 392 | return false 393 | end 394 | end 395 | 396 | local fileName = modname:gsub("%.", "/") .. ".lua" 397 | if DoesFileExist(fileName) and not fileCache[fileName] then 398 | local func, msg = loadfileNew(fileName) 399 | assert(func, msg) 400 | local result = func(modname, fileName) 401 | package.loaded[modname] = result 402 | return result 403 | else 404 | return requireOriginal(modname) 405 | end 406 | end 407 | 408 | local function FindReferences(fileName) 409 | local references = {} 410 | 411 | if log then Log("[ref_search] Searching for references started") end 412 | 413 | -- debug/performance flags 414 | -- storePath - store full path for every value as a string, involves 415 | -- a lot of string allocations, should be used for debugging purposes only 416 | local storePath = false 417 | local calculateMaxDepth = false 418 | local traverseLocals = true 419 | local traverseEnvs = true 420 | local traverseGlobals = traverseGlobals == nil and true or traverseGlobals 421 | local traverseRegistry = traverseRegistry == nil and true or traverseRegistry 422 | 423 | -- initialization 424 | local preallocateTable = module.PreallocateTable 425 | local queueValue = preallocateTable and preallocateTable(queuePreallocationSize, 0) or {} 426 | local queuePrevious = preallocateTable and preallocateTable(queuePreallocationSize, 0) or {} 427 | local queueName = preallocateTable and preallocateTable(queuePreallocationSize, 0) or {} 428 | local queueType = preallocateTable and preallocateTable(queuePreallocationSize, 0) or {} 429 | -- to store info about the local variable, for locals only 430 | -- TODO refactor it to not consist of tables, check GC usage 431 | local queueLink = preallocateTable and preallocateTable(queuePreallocationSize, 0) or {} 432 | local queuePath = {} -- for debug purposes only 433 | local size = 0 434 | local visited = preallocateTable and preallocateTable(0, visitedPreallocationSize) or {} 435 | visited[fileCache] = true 436 | visited[fileCache[fileName]] = true 437 | visited[functionSource] = true 438 | visited[FindReferences] = true 439 | if not traverseGlobals then 440 | visited[_G] = true 441 | visited[_GOriginal] = true 442 | end 443 | for k, v in pairs(visitedGlobal) do 444 | visited[k] = v 445 | end 446 | 447 | local function GetReferencePath(ptr) 448 | local path = {} 449 | local prev = ptr 450 | local length = 0 451 | while prev do 452 | table.insert(path, prev) 453 | prev = queuePrevious[prev] 454 | length = length + 1 455 | end 456 | 457 | local pathText = {} 458 | for i = length, 1, -1 do 459 | if i ~= 1 and type(queueValue[path[i]]) == "function" then 460 | local source = getinfo(queueValue[path[i]], "S").source 461 | table.insert(pathText, tostring(queueName[path[i]]) .. "->[upvalues in " .. tostring(source) .. "]") 462 | else 463 | table.insert(pathText, tostring(queueName[path[i]])) 464 | end 465 | end 466 | return table.concat(pathText, "/") 467 | end 468 | 469 | -- capture locals 470 | local stackLevel = 4 -- ignore getLocals(1), FindReferences(2) and Reload(3) 471 | while traverseLocals do 472 | local info = getinfo(stackLevel, "S") 473 | if not info then break end 474 | local localId = 1 475 | while true do 476 | local ln, lv = getlocal(stackLevel, localId) 477 | if ln ~= nil then 478 | size = size + 1 479 | queueValue[size] = lv 480 | queuePrevious[size] = nil 481 | queueName[size] = "[locals in " .. info.short_src .. "]/" .. tostring(ln) 482 | queueType[size] = type(lv) 483 | queueLink[size] = { 484 | stackLevel = stackLevel - 2, -- TODO fix this magic value 485 | localId = localId 486 | } 487 | if storePath then queuePath[size] = queueName[size] end 488 | else 489 | break 490 | end 491 | localId = localId + 1 492 | end 493 | stackLevel = stackLevel + 1 494 | end 495 | 496 | -- capture globals 497 | if traverseGlobals then 498 | size = size + 1 499 | queueValue[size] = _G 500 | queuePrevious[size] = nil 501 | queueName[size] = "globals(_G)" 502 | queueType[size] = type(_G) 503 | if storePath then queuePath[size] = queueName[size] end 504 | end 505 | 506 | if traverseRegistry then 507 | local registry = debug.getregistry() 508 | size = size + 1 509 | queueValue[size] = registry 510 | queuePrevious[size] = nil 511 | queueName[size] = "lua_registry" 512 | queueType[size] = type(registry) 513 | if storePath then queuePath[size] = queueName[size] end 514 | end 515 | 516 | -- loop 517 | local depthMax = calculateMaxDepth and 0 or nil 518 | local ptr = 0 519 | while ptr < size do 520 | ptr = ptr + 1 521 | local currentValue = queueValue[ptr] 522 | local currentType = type(currentValue) 523 | local currentPath = storePath and queuePath[ptr] 524 | 525 | if calculateMaxDepth then 526 | local depthCurrent = 0 527 | local prev = ptr 528 | while prev do 529 | depthCurrent = depthCurrent + 1 530 | prev = queuePrevious[prev] 531 | end 532 | depthMax = math.max(depthCurrent, depthMax) 533 | end 534 | 535 | if logReferencesSteps then 536 | local pathText = GetReferencePath(ptr) 537 | local definedIn = "" 538 | if currentType == "function" then 539 | local fileName 540 | if useGetInfo then 541 | local info = functionSource[currentValue] 542 | if not info then 543 | info = getinfo(currentValue, "S") 544 | functionSource[currentValue] = info 545 | end 546 | fileName = info.short_src 547 | else 548 | fileName = getfenv(currentValue)._sourceFileName 549 | end 550 | if fileName then 551 | definedIn = "defined in " .. tostring(fileName) 552 | end 553 | end 554 | Log("[ref_search] step", tostring(ptr), pathText, "=", tostring(currentValue), definedIn) 555 | end 556 | if currentType == "table" and not visited[currentValue] then 557 | visited[currentValue] = true 558 | local i = 0 559 | for k, v2 in pairs(currentValue) do 560 | local t = type(v2) 561 | if t == "table" or t == "function" then 562 | i = i + 1 563 | size = size + 1 564 | queueValue[size] = v2 565 | queuePrevious[size] = ptr 566 | queueName[size] = k 567 | queueLink[size] = { 568 | owner = currentValue, 569 | key = k 570 | } 571 | if storePath then queuePath[size] = currentPath .. "/" .. tostring(k) end 572 | end 573 | local t = type(k) 574 | if t == "table" or t == "function" then 575 | i = i + 1 576 | size = size + 1 577 | queueValue[size] = k 578 | queuePrevious[size] = ptr 579 | queueName[size] = k 580 | queueLink[size] = { 581 | owner = currentValue 582 | } 583 | if storePath then queuePath[size] = currentPath .. "/" .. tostring(k) end 584 | end 585 | end 586 | local mt = getmetatable(currentValue) 587 | if mt then 588 | size = size + 1 589 | queueValue[size] = mt 590 | queuePrevious[size] = ptr 591 | queueName[size] = "mt" 592 | if storePath then queuePath[size] = currentPath .. "/mt" end 593 | end 594 | elseif currentType == "function" then 595 | local target = false 596 | local env = getfenv(currentValue) 597 | target = env and env._sourceFileName == fileName 598 | if not env and useGetInfo then 599 | local info = functionSource[currentValue] 600 | if not info then 601 | info = getinfo(currentValue, "S") 602 | functionSource[currentValue] = info 603 | end 604 | target = info.short_src == fileName 605 | end 606 | if target then 607 | local index = env and env._sourceFileIndex or 0 608 | local list = references[index] or {} 609 | table.insert(list, { 610 | path = storePath and currentPath or (storeReferencePath and GetReferencePath(ptr)), 611 | value = currentValue, 612 | link = queueLink[ptr] 613 | }) 614 | references[index] = list 615 | end 616 | 617 | if not visited[currentValue] then 618 | if traverseEnvs and env ~= _GOriginal and env ~= envMetatable then 619 | size = size + 1 620 | queueValue[size] = env 621 | queuePrevious[size] = ptr 622 | queueName[size] = "ENV" 623 | if storePath then queuePath[size] = currentPath .. "/" .. queueName[size] end 624 | end 625 | 626 | visited[currentValue] = true 627 | local i = 1 628 | while true do 629 | local ln, lv = getupvalue(currentValue, i) 630 | if ln ~= nil then 631 | local t = type(lv) 632 | if t == "table" or t == "function" then 633 | size = size + 1 634 | queueValue[size] = lv 635 | queuePrevious[size] = ptr 636 | queueName[size] = ln 637 | queueLink[size] = { 638 | owner = currentValue, 639 | upvalueId = i 640 | } 641 | if storePath then queuePath[size] = currentPath .. "/" .. tostring(ln) end 642 | end 643 | else 644 | break 645 | end 646 | i = i + 1 647 | end 648 | end 649 | end 650 | if log and ptr % 100000 == 0 then 651 | Log("[ref_search] did ", ptr, " steps") 652 | end 653 | end 654 | 655 | if log then Log("[ref_search] Finished in", ptr, "steps") end 656 | 657 | return references, visited 658 | end 659 | 660 | -- TODO use queueName instead of storing the whole path 661 | local function traverse(data, fileName, visitedDuringSearch) 662 | if log then Log("-> traverse", fileName) end 663 | local storePath = log 664 | local logContent = log and false 665 | local logPush = log and false 666 | 667 | local functions = {} 668 | local upvalues = {} 669 | local tables = {} 670 | 671 | local queue = {} 672 | local queuePrevious = {} 673 | local queueLink = {} 674 | local queuePath = {} 675 | local visited = { 676 | [_G] = true, -- do not traverse _G which is found in ENVs of the functions 677 | [_GOriginal] = true 678 | } 679 | for k, v in pairs(visitedGlobal) do 680 | visited[k] = v 681 | end 682 | local size = 0 683 | local function push(obj, name, previous, link) 684 | size = size + 1 685 | queue[size] = obj 686 | queuePrevious[size] = previous 687 | queueLink[size] = link 688 | if storePath then queuePath[size] = (queuePath[previous] or "") .. "/[" .. tostring(name) .. "]" end 689 | if logPush then Log(" push [", obj, "] at", size) end 690 | if type(obj) == "table" then 691 | visited[obj] = true 692 | end 693 | end 694 | push(data, "data") 695 | 696 | local ptr = 0 697 | while ptr < size do 698 | ptr = ptr + 1 699 | local currentValue = queue[ptr] 700 | local currentType = type(currentValue) 701 | if storePath then Log(ptr .. ".", queuePath[ptr], "= [", currentValue, "]") end 702 | 703 | if currentType == "function" then 704 | -- For LuaJIT: 705 | -- Do not traverse a function, if it was defined before the file was 706 | -- loaded, cause it 100% doesn't come from the file we loaded 707 | -- For vanilla Lua: 708 | -- Remember that function with no ENV may have exactly the same 709 | -- address in vanilla Lua, so we have to traverse a function even if 710 | -- it seems like it existed before we loaded the file 711 | if not visitedDuringSearch[currentValue] 712 | or (useGetInfo and not getfenv(currentValue)) 713 | then 714 | local target = false 715 | local env = getfenv(currentValue) 716 | if useGetInfo then 717 | local info = functionSource[currentValue] 718 | if not info then 719 | info = getinfo(currentValue, "S") 720 | functionSource[currentValue] = info 721 | end 722 | target = info.short_src == fileName 723 | else 724 | target = env and env._sourceFileName == fileName 725 | end 726 | if target then 727 | local info = functionSource[currentValue] 728 | if not info then 729 | info = getinfo(currentValue, "S") 730 | functionSource[currentValue] = info 731 | end 732 | local linedefined = info.linedefined 733 | if functions[linedefined] then 734 | -- TODO print warning if two functions are defined at the same line 735 | table.insert(functions[linedefined], ptr) 736 | else 737 | functions[linedefined] = { ptr } 738 | end 739 | 740 | -- Note: unlike tables, we push functions even _if they were 741 | -- visited_, in order to store all routes to them (see above code) 742 | -- this is why we have to check here if we didn't already 743 | -- visited this functions, and only then mark it as visited 744 | if not visited[currentValue] then 745 | visited[currentValue] = true 746 | local i = 1 747 | while true do 748 | local ln, lv = getupvalue(currentValue, i) 749 | if ln ~= nil then 750 | local upvalueid = upvalueid(currentValue, i) 751 | if upvalues[ln] and upvalues[ln].id ~= upvalueid then 752 | Error("Two different upvalues with the same name found, please rename one of them. Upvalue `" 753 | .. tostring(ln) .. "` referenced in", GetFuncDesc(upvalues[ln].func), 754 | "and", GetFuncDesc(currentValue)) 755 | end 756 | if logContent then Log(" - upvalue [", ln, "] = [", lv, "]") end 757 | upvalues[ln] = { 758 | id = upvalueid, 759 | func = currentValue, 760 | index = i, 761 | value = lv 762 | } 763 | local vtype = type(lv) 764 | if vtype == "function" or (vtype == "table" and not visited[lv]) then 765 | -- TODO optimize out the string concatenation 766 | push(lv, "upvalue " .. ln, ptr, { upvalueName = ln, upvalueIndex = i }) 767 | end 768 | else 769 | break 770 | end 771 | i = i + 1 772 | end 773 | end 774 | end 775 | if not visited[env] and env ~= envMetatable then 776 | push(env, "env", ptr, { env = currentValue }) 777 | end 778 | end 779 | elseif currentType == "table" then 780 | tables[currentValue] = ptr 781 | -- TODO traverse keys? 782 | for k, v in pairs(currentValue) do 783 | if logContent then Log(" - pair [", k, "] = [", v, "]") end 784 | local vtype = type(v) 785 | if vtype == "function" or (vtype == "table" and not visited[v]) then 786 | push(v, k, ptr, { key = k }) 787 | end 788 | end 789 | -- loading a file should create any tables with metatables, because if it is, 790 | -- then file executes "setmetable", which isn't nice 791 | local mt = getmetatable(currentValue) 792 | if mt and not visited[mt] then 793 | if logContent then Log(" - metatable", mt) end 794 | push(mt, "metatable", ptr, { metatable = true }) 795 | end 796 | end 797 | end 798 | 799 | if log then Log("traverse completed in", size, "steps") end 800 | 801 | if log and inspect then 802 | Log("Upvalues data:") 803 | Log(inspect(upvalues)) 804 | end 805 | 806 | return { 807 | queue = queue, 808 | queueLink = queueLink, 809 | queuePath = storePath and queuePath or nil, 810 | queuePrevious = queuePrevious, 811 | functions = functions, 812 | upvalues = upvalues, 813 | tables = tables 814 | } 815 | end 816 | 817 | local function SeparateReferencesByUpvalues(references, returnValuesByIndex) 818 | local upvalueidToFunc = {} 819 | local functionToIndex = {} 820 | 821 | if log then Log("Creating a map [function : fileIndex] based on captured return values") end 822 | for index, _returnValues in pairs(returnValuesByIndex) do 823 | if not references[index] then 824 | if log then Log(" Traverse return values for fileIndex no.", index, "in order to look for functions") end 825 | local queue = { _returnValues } 826 | local size = 1 827 | local visited = { 828 | [_G] = true, 829 | [_GOriginal] = true, 830 | [_returnValues] = true, 831 | } 832 | local validType = { 833 | ["table"] = true, 834 | ["function"] = true 835 | } 836 | local ptr = 0 837 | while ptr < size do 838 | ptr = ptr + 1 839 | local value = queue[ptr] 840 | local vtype = type(value) 841 | if vtype == "table" then 842 | for k, v in pairs(value) do 843 | if validType[type(k)] and not visited[k] then 844 | size = size + 1 845 | queue[size] = k 846 | visited[k] = true 847 | end 848 | if validType[type(v)] and not visited[v] then 849 | size = size + 1 850 | queue[size] = v 851 | visited[v] = true 852 | end 853 | end 854 | elseif vtype == "function" then 855 | if log then Log(" ", GetFuncDesc(value), "is associated with fileIndex", index) end 856 | functionToIndex[value] = index 857 | visited[value] = true 858 | 859 | local i = 1 860 | while true do 861 | local name, v = getupvalue(value, i) 862 | if name == nil then break end 863 | 864 | local id = upvalueid(value, i) 865 | upvalueidToFunc[id] = value 866 | 867 | if validType[type(v)] and not visited[v] then 868 | size = size + 1 869 | queue[size] = v 870 | visited[v] = true 871 | end 872 | i = i + 1 873 | end 874 | end 875 | end 876 | end 877 | end 878 | 879 | if log then Log("Separating functions into buckets based on their upvalues") end 880 | local refList = references[0] 881 | references[0] = nil 882 | local index = 0 883 | 884 | for fileIndex, list in pairs(references) do 885 | for _, ref in ipairs(list) do 886 | local func = ref.value 887 | functionToIndex[func] = fileIndex 888 | 889 | if log then Log(" ", GetFuncDesc(func), "is associated with file index", fileIndex) end 890 | 891 | local i = 1 892 | while true do 893 | if getupvalue(func, i) == nil then 894 | break 895 | end 896 | local id = upvalueid(func, i) 897 | upvalueidToFunc[id] = func 898 | i = i + 1 899 | end 900 | end 901 | end 902 | 903 | for _, ref in ipairs(refList) do 904 | local func = ref.value 905 | local myIndexes = {} 906 | 907 | if log then Log(" Analyzing", GetFuncDesc(func)) end 908 | 909 | local myIndex = functionToIndex[func] 910 | if myIndex then 911 | myIndexes[myIndex] = true 912 | if log then Log(" This function's fileIndex was deduced based on captured return values, and it is", myIndex) end 913 | else 914 | -- no deduced index for this function, try to find shared upvalues 915 | -- with other functions which do have fileIndex associated 916 | local i = 1 917 | while true do 918 | if getupvalue(func, i) == nil then 919 | break 920 | end 921 | 922 | local id = upvalueid(func, i) 923 | if upvalueidToFunc[id] then 924 | local myIndex = functionToIndex[upvalueidToFunc[id]] 925 | myIndexes[myIndex] = true 926 | if log then Log(" Upvalue with id", id, "is referred to the bucket no.", myIndex) end 927 | else 928 | upvalueidToFunc[id] = func 929 | if log then Log(" Upvalue with id", id, "seems to be new") end 930 | end 931 | i = i + 1 932 | end 933 | 934 | local myFirstIndex = next(myIndexes) 935 | if myFirstIndex == nil then 936 | -- negative file indexes are used for these temporal buckets, to avoid a conflict with real fileIndexes 937 | index = index - 1 938 | myIndex = index 939 | if log then Log(" All upvalues were new, introducing a new bucket with id", myIndex) end 940 | else 941 | myIndex = myFirstIndex 942 | if log then Log(" This function shared upvalues with functions in the bucket no.", myIndex) end 943 | for i, _ in pairs(myIndexes) do 944 | if i ~= myIndex then 945 | for _, ref in ipairs(references[i]) do 946 | table.insert(references[myIndex], ref) 947 | end 948 | if log then Log(" Moving all functions from bucket no.", i, "to above bucket,", 949 | "since those functions are sharing upvalues") end 950 | references[i] = nil 951 | end 952 | end 953 | end 954 | end 955 | functionToIndex[func] = myIndex 956 | references[myIndex] = references[myIndex] or {} 957 | table.insert(references[myIndex], ref) 958 | end 959 | end 960 | 961 | local function GetValueByRoute(routeStart, fileData1, traverseStartingPoint) 962 | -- build route for this reference 963 | local route = {} 964 | local prev = routeStart 965 | while prev do 966 | table.insert(route, fileData1.queueLink[prev]) 967 | prev = fileData1.queuePrevious[prev] 968 | end 969 | 970 | -- print route 971 | if log then 972 | Log(" route:", #route, "steps") 973 | for i = #route, 1, -1 do 974 | local step = route[i] 975 | local msg = " " .. tostring(#route - i + 1) .. "." 976 | for k,v in pairs(step) do 977 | msg = msg .. " " .. tostring(k) .. " = " .. tostring(v) 978 | end 979 | Log(msg) 980 | end 981 | end 982 | 983 | -- find a new value using above route 984 | local newValueFound = true 985 | local currentValue = traverseStartingPoint 986 | local lastValue 987 | for stepNumber = #route, 1, -1 do 988 | lastValue = currentValue 989 | local step = route[stepNumber] 990 | local currentType = type(currentValue) 991 | if currentType == "function" then 992 | assert(step.upvalueName or step.env) 993 | if step.upvalueName then 994 | local i = 1 995 | local found = false 996 | while true do 997 | local name, value = getupvalue(currentValue, i) 998 | if name == nil then 999 | break 1000 | elseif name == step.upvalueName then 1001 | currentValue = value 1002 | found = true 1003 | break 1004 | end 1005 | i = i + 1 1006 | end 1007 | if not found then 1008 | if log then 1009 | Log(" wasn't able to take step no.", #route - stepNumber + 1, 1010 | "in the new file, missing upvalue", step.upvalueName, 1011 | "(originally at index", tostring(step.upvalueIndex) .. ")") 1012 | end 1013 | newValueFound = false 1014 | break 1015 | end 1016 | elseif step.env then 1017 | currentValue = getfenv(currentValue) 1018 | end 1019 | elseif currentType == "table" then 1020 | assert(step.key or step.metatable) 1021 | if step.key then 1022 | currentValue = currentValue[step.key] 1023 | else 1024 | currentValue = getmetatable(currentValue) 1025 | end 1026 | end 1027 | end 1028 | return newValueFound, currentValue, lastValue 1029 | end 1030 | 1031 | Reload = function(fileName, chunkOriginal, chunkNew, returnValues) 1032 | reloadTimes = reloadTimes + 1 1033 | if log then Log("*** Reloading", fileName, "***") end 1034 | 1035 | if log then Log("\n*** LOOKING FOR REFERENCES ***") end 1036 | local references, visitedDuringSearch = FindReferences(fileName) 1037 | 1038 | 1039 | if log then Log("\n*** PREPARE RETURN VALUES BY INDEX ***") end 1040 | -- We store return values for all versions of the file in a one big table 1041 | -- cause we want them to be automatically removed by gc (see returnValuesMt) 1042 | -- It's okay if we load file once in applications lifetime, use it, 1043 | -- remove all references and the entry in the table is still present 1044 | -- Worse if entries for each version of the file are present. 1045 | local returnValuesByIndex = {} 1046 | for k, v in pairs(returnValues) do 1047 | local t = returnValuesByIndex[k.fileIndex] 1048 | if not t then 1049 | t = {} 1050 | returnValuesByIndex[k.fileIndex] = t 1051 | end 1052 | t[k.valueIndex] = v 1053 | if log then Log("return value for file with index [", k.fileIndex, "] no.", k.valueIndex, "is", v) end 1054 | end 1055 | 1056 | -- If we don't have information about file indexes for functions, 1057 | -- they all go into the bucket with id=0 in `references`, 1058 | -- and we have to separate function into several buckets based on their 1059 | -- upvalues, i.e. if two functions share an upvalue, we place them in the 1060 | -- same bucket. So essentially it is the same as having file indexes for 1061 | -- those functions, but with additional work required. 1062 | if useGetInfo and references[0] then 1063 | SeparateReferencesByUpvalues(references, returnValuesByIndex) 1064 | end 1065 | 1066 | -- Update return values. If a file returns a table with data without functions, 1067 | -- there will be no references, but we should update return values anyway. 1068 | for index, _ in pairs(returnValuesByIndex) do 1069 | if not references[index] then 1070 | references[index] = {} 1071 | if log then 1072 | Log("adding empty reference list based on return values", 1073 | "for file index", index) 1074 | end 1075 | end 1076 | end 1077 | 1078 | local versions = 0 1079 | for _, _ in pairs(references) do 1080 | versions = versions + 1 1081 | end 1082 | if log then Log("FOUND", versions, "VERSIONS OF THE", fileName, ":") end 1083 | 1084 | if log then 1085 | local version = 1 1086 | for index, list in pairs(references) do 1087 | Log(version .. ". ", fileName, "with index [", index, "]") 1088 | for i, ref in ipairs(list) do 1089 | Log(" ref#" .. i, "at [", ref.path, "] to", GetFuncDesc(ref.value)) 1090 | end 1091 | version = version + 1 1092 | end 1093 | end 1094 | 1095 | if log then Log("\n*** BUILDING ROUTES TO FUNCTIONS IN ORIGINAL FILE ***") end 1096 | 1097 | globalsAccessed = {} 1098 | if handleGlobalModules then 1099 | if log then Log("tracking globals...") end 1100 | trackGlobals = true 1101 | _G = setmetatable({}, envMetatable) 1102 | end 1103 | -- When traversing the original file, we can get to the references (e.g. via 1104 | -- following upvalues), and we shouldn't confuse them with new functions, 1105 | -- but we can't rely on fileIndex on Lua5.2 (some functions may not have it), 1106 | -- so instead we maintain the list of references, and check if a given 1107 | -- function isn't is this list 1108 | SetupChunkEnv(chunkOriginal, fileName, 0) -- fileIndex is irrelevant here 1109 | local returnValuesOriginal = Pack(chunkOriginal()) 1110 | if trackGlobals then 1111 | trackGlobals = false 1112 | _G = _GOriginal 1113 | end 1114 | local data = { 1115 | ["return_values"] = returnValuesOriginal 1116 | } 1117 | for name, _ in pairs(globalsAccessed) do 1118 | data["global_" .. name] = _G[name] 1119 | if log then Log(" schedule global [", name , "] to be traversed") end 1120 | end 1121 | 1122 | local fileData1 = traverse(data, fileName, visitedDuringSearch) 1123 | 1124 | local file = fileCache[fileName] 1125 | for fileIndex, list in pairs(references) do 1126 | if log then Log("\n*** UPDATING FILE WITH INDEX", fileIndex, "***") end 1127 | -- set env for newly loaded file to ensure that new functions has correct fileName and the fileIndex of the old file 1128 | SetupChunkEnv(chunkNew, fileName, fileIndex) 1129 | local returnValuesNew = Pack(UpdateReturnValues(file, fileIndex, chunkNew())) 1130 | local data = { 1131 | ["return_values"] = returnValuesNew 1132 | } 1133 | for name, _ in pairs(globalsAccessed) do 1134 | data["global_" .. name] = _G[name] 1135 | end 1136 | local fileData2 = traverse(data, fileName, visitedDuringSearch) 1137 | local detectedTables = {} 1138 | 1139 | if log then Log("\n*** UPDATING REFERENCES && SET ENVS OF THE NEW FUNCs TO ENV OF OLD ONES ***") end 1140 | if log and #list == 0 then Log("There are no references for this file index - no functions to update.") end 1141 | 1142 | for refIndex, ref in ipairs(list) do 1143 | local linedefined = getinfo(ref.value, "S").linedefined 1144 | local functionPtrList = fileData1.functions[linedefined] 1145 | if not functionPtrList then 1146 | if log then Log(refIndex .. ".", ref.value, "- wasn't able to find this one, probably it's a function made in another function" 1147 | .. "\n It was defined at line", linedefined) end 1148 | else 1149 | for routeIndex, routeStart in ipairs(functionPtrList) do 1150 | if log then 1151 | Log("ref#" .. refIndex, ref.value, "found in", 1152 | fileData1.queuePath and fileData1.queuePath[routeStart] or "", 1153 | "(route#" .. routeIndex .. ")") 1154 | end 1155 | 1156 | local newValueFound, currentValue, lastValue = GetValueByRoute(routeStart, fileData1, data) 1157 | 1158 | if not newValueFound then 1159 | if log then Log(" wasn't able to retrieve new value") end 1160 | else 1161 | if log then Log(" found new value", currentValue) end 1162 | -- skip nil values unless it is a last route, and we found nothing besides nil 1163 | -- then assume this function just got removed and we have to remove it. 1164 | if currentValue == nil and routeIndex ~= #functionPtrList then 1165 | if log then Log(" but is is a nil value, so first we should check other routes - maybe we will find non-nil value") end 1166 | else 1167 | -- update the reference 1168 | if ref.link.localId then 1169 | -- if it is a local variable 1170 | local stackLevel = ref.link.stackLevel + 1 1171 | local localId = ref.link.localId 1172 | if log then 1173 | local name, value = getlocal(stackLevel, localId) 1174 | Log(" set local [", name, "] at stack level", stackLevel, "at index", localId, "from [", value, "] to [", currentValue, "]") 1175 | end 1176 | setlocal(stackLevel, localId, currentValue) 1177 | elseif type(ref.link.owner) == "function" then 1178 | if log then 1179 | local name, _ = getupvalue(ref.link.owner, ref.link.upvalueId) 1180 | Log(" set upvalue [", ref.link.upvalueId, "] called [", name, "] in", ref.link.owner, "to [", currentValue, "]") 1181 | end 1182 | setupvalue(ref.link.owner, ref.link.upvalueId, currentValue) 1183 | elseif type(ref.link.owner) == "table" then 1184 | if lastValue then 1185 | detectedTables[lastValue] = { 1186 | current = ref.link.owner, -- in fact it is "current", but we wand to override 1187 | -- values in the original table, since this is what may be referenced 1188 | new = lastValue, -- new 1189 | original = fileData1.queue[fileData1.queuePrevious[routeStart]] -- original 1190 | } 1191 | end 1192 | if ref.link.key ~= nil then 1193 | ref.link.owner[ref.link.key] = currentValue 1194 | if log then Log(" set value [", ref.link.key, "] in", ref.link.owner, "to [", currentValue, "]") end 1195 | else 1196 | if currentValue then 1197 | ref.link.owner[currentValue] = ref.link.owner[ref.value] 1198 | if log then Log(" set key [", currentValue, "] in", ref.link.owner, "to [", ref.link.owner[ref.value], "]") end 1199 | else 1200 | if log then Log(" remove key [", ref.value, "] in", ref.link.owner) end 1201 | end 1202 | ref.link.owner[ref.value] = nil 1203 | end 1204 | else 1205 | error("DEV ERROR: invalid owner type in the reference, probably code gathering references is broken") 1206 | end 1207 | 1208 | -- stop going through the routes, if we a found a new non-nil value for this reference 1209 | break 1210 | end 1211 | end 1212 | end 1213 | end 1214 | end 1215 | 1216 | if log then Log("\n*** BUILDING CURRENT UPVALUES LIST ***") end 1217 | local upvaluesOriginal = fileData1.upvalues 1218 | local upvaluesNew = fileData2.upvalues 1219 | local upvaluesCurrent = {} 1220 | 1221 | local function IsFileScopeFunction(func, linedefined) 1222 | linedefined = linedefined or getinfo(func, "S").linedefined 1223 | return fileData1.functions[linedefined] ~= nil 1224 | end 1225 | 1226 | local ignoreUpvalues = {} 1227 | local visited = {} 1228 | for _, ref in ipairs(list) do 1229 | local currentValue = ref.value 1230 | local linedefined = getinfo(currentValue, "S").linedefined 1231 | if not visited[linedefined] then 1232 | visited[linedefined] = true 1233 | local upvalues = upvaluesCurrent 1234 | local isFileScopeFunction = IsFileScopeFunction(currentValue, linedefined) 1235 | if isFileScopeFunction or retrieveUpvaluesFromNestedFunctions then 1236 | local i = 1 1237 | while true do 1238 | local ln, lv = getupvalue(currentValue, i) 1239 | if ln ~= nil then 1240 | -- ignore upvalues that aren't present in the original file - they are probably local variables in functions, 1241 | -- which are references in nested functions 1242 | if upvaluesOriginal[ln] then 1243 | local write = true 1244 | local upvalueid = upvalueid(currentValue, i) 1245 | if upvalues[ln] and upvalues[ln].id ~= upvalueid then 1246 | local isFileScopeFunction_old = IsFileScopeFunction(upvalues[ln].func) 1247 | if not isFileScopeFunction and not isFileScopeFunction_old then 1248 | if log then Log("Note: Two different upvalues with the same names, referenced in two different nested functions\n", 1249 | "with name [", ln, "] will be ignored for safety. Because they probably aren't file-scoped upvalues.\n", 1250 | "There seems to be a file-scoped upvalue with the name, which may not be referenced in any accessible file-scoped func,\n", 1251 | "But we can't use those upvalues, because we can't tell which one (if there is one) is the file-scoped upvalue.") 1252 | end 1253 | ignoreUpvalues[ln] = true 1254 | elseif isFileScopeFunction and isFileScopeFunction_old then 1255 | error("DEV ERROR: something went wrong... Two different file-scoped functions with two different upvalues with the same name?.\n" 1256 | .. "We check for this case when we load original file, so this shouldn't happen.") 1257 | else 1258 | -- at this point we know, that one upvalue is from a nested func, and another one from the file-scoped func 1259 | if log then Log("Note: There are at least two upvalues with the same name [", ln, "],\n", 1260 | "but one of them is referenced in nested function, why another one is referenced in the file-scoped func,\n", 1261 | "so we are gonna use this one from the file-scoped func.") 1262 | end 1263 | if isFileScopeFunction then 1264 | -- below we override an upvalue from nested function with this upvalue from file-scoped function 1265 | ignoreUpvalues[ln] = nil 1266 | else 1267 | -- do not override upvalue, since we already have one from the file-scoped function 1268 | write = false 1269 | end 1270 | end 1271 | end 1272 | if write then 1273 | upvalues[ln] = { 1274 | id = upvalueid, 1275 | func = currentValue, 1276 | index = i, 1277 | value = lv 1278 | } 1279 | end 1280 | end 1281 | else 1282 | break 1283 | end 1284 | i = i + 1 1285 | end 1286 | else 1287 | if log then Log(GetFuncDesc(currentValue), "is seems to be a nested function, so we ignore its upvalues.\n", 1288 | "If this functions references upvalues from the file which aren't referenced in other accesible functions -\n", 1289 | "they won't be taken into account. New functions won't use them. Their values won't be updated, if they are const.") 1290 | end 1291 | end 1292 | end 1293 | end 1294 | 1295 | for k in pairs(ignoreUpvalues) do 1296 | -- remove any upvalues that weren't sure are file-scope upvalues 1297 | upvaluesCurrent[k] = nil 1298 | if log then Log("Note: removing upvalue [", k, "] from the list, because we aren't sure if it's a file-scoped upvalue or not.") end 1299 | end 1300 | 1301 | if logUpvalues and inspect then 1302 | local function LogUpvalues(message, upvalues) 1303 | Log(message) 1304 | Log(inspect(upvalues)) 1305 | end 1306 | 1307 | LogUpvalues("Current upvalues:", upvaluesCurrent) 1308 | LogUpvalues("Original upvalues:", upvaluesOriginal) 1309 | LogUpvalues("New upvalues:", upvaluesNew) 1310 | end 1311 | 1312 | 1313 | if log then Log("\n*** UPDATING OLD CONST UPVALUES TO NEW VALUES ***") end 1314 | 1315 | local queue = {} 1316 | local queueLink = {} 1317 | local visited = {} 1318 | for k, v in pairs(visitedGlobal) do 1319 | visited[k] = v 1320 | end 1321 | local size = 0 1322 | 1323 | local function push(obj, link) 1324 | size = size + 1 1325 | queue[size] = obj 1326 | queueLink[size] = link 1327 | end 1328 | 1329 | for _, v in pairs(detectedTables) do 1330 | push(v, { debugName = "detected_table" }) 1331 | end 1332 | 1333 | for k, _ in pairs(upvaluesCurrent) do 1334 | if upvaluesOriginal[k] then 1335 | if not upvaluesNew[k] then 1336 | if log then Log(" Note: upvalue [", k, "] seems to be removed from the new version of the file.") end 1337 | end 1338 | push({ 1339 | current = upvaluesCurrent[k].value, 1340 | new = upvaluesNew[k] and upvaluesNew[k].value, -- do not add `or nil` here because it will replace `false` `value` by `nil` 1341 | original = upvaluesOriginal[k].value 1342 | }, { 1343 | func = upvaluesCurrent[k].func, 1344 | upvalueIndex = upvaluesCurrent[k].index, 1345 | debugName = k 1346 | }) 1347 | else 1348 | if log then Log(" Note: upvalue [", k, "] isn't present in the original file, so it is probably an upvalue of a nested function.") end 1349 | end 1350 | end 1351 | 1352 | if log then Log(" Current fileIndex is", fileIndex) end 1353 | local returnedTables = returnValuesByIndex[fileIndex] 1354 | if returnedTables then 1355 | for i, v in pairs(returnedTables) do 1356 | local newValue = returnValuesNew[i] 1357 | if type(newValue) == "table" then 1358 | local original = returnValuesOriginal[i] 1359 | assert(type(original) == "table", 1360 | "DEV ERROR: type of return value in original file is different from the one stored in returnValues") 1361 | push({ 1362 | current = v, 1363 | new = newValue, 1364 | original = original 1365 | }, { 1366 | debugName = "return value no. " .. i 1367 | }) 1368 | else 1369 | if log and type(v) == "table" then 1370 | Log(" Note: return value no. [", i, "] from the original file, which was a table, is", type(newValue)) 1371 | end 1372 | end 1373 | end 1374 | else 1375 | if log then Log(" Note: old version of the file didn't return any tables, no special handling of the returned values is required.") end 1376 | end 1377 | 1378 | local ptr = 0 1379 | local tableNewValue = {} 1380 | while ptr < size do 1381 | ptr = ptr + 1 1382 | local obj = queue[ptr] 1383 | if log then Log(ptr .. ". stored in: [", queueLink[ptr].key or queueLink[ptr].debugName, "] current: [", obj.current, "] original: [", obj.original, "] new: [", obj.new, "]") end 1384 | -- if there is a difference 1385 | if obj.current ~= obj.new then 1386 | local currentType = type(obj.current) 1387 | if currentType == "table" and type(obj.original) == "table" and type(obj.new) == "table" then 1388 | if not visited[obj.current] then 1389 | visited[obj.current] = true 1390 | tableNewValue[obj.new] = obj.current 1391 | -- TODO take metatables into consideration 1392 | local isArray = true 1393 | local count1 = 0 1394 | local count2 = 0 1395 | local isStatic = true 1396 | for k, v in pairs(obj.current) do 1397 | count1 = count1 + 1 1398 | end 1399 | isArray = count1 == #obj.current 1400 | if isArray then 1401 | for k, v in pairs(obj.original) do 1402 | count2 = count2 + 1 1403 | end 1404 | isArray = count2 == #obj.original 1405 | if isArray then 1406 | for i, v in ipairs(obj.current) do 1407 | -- TODO maybe a deep comparison? how about a static array of tables? 1408 | if v ~= obj.original[i] then 1409 | isStatic = false 1410 | break 1411 | end 1412 | end 1413 | end 1414 | end 1415 | if not isArray or isStatic then 1416 | if log then Log(" it's a table value, and we are gonna traverse it further either because it's a dynamic array or not an array at all.") end 1417 | -- traverse further 1418 | for k, _ in pairs(obj.current) do 1419 | push({ 1420 | current = obj.current[k], 1421 | new = obj.new[k], 1422 | original = obj.original[k] 1423 | }, { table = obj.current, key = k }) 1424 | end 1425 | 1426 | for k, _ in pairs(obj.new) do 1427 | if obj.current[k] == nil and obj.original[k] == nil then 1428 | push({ 1429 | current = nil, 1430 | new = obj.new[k], 1431 | original = nil 1432 | }, { table = obj.current, key = k }) 1433 | end 1434 | end 1435 | else 1436 | if log then Log(" it's a table value, but we aren't gonna traverse it, because it's a static array.") end 1437 | end 1438 | end 1439 | elseif currentType ~= "function " and currentType ~= "thread" and currentType ~= "userdata" then 1440 | local isConst = obj.original == obj.current 1441 | if isConst then 1442 | if log then Log(" it's a POD value") end 1443 | end 1444 | 1445 | local isTableToPOD = type(obj.current) == "table" and type(obj.original) == "table" 1446 | if isTableToPOD then 1447 | if log then Log(" it's a table, which is a POD value in the new version of the file\n" 1448 | .. " So we just change this value into a new POD, but if there are anonymous functions from old file using this value, they may broke.") end 1449 | end 1450 | 1451 | if isConst or isTableToPOD then 1452 | -- it is a const value -- update old upvalue 1453 | local ref = queueLink[ptr] 1454 | if log then Log(" it's a CONST value") end 1455 | -- update the reference 1456 | if ref.func then 1457 | setupvalue(ref.func, ref.upvalueIndex, obj.new) 1458 | local ln, _ = getupvalue(ref.func, ref.upvalueIndex) 1459 | if log then Log(" set upvalue [", ln, "] in", ref.func, "from [", obj.current, "] to [", obj.new, "]") end 1460 | elseif ref.table then 1461 | ref.table[ref.key] = obj.new 1462 | if log then Log(" set value [", ref.key, "] in", ref.table, "from [", obj.current, "] to [", obj.new, "]") end 1463 | else 1464 | error("DEV ERROR: we try to update a value which isn't stored anywhere, either this the link is broken, or we are updating a return value.") 1465 | end 1466 | else 1467 | if log then Log(" it's NOT a CONST value") end 1468 | end 1469 | else 1470 | if log then Log(" it's a", currentType, "value - ignored") end 1471 | end 1472 | end 1473 | end 1474 | 1475 | if log then Log("\n*** MERGING UPVALUES OF NEW FUNCTIONS TO OLD ONES ***") end 1476 | for _, ptrList in pairs(fileData2.functions) do 1477 | local ptr = ptrList[1] 1478 | local func = fileData2.queue[ptr] 1479 | if log then Log("- processing function", func, "in ptr", ptr) end 1480 | local i = 1 1481 | while true do 1482 | local ln, lv = getupvalue(func, i) 1483 | if ln ~= nil then 1484 | local upvalue_new = upvaluesCurrent[ln] 1485 | if upvalue_new then 1486 | upvaluejoin(func, i, upvalue_new.func, upvalue_new.index) 1487 | if log then 1488 | Log(" link upvalue [", ln, "] to upvalue no.", upvalue_new.index, "from", upvalue_new.func, 1489 | "(was [", lv, "] now [", select(2, getupvalue(upvalue_new.func, upvalue_new.index)), "])") 1490 | end 1491 | else 1492 | if log then Log(" wasn't able to find upvalue [", ln, "] in the old version of the file") end 1493 | local newValue = tableNewValue[lv] 1494 | if newValue then 1495 | setupvalue(func, i, newValue) 1496 | if log then Log(" set upvalue [", ln, "] to table [", newValue, "] which was found during comparison of the data beetwen the original and new files") end 1497 | end 1498 | end 1499 | else 1500 | if log and i == 1 then 1501 | Log(" no upvalues") 1502 | end 1503 | break 1504 | end 1505 | i = i + 1 1506 | end 1507 | end 1508 | end 1509 | 1510 | if log then Log("\n*** RELOADING FINISHED ***") end 1511 | end 1512 | 1513 | function module.SetPrintReloadingLogs(enable) 1514 | log = enable 1515 | end 1516 | 1517 | function module.SetLogCacheAccess(enable) 1518 | logCacheAccess = enable 1519 | end 1520 | 1521 | function module.SetLogReferencesSteps(enable) 1522 | logReferencesSteps = enable 1523 | end 1524 | 1525 | function module.SetStoreReferencePath(enable) 1526 | storeReferencePath = enable 1527 | end 1528 | 1529 | function module.SetLogUpvalues(enable) 1530 | logUpvalues = enable 1531 | end 1532 | 1533 | function module.SetTraverseGlobals(enable) 1534 | traverseGlobals = enable 1535 | end 1536 | 1537 | function module.SetTraverseRegistry(enable) 1538 | traverseRegistry = enable 1539 | end 1540 | 1541 | function module.SetHandleGlobalModules(enable) 1542 | handleGlobalModules = enable 1543 | end 1544 | 1545 | function module.SetErrorHandler(errorHandler) 1546 | customErrorHandler = errorHandler 1547 | end 1548 | 1549 | function module.GetFileCache() 1550 | return fileCache 1551 | end 1552 | 1553 | function module.ClearFileCache() 1554 | fileCache = {} 1555 | end 1556 | 1557 | function module.SetUseCache(enable) 1558 | useCache = enable 1559 | end 1560 | 1561 | function module.SetUseOldFileOnError(enable) 1562 | useOldFileOnError = enable 1563 | end 1564 | 1565 | 1566 | function module.SetUseGetInfo(enable) 1567 | useGetInfo = enable 1568 | end 1569 | 1570 | function module.SetQueuePreallocationSize(value) 1571 | queuePreallocationSize = value 1572 | end 1573 | 1574 | function module.SetVisitedPreallocationSize(value) 1575 | visitedPreallocationSize = value 1576 | end 1577 | 1578 | -- a game is expected to provide getTimestamp function 1579 | local staticTimestamp = 0 1580 | function module.FileGetTimestamp(fileName) 1581 | if lfs then 1582 | return lfs.attributes(fileName, "modification") 1583 | elseif love and love.filesystem.getInfo then 1584 | return love.filesystem.getInfo(fileName).modtime 1585 | elseif love and love.filesystem.getLastModified then 1586 | return love.filesystem.getLastModified(fileName) 1587 | else 1588 | staticTimestamp = staticTimestamp + 1 1589 | return staticTimestamp 1590 | end 1591 | end 1592 | 1593 | function module.SetEnableTimestampCheck(enable) 1594 | enableTimestampCheck = enable 1595 | end 1596 | 1597 | local function GetTime() 1598 | if chronos then 1599 | return chronos.nanotime() 1600 | elseif love then 1601 | return love.timer.getTime() 1602 | end 1603 | return 0 1604 | end 1605 | 1606 | local monitorPtr = 1 1607 | function module.Monitor(step, log) 1608 | local momentStart = GetTime() 1609 | local filesNumber = #loadedFilesList 1610 | local filesToMonitor = step and math.min(filesNumber - 1, step) or filesNumber - 1 1611 | local target = monitorPtr + filesToMonitor 1612 | local reloading = false 1613 | for i = monitorPtr, target do 1614 | local filename = loadedFilesList[i % filesNumber + 1] 1615 | local cached = fileCache[filename] 1616 | if cached then 1617 | local timestamp = module.FileGetTimestamp(filename) 1618 | if timestamp and timestamp > cached.timestamp then 1619 | local file = io.open(filename, "r") 1620 | if file then 1621 | local success = true 1622 | if lfs then 1623 | success = lfs.lock(file, "r") 1624 | if success then 1625 | lfs.unlock(file) 1626 | end 1627 | end 1628 | io.close(file) 1629 | 1630 | if success then 1631 | if log then Log("Reloading", filename, "old timestamp:", cached.timestamp, "new timestamp:", timestamp) end 1632 | ScheduleReload(filename) 1633 | reloading = true 1634 | else 1635 | if log then Log("Failed to retrieve lock on a file") end 1636 | end 1637 | end 1638 | end 1639 | end 1640 | end 1641 | monitorPtr = target + 1 1642 | if log then 1643 | local duration = GetTime() - momentStart 1644 | local timeinfo = "" 1645 | if duration > 0 then 1646 | timeinfo = "Monitoring took " .. string.format("%.3f", duration * 1000) .. "ms, " 1647 | end 1648 | Log(timeinfo .. "monitored", (filesToMonitor + 1) .. "/" .. filesNumber, "files") 1649 | end 1650 | if reloading then 1651 | momentStart = GetTime() 1652 | end 1653 | ReloadScheduledFiles() 1654 | if reloading then 1655 | local duration = GetTime() - momentStart 1656 | if duration > 0 then 1657 | Log("Reloading took", string.format("%.3f", duration * 1000) .. "ms" ) 1658 | end 1659 | end 1660 | end 1661 | 1662 | -- this function is expected to be overridden by the game 1663 | function module.ShouldReload(fileName) 1664 | return true 1665 | end 1666 | 1667 | function module.ReloadFile(fileName, ignoreTimestamp) 1668 | if ignoreTimestamp == nil then 1669 | ignoreTimestamp = not enableTimestampCheck 1670 | end 1671 | -- check if this was loaded at least once (otherwise there is nothing to reload) 1672 | local file = fileCache[fileName] 1673 | if file and module.ShouldReload(fileName) then 1674 | local timestamp = module.FileGetTimestamp(fileName) 1675 | if timestamp > file.timestamp or ignoreTimestamp then 1676 | ScheduleReload(fileName) 1677 | ReloadScheduledFiles() 1678 | return true 1679 | end 1680 | end 1681 | return false 1682 | end 1683 | 1684 | function module.ReloadScheduledFiles() 1685 | ReloadScheduledFiles() 1686 | end 1687 | 1688 | function module.SetVisited(k, v) 1689 | visitedGlobal[k] = v 1690 | end 1691 | 1692 | function module.Inject() 1693 | -- if setfenv isn't accessible, assume we are dealing wih lua5.2+ 1694 | setfenvOriginal = setfenv 1695 | loadfileOriginal = loadfile 1696 | requireOriginal = require 1697 | 1698 | -- avoid overriding global setfenv if it wasn't defined 1699 | setfenv = setfenv and setfenvNew or setfenv 1700 | loadfile = loadfileNew 1701 | dofile = dofileNew 1702 | require = require and requireNew 1703 | end 1704 | 1705 | return module 1706 | --------------------------------------------------------------------------------