diff --git a/.classpath b/.classpath index f799784c..f8b7ddb4 100644 --- a/.classpath +++ b/.classpath @@ -6,7 +6,6 @@ - diff --git a/src/test/errors/args.lua b/src/test/errors/args.lua new file mode 100644 index 00000000..d35942da --- /dev/null +++ b/src/test/errors/args.lua @@ -0,0 +1,133 @@ +-- utilities to check that args of various types pass or fail +-- argument type checking + +anylua = { nil, 'abc', 1.23, true, {aa=11,bb==22}, print } + +somestring = { 'abc' } +somenumber = { 1.23 } +somestrnum = { 'abc', 1.23 } +someboolean = { true } +sometable = { {aa=11,bb=22} } +somefunction = { print } +somenil = { nil } + +local function contains(set,val) + local m = #set + for i=1,m do + if set[i] == val then + return true + end + end + return false +end + +local function except(some) + local n = #anylua + local z = {} + local j = 1 + for i=1,n do + if not contains(some, anylua[i]) then + z[j] = anylua[i] + j = j + 1 + end + end + return z +end + +notastring = except(somestring) +notanumber = except(somenumber) +notastrnum = except(somestrnum) +notaboolean = except(someboolean) +notatable = except(sometable) +notafunction = except(somefunction) +notanil = except(somenil) + +local function signature(name,arglist) + local t = {} + for i=1,#arglist do + if type(arglist[i]) == 'table' then + t[i] = 'table' + elseif type(arglist[i]) == 'function' then + t[i] = 'function' + else + t[i] = tostring(arglist[i]) + end + end + return name..'('..table.concat(t,',')..')' +end + +local function expand(argsets, typesets, ...) + local n = typesets and #typesets or 0 + if n <= 0 then + table.insert(argsets,{...}) + return argsets + end + + local t = typesets[1] + local s = {select(2,unpack(typesets))} + local m = #t + for i=1,m do + expand(argsets, s, t[i], ...) + end + return argsets +end + +local function arglists(typesets) + local argsets = expand({},typesets) + return ipairs(argsets) +end + +local function lookup( name ) + return loadstring('return '..name)() +end + +local function invoke( name, arglist ) + local s,c = pcall(lookup, name) + if not s then return s,c end + return pcall(c, unpack(arglist)) +end + +-- check that all combinations of arguments pass +function checkallpass( name, typesets ) + for i,v in arglists(typesets) do + local sig = signature(name,v) + local s,e = invoke( name, v ) + if s then + print( 'pass', sig ) + else + print( 'fail', sig, e ) + end + end +end + +-- check that all combinations of arguments fail in some way, +-- ignore error messages +function checkallfail( name, typesets ) + for i,v in arglists(typesets) do + local sig = signature(name,v) + local s,e,f,g = invoke( name, v ) + if not s then + print( 'ok', sig ) + else + print( 'needcheck', sig, e, f, g ) + end + end +end + +-- check that all combinations of arguments fail in some way, +-- ignore error messages +function checkallerrors( name, typesets, template ) + for i,v in arglists(typesets) do + local sig = signature(name,v) + local s,e,f,g = invoke( name, v ) + if not s then + if string.match(e, template) then + print( 'ok', sig, 'template='..template ) + else + print( 'badmsg', sig, "template='"..template.."' actual='"..e.."'" ) + end + else + print( 'needcheck', sig, e, f, g ) + end + end +end diff --git a/src/test/errors/baselibargs.lua b/src/test/errors/baselibargs.lua new file mode 100644 index 00000000..d48ad2ad --- /dev/null +++ b/src/test/errors/baselibargs.lua @@ -0,0 +1,9 @@ +package.path = "?.lua;src/test/errors/?.lua" +require 'args' + + +-- arg types for basic library functions +local notastrnumnil={true,{},print} +checkallpass('dofile', {{nil,'src/test/errors/args.lua','args.lua'}}) +checkallfail('dofile', {notastrnumnil}) +checkallerrors('dofile', {notastrnumnil}, 'bad argument') \ No newline at end of file diff --git a/src/test/java/org/luaj/AllTests.java b/src/test/java/org/luaj/AllTests.java index 4aa7f427..cc79dbc6 100644 --- a/src/test/java/org/luaj/AllTests.java +++ b/src/test/java/org/luaj/AllTests.java @@ -10,13 +10,14 @@ public class AllTests { // debug tests TestSuite vm = new TestSuite("VM"); + vm.addTestSuite(org.luaj.vm.CompatibiltyTest.class); + vm.addTestSuite(org.luaj.vm.ErrorMessageTest.class); vm.addTestSuite(org.luaj.vm.LuaStateTest.class); vm.addTestSuite(org.luaj.vm.LoadStateTest.class); vm.addTestSuite(org.luaj.vm.LStringTest.class); vm.addTestSuite(org.luaj.vm.MathLibTest.class); vm.addTestSuite(org.luaj.vm.LTableTest.class); vm.addTestSuite(org.luaj.vm.LWeakTableTest.class); - vm.addTestSuite(org.luaj.vm.LuaJTest.class); suite.addTest(vm); // compiler tests diff --git a/src/test/java/org/luaj/debug/DebugStackStateTest.java b/src/test/java/org/luaj/debug/DebugStackStateTest.java index 86a204c8..c585e2e6 100644 --- a/src/test/java/org/luaj/debug/DebugStackStateTest.java +++ b/src/test/java/org/luaj/debug/DebugStackStateTest.java @@ -21,6 +21,7 @@ ******************************************************************************/ package org.luaj.debug; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -33,13 +34,12 @@ import org.luaj.vm.LClosure; import org.luaj.vm.LPrototype; import org.luaj.vm.LValue; import org.luaj.vm.LoadState; -import org.luaj.vm.LuaState; import org.luaj.vm.Platform; public class DebugStackStateTest extends TestCase { public void testDebugStackState() throws InterruptedException, IOException { - String script = "/test6.lua"; + String script = "src/test/res/test6.lua"; // set up the vm System.setProperty(Platform.PROPERTY_LUAJ_DEBUG, "true"); @@ -51,7 +51,7 @@ public class DebugStackStateTest extends TestCase { final DebugLuaState state = (DebugLuaState) Platform.newLuaState(); LuaC.install(); - InputStream is = getClass().getResourceAsStream( script ); + InputStream is = new FileInputStream( script ); LPrototype p = LoadState.undump(state, is, script); // create closure and execute diff --git a/src/test/java/org/luaj/jit/LuaJitTest.java b/src/test/java/org/luaj/jit/LuaJitTest.java index 596cef4d..9fbd1915 100644 --- a/src/test/java/org/luaj/jit/LuaJitTest.java +++ b/src/test/java/org/luaj/jit/LuaJitTest.java @@ -4,17 +4,17 @@ import java.io.IOException; import org.luaj.jit.LuaJit; import org.luaj.vm.LPrototype; -import org.luaj.vm.LuaJTest; +import org.luaj.vm.CompatibiltyTest; import org.luaj.vm.LuaState; /** * Suite of standard tests, but using the LuaJit compiler * for all loaded prototypes. */ -public class LuaJitTest extends LuaJTest { +public class LuaJitTest extends CompatibiltyTest { - protected LPrototype loadScriptResource( LuaState state, String name ) throws IOException { - LPrototype p = super.loadScriptResource(state, name); + protected LPrototype loadScript( LuaState state, String name ) throws IOException { + LPrototype p = super.loadScript(state, name); return LuaJit.jitCompile(p); } diff --git a/src/test/java/org/luaj/vm/CompatibiltyTest.java b/src/test/java/org/luaj/vm/CompatibiltyTest.java new file mode 100644 index 00000000..69f633f5 --- /dev/null +++ b/src/test/java/org/luaj/vm/CompatibiltyTest.java @@ -0,0 +1,178 @@ +package org.luaj.vm; + +import java.io.IOException; + +/** + * Compatibility tests for the Luaj VM + * + * Results are compared for exact match with + * the installed C-based lua environment. + */ +public class CompatibiltyTest extends ScriptDrivenTest { + + private static final String dir = "src/test/res"; + + public CompatibiltyTest() { + super(dir); + } + + public void testTest1() throws IOException, InterruptedException { + runTest("test1"); + } + + public void testTest2() throws IOException, InterruptedException { + runTest("test2"); + } + + public void testTest3() throws IOException, InterruptedException { + runTest("test3"); + } + + public void testTest4() throws IOException, InterruptedException { + runTest("test4"); + } + + public void testTest5() throws IOException, InterruptedException { + runTest("test5"); + } + + public void testTest6() throws IOException, InterruptedException { + runTest("test6"); + } + + public void testTest7() throws IOException, InterruptedException { + runTest("test7"); + } + + public void testTest8() throws IOException, InterruptedException { + runTest("test8"); + } + + public void testArgtypes() throws IOException, InterruptedException { + runTest("argtypes"); + } + + public void testAutoload() throws IOException, InterruptedException { + runTest("autoload"); + } + + public void testBaseLib() throws IOException, InterruptedException { + runTest("baselib"); + } + + public void testBoolean() throws IOException, InterruptedException { + runTest("boolean"); + } + + public void testCalls() throws IOException, InterruptedException { + runTest("calls"); + } + + public void testCoercions() throws IOException, InterruptedException { + runTest("coercions"); + } + + public void testCoroutines() throws IOException, InterruptedException { + runTest("coroutines"); + } + + public void testCompare() throws IOException, InterruptedException { + runTest("compare"); + } + + public void testErrors() throws IOException, InterruptedException { + runTest("errors"); + } + + public void testHugeTable() throws IOException, InterruptedException { + runTest("hugetable"); + } + + public void testLoops() throws IOException, InterruptedException { + runTest("loops"); + } + + public void testManyLocals() throws IOException, InterruptedException { + runTest("manylocals"); + } + + public void testMathLib() throws IOException, InterruptedException { + runTest("mathlib"); + } + + public void testMetatables() throws IOException, InterruptedException { + runTest("metatables"); + } + + public void testModule() throws IOException, InterruptedException { + runTest("module"); + } + + public void testNext() throws IOException, InterruptedException { + runTest("next"); + } + + public void testPcalls() throws IOException, InterruptedException { + runTest("pcalls"); + } + + public void testRequire() throws IOException, InterruptedException { + runTest("require"); + } + + public void testSelect() throws IOException, InterruptedException { + runTest("select"); + } + + public void testSetfenv() throws IOException, InterruptedException { + runTest("setfenv"); + } + + public void testSetlist() throws IOException, InterruptedException { + runTest("setlist"); + } + + public void testSimpleMetatables() throws IOException, InterruptedException { + runTest("simplemetatables"); + } + + public void testStack() throws IOException, InterruptedException { + runTest("stack"); + } + + public void testStrLib() throws IOException, InterruptedException { + runTest("strlib"); + } + + public void testSort() throws IOException, InterruptedException { + runTest("sort"); + } + + public void testTable() throws IOException, InterruptedException { + runTest("table"); + } + + public void testTailcall() throws IOException, InterruptedException { + runTest("tailcall"); + } + + public void testType() throws IOException, InterruptedException { + runTest("type"); + } + + public void testUpvalues() throws IOException, InterruptedException { + runTest("upvalues"); + } + + public void testUpvalues2() throws IOException, InterruptedException { + runTest("upvalues2"); + } + + public void testUpvalues3() throws IOException, InterruptedException { + runTest("upvalues3"); + } + + public void testWeakTable() throws IOException, InterruptedException { + runTest("weaktable"); + } +} diff --git a/src/test/java/org/luaj/vm/ErrorMessageTest.java b/src/test/java/org/luaj/vm/ErrorMessageTest.java new file mode 100644 index 00000000..62a1910a --- /dev/null +++ b/src/test/java/org/luaj/vm/ErrorMessageTest.java @@ -0,0 +1,22 @@ +package org.luaj.vm; + +import java.io.IOException; + +/** + * Test error messages produced by luaj. + */ +public class ErrorMessageTest extends ScriptDrivenTest { + + private static final String dir = "src/test/errors"; + + public ErrorMessageTest() { + super(dir); + } + + public void testBaseLibArgs() throws IOException, InterruptedException { + runTest("baselibargs"); + } + + + +} diff --git a/src/test/java/org/luaj/vm/LuaJTest.java b/src/test/java/org/luaj/vm/LuaJTest.java deleted file mode 100644 index c90a1635..00000000 --- a/src/test/java/org/luaj/vm/LuaJTest.java +++ /dev/null @@ -1,345 +0,0 @@ -package org.luaj.vm; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import junit.framework.TestCase; - -import org.luaj.TestPlatform; -import org.luaj.compiler.LuaC; -import org.luaj.lib.BaseLib; - -public class LuaJTest extends TestCase { - - protected void setUp() throws Exception { - super.setUp(); - Platform.setInstance(new TestPlatform()); - } - - public void testTest1() throws IOException, InterruptedException { - runTest("test1"); - } - - public void testTest2() throws IOException, InterruptedException { - runTest("test2"); - } - - public void testTest3() throws IOException, InterruptedException { - runTest("test3"); - } - - public void testTest4() throws IOException, InterruptedException { - runTest("test4"); - } - - public void testTest5() throws IOException, InterruptedException { - runTest("test5"); - } - - public void testTest6() throws IOException, InterruptedException { - runTest("test6"); - } - - public void testTest7() throws IOException, InterruptedException { - runTest("test7"); - } - - public void testTest8() throws IOException, InterruptedException { - runTest("test8"); - } - - public void testArgtypes() throws IOException, InterruptedException { - runTest("argtypes"); - } - - public void testAutoload() throws IOException, InterruptedException { - runTest("autoload"); - } - - public void testBaseLib() throws IOException, InterruptedException { - runTest("baselib"); - } - - public void testBoolean() throws IOException, InterruptedException { - runTest("boolean"); - } - - public void testCalls() throws IOException, InterruptedException { - runTest("calls"); - } - - public void testCoercions() throws IOException, InterruptedException { - runTest("coercions"); - } - - public void testCoroutines() throws IOException, InterruptedException { - runTest("coroutines"); - } - - public void testCompare() throws IOException, InterruptedException { - runTest("compare"); - } - - public void testErrors() throws IOException, InterruptedException { - runTest("errors"); - } - - public void testHugeTable() throws IOException, InterruptedException { - runTest("hugetable"); - } - - public void testLoops() throws IOException, InterruptedException { - runTest("loops"); - } - - public void testManyLocals() throws IOException, InterruptedException { - runTest("manylocals"); - } - - public void testMathLib() throws IOException, InterruptedException { - runTest("mathlib"); - } - - public void testMetatables() throws IOException, InterruptedException { - runTest("metatables"); - } - - public void testModule() throws IOException, InterruptedException { - runTest("module"); - } - - public void testNext() throws IOException, InterruptedException { - runTest("next"); - } - - public void testPcalls() throws IOException, InterruptedException { - runTest("pcalls"); - } - - public void testRequire() throws IOException, InterruptedException { - runTest("require"); - } - - public void testSelect() throws IOException, InterruptedException { - runTest("select"); - } - - public void testSetfenv() throws IOException, InterruptedException { - runTest("setfenv"); - } - - public void testSetlist() throws IOException, InterruptedException { - runTest("setlist"); - } - - public void testSimpleMetatables() throws IOException, InterruptedException { - runTest("simplemetatables"); - } - - public void testStack() throws IOException, InterruptedException { - runTest("stack"); - } - - public void testStrLib() throws IOException, InterruptedException { - runTest("strlib"); - } - - public void testSort() throws IOException, InterruptedException { - runTest("sort"); - } - - public void testTable() throws IOException, InterruptedException { - runTest("table"); - } - - public void testTailcall() throws IOException, InterruptedException { - runTest("tailcall"); - } - - public void testType() throws IOException, InterruptedException { - runTest("type"); - } - - public void testUpvalues() throws IOException, InterruptedException { - runTest("upvalues"); - } - - public void testUpvalues2() throws IOException, InterruptedException { - runTest("upvalues2"); - } - - public void testUpvalues3() throws IOException, InterruptedException { - runTest("upvalues3"); - } - - public void testWeakTable() throws IOException, InterruptedException { - runTest("weaktable"); - } - - // */ - private void runTest(String testName) throws IOException, - InterruptedException { - - // new lua state - LuaState state = Platform.newLuaState(); - - // install the compiler - LuaC.install(); - - // load the file - LPrototype p = loadScriptResource(state, testName); - p.source = LString.valueOf("stdin"); - - // Replace System.out with a ByteArrayOutputStream - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BaseLib.redirectOutput(outputStream); - try { - // create closure and execute - LClosure c = p.newClosure(state._G); - state.pushlvalue(c); - state.call(0, 0); - - final String actualOutput = new String(outputStream.toByteArray()); - final String expectedOutput = getExpectedOutput(testName); - - assertEquals(expectedOutput, actualOutput); - } finally { - BaseLib.restoreStandardOutput(); - outputStream.close(); - } - } - - protected LPrototype loadScriptResource(LuaState state, String name) - throws IOException { - InputStream script = getClass().getResourceAsStream( - "/" + name + ".luac"); - if (script == null) { - script = getClass().getResourceAsStream("/" + name + ".lua"); - if (script == null) { - fail("Could not load script for test case: " + name); - } - } - - try { - // Use "stdin" instead of resource name so that output matches - // standard Lua. - return LoadState.undump(state, script, "stdin"); - } finally { - script.close(); - } - } - - private String getExpectedOutput(final String testName) throws IOException, - InterruptedException { - String expectedOutputName = "/" + testName + "-expected.out"; - InputStream is = getClass().getResourceAsStream(expectedOutputName); - if (is != null) { - try { - return readString(is); - } finally { - is.close(); - } - } else { - InputStream script; - // script = getClass().getResourceAsStream( "/" + testName + ".luac" - // ); - // if ( script == null ) { - script = getClass().getResourceAsStream("/" + testName + ".lua"); - if (script == null) { - fail("Could not find script for test case: " + testName); - } - // } - try { - return collectProcessOutput(new String[] { "lua", "-" }, script); - } finally { - script.close(); - } - } - } - - private String collectProcessOutput(String[] cmd, final InputStream input) - throws IOException, InterruptedException { - Runtime r = Runtime.getRuntime(); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final Process p = r.exec(cmd); - try { - // start a thread to write the given input to the subprocess. - Thread inputCopier = (new Thread() { - public void run() { - try { - OutputStream processStdIn = p.getOutputStream(); - try { - copy(input, processStdIn); - } finally { - processStdIn.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - inputCopier.start(); - - // start another thread to read output from the subprocess. - Thread outputCopier = (new Thread() { - public void run() { - try { - InputStream processStdOut = p.getInputStream(); - try { - copy(processStdOut, baos); - } finally { - processStdOut.close(); - } - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - }); - outputCopier.start(); - - // start another thread to read output from the subprocess. - Thread errorCopier = (new Thread() { - public void run() { - try { - InputStream processError = p.getErrorStream(); - try { - copy(processError, System.err); - } finally { - processError.close(); - } - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - }); - errorCopier.start(); - - p.waitFor(); - inputCopier.join(); - outputCopier.join(); - errorCopier.join(); - - return new String(baos.toByteArray()); - - } finally { - p.destroy(); - } - } - - private String readString(InputStream is) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - copy(is, baos); - return new String(baos.toByteArray()); - } - - private void copy(InputStream is, OutputStream os) throws IOException { - byte[] buf = new byte[1024]; - int r; - while ((r = is.read(buf)) >= 0) { - os.write(buf, 0, r); - } - } - -} diff --git a/src/test/java/org/luaj/vm/ScriptDrivenTest.java b/src/test/java/org/luaj/vm/ScriptDrivenTest.java new file mode 100644 index 00000000..66734d93 --- /dev/null +++ b/src/test/java/org/luaj/vm/ScriptDrivenTest.java @@ -0,0 +1,190 @@ +package org.luaj.vm; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import junit.framework.TestCase; + +import org.luaj.compiler.LuaC; +import org.luaj.lib.BaseLib; +import org.luaj.platform.J2sePlatform; + +abstract +public class ScriptDrivenTest extends TestCase { + + private final String basedir; + + protected ScriptDrivenTest( String directory ) { + basedir = directory; + } + + // */ + protected void runTest(String testName) throws IOException, + InterruptedException { + + // set platform relative to directory + Platform.setInstance(new J2sePlatform() { + public InputStream openFile(String fileName) { + return super.openFile(basedir+"/"+fileName); + } + }); + + // new lua state + LuaState state = Platform.newLuaState(); + + // install the compiler + LuaC.install(); + + // load the file + LPrototype p = loadScript(state, testName); + p.source = LString.valueOf("stdin"); + + // Replace System.out with a ByteArrayOutputStream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BaseLib.redirectOutput(outputStream); + try { + // create closure and execute + LClosure c = p.newClosure(state._G); + state.pushlvalue(c); + state.call(0, 0); + + final String actualOutput = new String(outputStream.toByteArray()); + final String expectedOutput = getExpectedOutput(testName); + + assertEquals(expectedOutput, actualOutput); + } finally { + BaseLib.restoreStandardOutput(); + outputStream.close(); + } + } + + protected LPrototype loadScript(LuaState state, String name) + throws IOException { + File file = new File(basedir+"/"+name+".luac"); + if ( !file.exists() ) + file = new File(basedir+"/"+name+".lua"); + if ( !file.exists() ) + fail("Could not load script for test case: " + name); + + InputStream script = new FileInputStream(file); + try { + // Use "stdin" instead of resource name so that output matches + // standard Lua. + return LoadState.undump(state, script, "stdin"); + } finally { + script.close(); + } + } + + private String getExpectedOutput(final String name) throws IOException, + InterruptedException { + String expectedOutputName = basedir+"/"+name+"-expected.out"; + InputStream is = getClass().getResourceAsStream(expectedOutputName); + if (is != null) { + try { + return readString(is); + } finally { + is.close(); + } + } else { + File file = new File(basedir+"/"+name+".lua"); + if ( !file.exists() ) + fail("Could not load script for test case: " + name); + InputStream script = new FileInputStream(file); + // } + try { + return collectProcessOutput(new String[] { "lua", "-" }, script); + } finally { + script.close(); + } + } + } + + private String collectProcessOutput(String[] cmd, final InputStream input) + throws IOException, InterruptedException { + Runtime r = Runtime.getRuntime(); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final Process p = r.exec(cmd); + try { + // start a thread to write the given input to the subprocess. + Thread inputCopier = (new Thread() { + public void run() { + try { + OutputStream processStdIn = p.getOutputStream(); + try { + copy(input, processStdIn); + } finally { + processStdIn.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + inputCopier.start(); + + // start another thread to read output from the subprocess. + Thread outputCopier = (new Thread() { + public void run() { + try { + InputStream processStdOut = p.getInputStream(); + try { + copy(processStdOut, baos); + } finally { + processStdOut.close(); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + }); + outputCopier.start(); + + // start another thread to read output from the subprocess. + Thread errorCopier = (new Thread() { + public void run() { + try { + InputStream processError = p.getErrorStream(); + try { + copy(processError, System.err); + } finally { + processError.close(); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + }); + errorCopier.start(); + + p.waitFor(); + inputCopier.join(); + outputCopier.join(); + errorCopier.join(); + + return new String(baos.toByteArray()); + + } finally { + p.destroy(); + } + } + + private String readString(InputStream is) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + copy(is, baos); + return new String(baos.toByteArray()); + } + + private void copy(InputStream is, OutputStream os) throws IOException { + byte[] buf = new byte[1024]; + int r; + while ((r = is.read(buf)) >= 0) { + os.write(buf, 0, r); + } + } + +} diff --git a/version.properties b/version.properties index 87d19161..2d54a2b4 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -version: 0.40 +version: 0.41