#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%new-style
%enable-all-warnings
%require-types
%strict-args
%no-child-restrictions
# to test ProgramControl::getProgram
%allow-debugger

%requires ../../../../../qlib/Util.qm
%requires ../../../../../qlib/QUnit.qm
%exec-class ProgramTest

int sub t(any a) {
    return a + 1;
}

list sub funcTest() {
    return list();
}
list sub funcTest(int a) {
    return list(a);
}
list sub funcTest(string a) {
    return list(a);
}
list sub funcTest(int a, string b) {
    return list(a, b);
}
list sub funcTest(int a, binary b, *string c) {
    return list(a, b);
}

nothing sub funcParamTypeTest(
    int i, *int i2,
    string s, *string s2,
    bool bo, *bool bo2,
    float f, *float f2,
    number n, *number n2,
    binary bi, *binary b2,
    list l, *list l2,
    hash h, *hash h2,
    object o, *object o2,
    any a,
    date d, *date d2,
    code co, *code co2,
    data da, *data da2,
    reference r, *reference r2,
    #null
    #nothing
    softint si, *softint si2,
    softfloat sf, *softfloat sf2,
    softnumber sn, *softnumber sn2,
    softbool sb, *softbool sb2,
    softstring ss, *softstring ss2,
    softdate sd, *softdate sd2,
    softlist sl, *softlist sl2,
    timeout t, *timeout t2,
    closure cl,
    callref cr,
    Test test,
    TestNS testns,
    NS::TestNS nsttestns
    ) {
}

namespace NS {
    list sub funcNSTest() {
        return list();
    }
    list sub funcNSTest(int a) {
        return list(a);
    }
    list sub funcNSTest(string a) {
        return list(a);
    }
    list sub funcNSTest(int a, string b) {
        return list(a, b);
    }
    list sub funcNSTest(int a, binary b, *string c) {
        return list(a, b);
    }

    class TestNS {
        private list funcTest() {
            return list();
        }
        list funcTest(int a) {
            return list(a);
        }
        list funcTest(string a) {
            return list(a);
        }
        list funcTest(int a, string b) {
            return list(a, b);
        }
        list funcTest(int a, binary b, *string c) {
            return list(a, b);
        }
    }

    class TestNS2 inherits TestNS {
        private list funcTest() {
            return list();
        }
        list funcTest(int a) {
            return list(a);
        }
        list funcTest(string a) {
            return list(a);
        }
        list funcTest(int a, string b) {
            return list(a, b);
        }
        list funcTest(int a, binary b, *string c) {
            return list(a, b);
        }
    }
    class TestNS3 inherits TestNS {
    }
}

class Test inherits Socket {
    private {
        int a;
        int b;
    }
    public {
        list data;
        hash t;
    }

    constructor(any a, any b, any c) {
        a = 1;
        b = 2;
        data = (a, b, c);
    }

    constructor(list dummy) { # overload test
        data = dummy;
    }

    destructor() {
    }

    copy(Test old) {
    }

    any getData(int elem) {
        return data[elem];
    }
    string methodGate(string m) {
        return m;
    }
    string memberGate(string m) {
        return "memberGate-" + m;
    }
    memberNotification(string m) {
        t.m = self.m;
    }
    code closure(any x) {
        int a = 1;
        # return a closure encapsulating the state of the object
        return string sub (any y) {
            return sprintf("%s-%n-%n-%n", data[1], x, y, ++a);
        };
    }
    any argTest() {
        return argv;
    }

    list funcTest() {
        return list();
    }
    list funcTest(int a) {
        return list(a);
    }
    list funcTest(string a) {
        return list(a);
    }
    list funcTest(int a, string b) {
        return list(a, b);
    }
    list funcTest(int a, binary b, *string c) {
        return list(a, b);
    }
}

class ProgramTest inherits QUnit::Test {
    public {
    }

    private {
        const CombineSrc = "class B1 {private { x; }} class B2 {public {x;}} class D inherits B1, B2 {}";

        # global variables provided by the Qore language
        const QoreGlobalVars = ("Qore::ENV", "Qore::QORE_ARGV", "Qore::ARGV");
    }

    constructor() : QUnit::Test("ProgramTest", "1.0", \ARGV) {
        addTestCase("ellipses", \ellipsesTest());
        addTestCase("call static method", \callStaticMethodTest());
        addTestCase("expression test", \expressionTest());
        addTestCase("call ref", \callRefTest());
        addTestCase("issue 2993", \issue2993());
        addTestCase("issue 3136", \issue3136());
        addTestCase("issue 3027 test", \issue3027Test());
        addTestCase("issueModuleCmd() test", \issueModuleCmdTest());
        addTestCase("issue 2807", \issue2807());
        addTestCase("issue 2735 test", \issue2735Test());
        addTestCase("Program test", \programTest());
        addTestCase("Combine test", \combineTest());
        addTestCase("Int assignments test", \intAssignmentsTest());
        addTestCase("constant exp test", \constantExpressionTest());
        addTestCase("type error test", \typeErrorTest());
        addTestCase("broken-operators test", \brokenOperatorsTest());
        addTestCase("class test", \classTest());
        addTestCase("setThreadInit test", \setThreadInitTest());
        addTestCase("var test", \varTest());
        addTestCase("Program info test", \programInfoTest());
	    addTestCase("Find runtime function test", \findRuntimeTest());
    	set_return_value(main());
    }

    ellipsesTest() {
        {
            Program p(PO_NEW_STYLE);
            assertThrows("ILLEGAL-ELLIPSES", \p.parse(), ("const T = (1, 2, ...);", ""));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("auto sub t(...) { return argv[0]; }", "");
            assertEq(1, p.callFunction("t", 1));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("class T { abstract auto t(...); } class C inherits T { auto t(...) { return argv[0]; }}"
                "auto sub t(auto x) { C c(); T t = c; return t.t(x); }", "");
            assertEq(1, p.callFunction("t", 1));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            assertThrows("CALL-WITH-TYPE-ERRORS", \p.parse(), ("class T { abstract auto t(); } "
                "class C inherits T { auto t(...) { return argv[0]; }} "
                "auto sub t(auto x) { C c(); T t = c; return t.t(x); }", ""));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            assertThrows("ABSTRACT-CLASS-ERROR", \p.parse(), ("class T { abstract auto t(...); } "
                "class C inherits T { auto t() { return argv[0]; }} "
                "auto sub t(auto x) { C c(); T t = c; return t.t(x); }", ""));
        }

        {
            Program p(PO_NEW_STYLE | PO_BROKEN_VARARGS | PO_STRICT_ARGS);
            assertThrows("ABSTRACT-CLASS-ERROR", \p.parse(), ("class T { abstract auto t(); } "
                "class C inherits T { auto t(...) { return argv[0]; }} "
                "auto sub t(auto x) { C c(); T t = c; return t.t(x); }", ""));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("class T { abstract auto t(int i, ...); } class C inherits T { auto t(int i, ...) { return argv[0]; }}"
                "auto sub t(auto x) { C c(); T t = c; return t.t(1, x); }", "");
            assertEq(1, p.callFunction("t", 1));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("class T { constructor() { t(1); } abstract auto t(...); } "
                "class C inherits T { auto t(...) { return argv[0]; }} "
                "auto sub t0(auto x) { C c(); T t = c; return t.t(x); }", "");
            assertEq(1, p.callFunction("t0", 1));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("class T { constructor() { t(1); } abstract auto t(int i, ...); } "
                "class C inherits T { auto t(int i, ...) { return argv[0]; }} "
                "auto sub t0(auto x) { C c(); T t = c; return t.t(1, x); }", "");
            assertEq(1, p.callFunction("t0", 1));
        }

        {
            Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
            p.parse("class T { constructor() { background t(1); } abstract auto t(...); } "
                "class C inherits T { auto t(...) { return argv[0]; }} "
                "auto sub t0(auto x) { C c(); T t = c; return t.t(x); }", "");
            assertEq(1, p.callFunction("t0", 1));
        }
    }

    callStaticMethodTest() {
        Program p(PO_NEW_STYLE);
        p.parse("class T { static *list<auto> t() { return argv; }}", "");
        assertEq(1, p.callStaticMethod("T", "t", 1)[0]);
        assertEq(1, p.callStaticMethodArgs("T", "t", 1)[0]);
        assertThrows("UNKNOWN-CLASS", \p.callStaticMethod(), ("x", "x"));
        assertThrows("UNKNOWN-CLASS", \p.callStaticMethodArgs(), ("x", "x"));
    }

    expressionTest() {
        Program p(PO_NEW_STYLE);
        p.parse("int sub t() { return 1; }", "");
        assertEq(1, p.callFunction("t"));
        Expression exp0 = p.getExpression("1", "exp1");
        assertEq(1, exp0.eval());
        assertEq(1, exp0.eval());
        Expression exp1 = p.getExpression("round(nlog(1000) + 500).toString()", "exp1");
        assertEq("507", exp1.eval());
        Expression exp2 = p.getExpression("(int x = (rand() % 200)) + x * 2", "exp2");
        assertEq(Type::Int, exp2.eval().type());

        # only a single expression is allowed
        assertThrows("PARSE-EXCEPTION", \p.getExpression(), ("1); namespace X { sub t() {} } our int x = (3", "exp3"));
        # global variables may not be declared
        assertThrows("PARSE-EXCEPTION", \p.getExpression(), ("our int x = (rand() % 200)", "exp4"));

        # make sure the Program object is still usable after a parse error
        assertEq(Type::Int, exp2.eval().type());

        delete p;
        assertThrows("PROGRAM-ERROR", \exp0.eval());
    }

    callRefTest() {
        Program p(PO_NEW_STYLE);
        p.parse("int sub get_int() { return 1; } class T { static int t() { return 2; }}", "");
        code ref = p.getCallReference("get_int");
        assertEq(1, ref());
        ref = p.getCallReference("T::t");
        assertEq(2, ref());
        ref = get_call_reference("Serializable::deserialize");
        assertEq(1, ref(Serializable::serialize(1)));
        ref = p.getCallReference("Serializable::deserialize");
        assertEq(1, ref(Serializable::serialize(1)));
        assertThrows("CALL-REFERENCE-ERROR", \get_call_reference(), "get_int");
    }

    issue2993() {
        Program p(PO_NEW_STYLE);
        *hash<ExceptionInfo> h = p.parse("background printf('%y\n', gettid());", "issue 2993",
            WARN_RETURN_VALUE_IGNORED);
        assertNothing(h);
    }

    issue3136() {
        Program p(PO_NO_INHERIT_USER_HASHDECLS|PO_NO_INHERIT_SYSTEM_HASHDECLS|PO_ALLOW_WEAK_REFERENCES);
        assertEq((
            "PO_NO_INHERIT_USER_HASHDECLS",
            "PO_NO_INHERIT_SYSTEM_HASHDECLS",
            "PO_ALLOW_WEAK_REFERENCES",
        ), p.getParseOptionStringList());
    }

    issue3027Test() {
        if (!Option::HAVE_STACK_GUARD) {
            testSkip("Qore library was not built with stack protection support");
        }
        Program p(PO_NEW_STYLE);
        assertThrows("STACK-LIMIT-EXCEEDED", \p.parse(), ('sub foo() { hash h = T{"foo"}; return h.a; } '
            'const T = {"foo": { "a": 1, "b": foo()},}; foo();', ""));
    }

    issueModuleCmdTest() {
        Program p(PO_NEW_STYLE);

        assertThrows("PARSE-COMMAND-ERROR", \p.issueModuleCmd(), ("Util", "xxx"));
    }

    issue2807() {
        Program p(PO_NO_GLOBAL_VARS);
        p.loadModule("CsvUtil");
        assertTrue(True);
    }

    issue2735Test() {
        {
            Program p(PO_NEW_STYLE);
            assertThrows("DUPLICATE-SIGNATURE", \p.parse(), ("namespace T {sub t() {}} namespace T {sub t() {}}", "issue 2735"));
        }
        {
            Program p(PO_NEW_STYLE);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("namespace T {const A = 1;} namespace T {const A = 1;}", "issue 2735"));
        }
        {
            Program p(PO_NEW_STYLE);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("namespace T {class A;} namespace T {class A;}", "issue 2735"));
        }
        {
            Program p(PO_NEW_STYLE);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("namespace T {hashdecl A {int i;}} namespace T {hashdecl A {int i;}}", "issue 2735"));
        }
        {
            Program p(PO_NEW_STYLE);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("namespace T {our int a;} namespace T {our int a;}", "issue 2735"));
        }
    }

    programTest() {
        our Test to();
        our Test ro();

        string func = "namespace ITest { const val = 1.0; class X; } our int gv2 = 123; int sub t2(int a) { return a + 2; } int sub et(int a) { return t(a); } string sub tot() { return get_class_name(to); } Queue sub getObject() { return new Queue(); } X sub get_x() { return new X(); } sub deleteException() { ro.getData(0); delete ro; } bool sub check_ro() { return exists ro; }";

        string pf = "newfunc();";
        string nf = "bool sub newfunc() { return True; }";

        Program a(PO_NEW_STYLE);
        assertEq(PO_NEW_STYLE, a.getParseOptions());
        # PO_NEW_STYLE is made up of the following two parse options
        assertEq(("PO_ALLOW_BARE_REFS", "PO_ASSUME_LOCAL"), a.getParseOptionStringList());
        a.parsePending(pf, "pending test part1", 0);
        a.parsePending(nf, "pending test part2", 0);
        a.importFunction("t");
        a.importGlobalVariable("to");
        a.importGlobalVariable("ro", True);
        a.parse(func, "test");

        assertEq(True, a.callFunction("newfunc"));
        assertEq(3, a.callFunction("t2", 1));
        assertEq(4, a.callFunctionArgs("t2", list(int(2))));
        assertEq(2, a.callFunction("et", 1));
        assertEq("Test", a.callFunction("tot"));
        assertEq("memberGate-member", to.member);
        assertEq("method", to.method());

        a.define("MY_TEST_EMPTY");
        a.define("MY_TEST_STR", "a");
        assertEq(True, a.isDefined("MY_TEST_EMPTY"));
        assertEq(True, a.isDefined("MY_TEST_STR"));
        assertEq(False, a.isDefined("MY_TEST_NONE"));

        assertEq(False, exists a.getDefine("MY_TEST_EMPTY"));
        assertEq("a", a.getDefine("MY_TEST_STR"));

        a.undefine("MY_TEST_STR");
        assertEq(False, a.isDefined("MY_TEST_STR"));

        *hash defs = a.getAllDefines();
        assertEq(True, defs.PO_NEW_STYLE);
        assertEq(True, exists defs.QorePlatformCPU);
        assertEq(True, inlist("MY_TEST_EMPTY", keys defs));
        assertEq(False, inlist("MY_TEST_STR", keys defs));

        try {
            a.callFunction("deleteException");
        }
        catch (hash<ExceptionInfo> ex) {
            assertEq("ACCESS-ERROR", ex.err);
        }

        assertEq(True, a.callFunction("check_ro"));

        Queue o = a.callFunction("getObject");
        object ox = a.callFunction("get_x");
        delete a;
        assertEq("Queue", get_class_name(o));
        assertEq("X", get_class_name(ox));

        # test for incorrect parse location when processing constants after a commit
        a = new Program(PO_NEW_STYLE);
        *hash h = a.parse("sub x() {} const X1 = 'a'; const X2 = 'a'; const h = (X1: 1, X2: 2);", "warn", WARN_ALL);
        assertEq("warn", h.file);

        # make sure recursive constant definitions are handled
        try {
            a = new Program(PO_NEW_STYLE);
            a.parse("const A = B; const B = A;", "rec");
        }
        catch (hash<ExceptionInfo> ex) {
            assertEq("PARSE-EXCEPTION", ex.err);
        }

        string pstr = "class T { private { int i = 1; static *int j = 4; const X = 2; } int get() { return i; } static other (int x) {} } T sub getT() { return new T(); } int sub checkT(T t) { return t.get(); } list<int> sub get_thread_list0(Program p) { return p.getThreadList(); } list<int> sub get_thread_list1(Program p) { Counter c0(1); Counter c1(1); background waiter(c0, c1); c0.waitForZero(); on_exit c1.dec(); return p.getThreadList(); } sub waiter(Counter c0, Counter c1) { c0.dec(); c1.waitForZero(); }";

        Program p1(PO_NEW_STYLE);
        Program p2(PO_NEW_STYLE);

        p1.parse(pstr, "p");
        p2.parse(pstr, "p");

        object o2 = p1.callFunction("getT");
        assertEq(1, p1.callFunction("checkT", o2));
        assertEq(1, p2.callFunction("checkT", o2));

        list<int> l = p1.callFunction("get_thread_list0", p1);
        assertEq(1, l.size());
        assertEq(gettid(), l[0]);
        l = p1.callFunction("get_thread_list1", p1);
        assertEq(2, l.size());
        assertEq(gettid(), l[0]);

        Program p4(PO_NEW_STYLE);
        try {
            p4.parse("error", "error", 0, "source", 10);
            assertEq(True, False);
        } catch (hash<ExceptionInfo> ex) {
            on_error printf("%s\n", get_exception_string(ex));
            assertEq("source", ex.source);
            assertEq(10, ex.offset);
        }

        {
            Program p5(PO_NEW_STYLE);
            # issue #2175: the following code will crash without the fix
            assertThrows("PARSE-EXCEPTION", \p5.parse(), ("#", ""));
            # issue #2175: the following code will crash without the fix
            assertThrows("PARSE-EXCEPTION", \p5.parse(), ("/*", ""));
        }

        {
            Program p(PO_NEW_STYLE);
            # ensure that programs cannot be accessed while parsing
            p.parsePending("class T { int t() { int a = 1; return a; }} T t();", "");
            assertThrows("PROGRAM-ERROR", \p.run());
        }
    }

    combineTest() {
        Program p(PO_NEW_STYLE);
        testAssertion("combine", \p.parse(), (CombineSrc, "combine-test"), new TestResultExceptionRegexp("PARSE-ERROR", "B2"));
    }

    intAssignmentsTest() {
        {
            Program p(PO_NEW_STYLE);
            p.parse("any h = {}; int i = h;", "");

            assertThrows("RUNTIME-TYPE-ERROR", \p.run());
        }

        {
            Program p(PO_NEW_STYLE|PO_BROKEN_INT_ASSIGNMENTS);
            p.parse("any h = {}; int i = h;", "");

            assertEq(NOTHING, p.run());
        }

        {
            Program p(PO_NEW_STYLE|PO_BROKEN_INT_ASSIGNMENTS);
            p.parse("%correct-int-assignments\nany h = {}; int i = h;", "");
            assertThrows("RUNTIME-TYPE-ERROR", \p.run());
        }
    }

    constantExpressionTest() {
        {
            Program p(PO_NEW_STYLE);
            assertThrows("ILLEGAL-MEMBER-REFERENCE", \p.parse(), ("class T {public {int i;} t() {while (True) {switch (1) {case 1+i: break;}}}}", ""));
        }
    }

    typeErrorTest() {
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS);
            assertThrows("PARSE-TYPE-ERROR", \p.parse(), ("sub f() {} f(1);", ""));
        }
        {
            Program p(PO_NEW_STYLE|PO_NO_CHILD_PO_RESTRICTIONS|PO_STRICT_ARGS);
            p.parse("Program p(PO_NEW_STYLE); p.parse('sub f() {}', ''); p.callFunction('f', 1);", "");
            assertEq(NOTHING, p.run());
        }
        {
            Program p(PO_NEW_STYLE|PO_NO_CHILD_PO_RESTRICTIONS|PO_STRICT_ARGS);
            p.parse("Program p(PO_NEW_STYLE|PO_STRICT_ARGS); p.parse('sub f() {}', ''); p.callFunction('f', 1);", "");
            assertThrows("RUNTIME-OVERLOAD-ERROR", \p.run());
        }
        {
            Program p(PO_NEW_STYLE|PO_NO_CHILD_PO_RESTRICTIONS|PO_STRICT_ARGS);
            p.parse("Program p(PO_NEW_STYLE|PO_REQUIRE_TYPES); p.parse('sub f() {}', ''); p.callFunction('f', 1);", "");
            assertThrows("RUNTIME-OVERLOAD-ERROR", \p.run());
        }
    }

    brokenOperatorsTest() {
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("int i; i+ =1;", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("hours()", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_REQUIRE_TYPES);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("hours()", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("hours()", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS);
            assertEq(NOTHING, p.parse("%loose-args\nhours();", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_BROKEN_OPERATORS);
            assertEq(NOTHING, p.parse("int i; i+ =1;", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_BROKEN_OPERATORS);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("%correct-operators\nint i; int i+ =1;", ""));
        }
    }

    classTest() {
        # test constructor methods
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            # it is illegal to declare a static contructor() method
            assertThrows("ILLEGAL-STATIC-METHOD", \p.parse(), ("class X { static constructor(int i) {}}", ""));
        }

        # test destructor methods
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("class X { destructor(int i) {}}", ""));
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("class X { int destructor() {}}", ""));
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("class X { private destructor() {}}", ""));
            # it is illegal to declare a static destructor() method
            assertThrows("ILLEGAL-STATIC-METHOD", \p.parse(), ("class X { static destructor() {}}", ""));
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("class X { abstract destructor();}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { public destructor() {}}", ""));
        }

        # test copy methods
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-TYPE-ERROR", \p.parse(), ("class X { copy(int i) {}}", ""));
            assertThrows("PARSE-EXCEPTION", \p.parse(), ("class X { abstract copy() {}}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { private copy() {}}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { private copy(object obj) {}}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { private copy(*object obj) {}}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { private copy(*X obj) {}}", ""));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { private copy(X obj) {}}", ""));
        }

        # it is legal to declare a static copy() method
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertEq(NOTHING, p.parse("class X { static copy(X obj) {}}", ""));
        }
    }

    setThreadInitTest() {
        our int i = 0;
        our Counter cnt(1);
        Program p(PO_NEW_STYLE);
        p.importGlobalVariable("i");
        p.importGlobalVariable("cnt");
        bool b = p.setThreadInit(sub () { i = 1;});
        assertEq(False, b);
        p.parse("background cnt.dec();", "");
        p.run();
        cnt.waitForZero();
        assertEq(1, i);
        b = p.setThreadInit();
        assertEq(True, b);
        i = 0;
        cnt.inc();
        p.run();
        assertEq(0, i);
    }

    varTest() {
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi';", "");
            p.run();
            assertEq(("i": 1, "T::str": "hi"), p.getGlobalVars() - QoreGlobalVars);
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi';", "");
            p.run();
            p.setGlobalVarValue("i", 2);
            assertEq(("i": 2, "T::str": "hi"), p.getGlobalVars() - QoreGlobalVars);
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi';", "");
            p.run();
            assertThrows("RUNTIME-TYPE-ERROR", \p.setGlobalVarValue(), ("i", "hi"));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi';", "");
            p.run();
            assertThrows("UNKNOWN-VARIABLE", \p.setGlobalVarValue(), ("x", "hi"));
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi'; set_global_var_value('i', 'hi');", "");
            assertThrows("RUNTIME-TYPE-ERROR", \p.run());
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            p.parse("our int i = 1; namespace T { our string str; } T::str = 'hi'; set_global_var_value('x', 'hi');", "");
            assertThrows("UNKNOWN-VARIABLE", \p.run());
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("hash sub test(int frame) { int i = 1; string str = 'hi'; return get_local_vars(frame); }", "");

            hash vh = (
                "frame": ("type": "local", "value": 0),
                "i": ("type": "local", "value": 1),
                "str": ("type": "local", "value": "hi"),
                "argv": ("type": "local", "value": NOTHING),
                );

            assertEq(vh, p.callFunction("test", 0));
            assertEq(p, p.callFunction("test", 1).p.value);
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("hash sub test(int frame) { int i = 1; string str = 'hi'; set_local_var_value(0, 'i', 2); set_local_var_value(0, 'str', 'bye'); code f = sub() {i = 3;}; return get_local_vars(frame); }", "");

            hash vh = (
                "frame": ("type": "local", "value": 0),
                "i": ("type": "closure", "value": 2),
                "str": ("type": "local", "value": "bye"),
                "argv": ("type": "local", "value": NOTHING),
                );

            assertEq(vh, p.callFunction("test", 0) - "f");
            assertEq(p, p.callFunction("test", 1).p.value);
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("sub test() { int i = 1; set_local_var_value(0, 'x', 2); }", "");
            assertThrows("UNKNOWN-VARIABLE", \p.callFunction(), "test");
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("sub test() { int i = 1; set_local_var_value(0, 'x', 2); code f = sub() {i = 3;};}", "");
            assertThrows("UNKNOWN-VARIABLE", \p.callFunction(), "test");
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("sub test() { int i = 1; set_local_var_value(0, 'i', 'hi'); code f = sub() {i = 3;};}", "");
            assertThrows("RUNTIME-TYPE-ERROR", \p.callFunction(), "test");
        }

        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_ALLOW_DEBUGGER);
            p.parse("hash sub test1(int frame) { code c = sub () { if (frame > 10) throw 1; }; c(); return get_local_vars(frame); } hash sub test2(int frame) { int i = 1; string str = 'hi'; return test1(frame); }", "");

            hash vh = (
                "str": ("type": "local", "value": "hi"),
                "i": ("type": "local", "value": 1),
                "argv": ("type": "local", "value": NOTHING),
                "frame": ("type": "local", "value": 1),
                );

            assertEq(vh, p.callFunction("test2", 1));

            vh = (
                "c": ("type": "local", "value": NOTHING),
                "frame": ("type": "closure", "value": 0),
                "argv": ("type": "local", "value": NOTHING),
                );

            hash pvh = p.callFunction("test2", 0);
            pvh.c.value = NOTHING;
            assertEq(vh, pvh);
            assertEq({}, p.callFunction("test2", -1));
            assertEq(p, p.callFunction("test2", 2).p.value);
        }
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-EXCEPTION", "do not allow access", \p.parse(), ("get_local_vars(0);", ""));
        }
        {
            Program p(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES);
            assertThrows("PARSE-ERROR", "local variable.*already declared", \p.parse(), ("hash h; hash h;", ""));
        }
    }

    programInfoTest() {
        Program a(PO_NEW_STYLE);
        # sort by program ID otherwise tests may fail
        list<auto> pp = sort(ProgramControl::getAllPrograms(), int sub (ProgramControl l, ProgramControl r) { return l.getProgramId() <=> r.getProgramId(); });
        assertGt(1, a.getProgramId(), "getProgramId()");
        assertEq(True, pp.size()>=2, "getAllPrograms()");
        assertEq(pp[0].getProgramId(), ProgramControl::getProgram().getProgramId(), "getProgramId(0)");
        assertEq(a.getProgramId(), pp[pp.size()-1].getProgramId(), "getProgramId(1)");
        assertEq(ProgramControl::resolveProgramId(a.getProgramId()).getProgramId(), a.getProgramId(), "resolveProgramId()");
    }

    testCV(*string ns, *string cls, string func, string name) {
        string s = func;
        if (exists cls) {
            s = cls + "::" + s;
        }
#        if (exists ns) {
#            s = ns + "::" + s;   # namespace is ignored
#        }
        ProgramControl p = ProgramControl::getProgram();
        list val = p.findFunctionVariants(name);
        list l;
        push l, ("desc": s+"()", "params": list());
        push l, ("desc": s+"(int a)", "params": list("int"));
        push l, ("desc": s+"(string a)", "params": list("string"));
        push l, ("desc": s+"(int a, string b)", "params": list("int", "string"));
        push l, ("desc": s+"(int a, binary b, *string c)", "params": list("int", "binary", "*string"));
        testAssertionValue(sprintf("findFunctionVariants(%s)", name), val, l);

        testVS(name, val);
    }

    testVS(string name, list<auto> val) {
        ProgramControl p = ProgramControl::getProgram();
        foreach hash v in (val) {
            int st = p.findFunctionStatementId(name, v.params);
            # test for issue #2600
            try {
                p.findFunctionStatementId(name, ("Mutex",));
                assertFalse(True);
            }
            catch (hash<ExceptionInfo> ex) {
                assertEq("VARIANT-MATCH-ERROR", ex.err);
                assertNRegex(name + "\\(" + name, ex.desc);
            }
            testAssertionValue(sprintf("findFunctionStatementId(%s)", name), st > 0, True);
            # issue #2601: test empty args in match
            assertThrows("FIND-CALL-ERROR", \p.findFunctionStatementId(), (name, ("",)));
        }
    }

    findRuntimeTest() {
        ProgramControl p = ProgramControl::getProgram();
        assertThrows('PROGRAM-ERROR', \p.findFunctionVariants(), ("doesnotexist"), "findFunctionVariants(doesnotexist)");

        testCV(NOTHING, NOTHING, "funcTest", "funcTest");
        testCV("NS", NOTHING, "funcNSTest", "funcNSTest");
        testCV("NS", NOTHING, "funcNSTest", "NS::funcNSTest");
        testCV(NOTHING, "Test", "funcTest", "Test::funcTest");
        testCV("NS", "TestNS", "funcTest", "TestNS::funcTest");
        testCV("NS", "TestNS2", "funcTest", "TestNS2::funcTest");
        testCV("NS", "TestNS", "funcTest", "NS::TestNS::funcTest");

        try {
            list<auto> val;
            val = p.findFunctionVariants("Test::constructor");
            assertEq(2, val.size(), "findFunctionVariants(Test::constructor)");
            testVS("Test::constructor", val);
            val = p.findFunctionVariants("Test::destructor");
            assertEq(1, val.size(), "findFunctionVariants(Test::destructor)");
            testVS("Test::destructor", val);
            val = p.findFunctionVariants("Test::copy");
            assertEq(1, val.size(), "findFunctionVariants(Test::copy)");
            testVS("Test::copy", val);
            val = p.findFunctionVariants("Test::memberGate");
            assertEq(1, val.size(), "findFunctionVariants(Test::memberGate)");
            testVS("Test::memberGate", val);
            val = p.findFunctionVariants("Test::methodGate");
            assertEq(1, val.size(), "findFunctionVariants(Test::methodGate)");
            testVS("Test::methodGate", val);
            val = p.findFunctionVariants("Test::memberNotification");
            assertEq(1, val.size(), "findFunctionVariants(Test::memberNotification)");
            testVS("Test::memberNotification", val);
        } catch (hash<ExceptionInfo> ex) {
            printf("%s\n", get_exception_string(ex));
        }

        try {
            # extra stuff
            list val;
            val = p.findFunctionVariants("Test::methodGate");
            assertEq(1, val.size(), "findFunctionVariants(Test::methodGate)");
            testVS("Test::methodGate", val);
            val = p.findFunctionVariants("Test::memberGate");
            assertEq(1, val.size(), "findFunctionVariants(Test::memberGate)");
            testVS("Test::memberGate", val);
            val = p.findFunctionVariants("Test::memberNotification");
            assertEq(1, val.size(), "findFunctionVariants(Test::memberNotification)");
            testVS("Test::memberNotification", val);
        } catch (hash<ExceptionInfo> ex) {
            printf("%s\n", get_exception_string(ex));
        }

        try {
            # non existing func
            assertThrows('PROGRAM-ERROR', \p.findFunctionVariants(), ("NS::funcTest"), 'findFunctionVariants(NS::funcTest)');
            assertThrows('PROGRAM-ERROR', \p.findFunctionVariants(), ("NS::Test::funcTest"), 'findFunctionVariants(NS::Test::funcTest)');
            assertThrows('PROGRAM-ERROR', \p.findFunctionVariants(), ("NSNOTEXIST::funcTest"), 'findFunctionVariants(NSNOTEXIST::funcTest)');
            #assertThrows('PROGRAM-ERROR', \p.findFunctionVariants(), ("TestNS3::funcTest"), 'findFunctionVariants(TestNS3::funcTest)');  # TODO: resolved function from inherited class
        } catch (hash<ExceptionInfo> ex) {
            printf("%s\n", get_exception_string(ex));
        }

        list v = p.findFunctionVariants("funcParamTypeTest");
        assertEq(1, v.size());
        try {
            assertEq(
                "funcParamTypeTest(int i, *int i2, string s, *string s2, bool bo, *bool bo2, float f, *float f2, "
                    "number n, *number n2, binary bi, *binary b2, list l, *list l2, hash h, *hash h2, object o, "
                    "*object o2, any a, date d, *date d2, code co, *code co2, data da, *data da2, reference r, "
                    "*reference r2, softint si, *softint si2, softfloat sf, *softfloat sf2, softnumber sn, "
                    "*softnumber sn2, softbool sb, *softbool sb2, softstring ss, *softstring ss2, softdate sd, "
                    "*softdate sd2, softlist sl, *softlist sl2, timeout t, *timeout t2, code cl, code cr, "
                    "object<::Test> test, object<::NS::TestNS> testns, object<::NS::TestNS> nsttestns)",
                v[0].desc # TODO: class: Test ????, *softlist,  namespaces ignored, closure, callref as code
            );
        } catch (hash<ExceptionInfo> ex) {
            printf("%s\n", get_exception_string(ex));
        }
        try {
            assertEq(
                (
                    "int", "*int", "string", "*string", "bool", "*bool", "float", "*float", "number", "*number",
                    "binary", "*binary", "list", "*list", "hash", "*hash", "object", "*object", "any", "date",
                    "*date", "code", "*code", "data", "*data", "reference", "*reference", "softint", "*softint",
                    "softfloat", "*softfloat", "softnumber", "*softnumber", "softbool", "*softbool", "softstring",
                    "*softstring", "softdate", "*softdate", "softlist", "*softlist", "timeout", "*timeout", "code",
                    "code", "object<::Test>", "object<::NS::TestNS>", "object<::NS::TestNS>",
                ),
                v[0].params
            );
        } catch (hash<ExceptionInfo> ex) {
            printf("%s\n", get_exception_string(ex));
        }
    }
}
