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

%new-style
%enable-all-warnings
%require-types
%strict-args
%allow-debugger

%requires ../../../../qlib/QUnit.qm

const ST_ATTACH = 1;
const ST_BLOCK = 2;
const ST_STEP = 3;
const ST_FUNC_ENTER = 4;
const ST_FUNC_EXIT = 5;
const ST_EXCEPTION = 6;
const ST_EXIT = 7;
const ST_DETACH = 8;

const stResolve = (
    ST_ATTACH: "onAttach",
    ST_BLOCK: "onBlock",
    ST_STEP: "onStep",
    ST_FUNC_ENTER: "onFunctionEnter",
    ST_FUNC_EXIT: "onFunctionExit",
    ST_EXCEPTION: "onException",
    ST_EXIT: "onExit",
    ST_DETACH: "onDetach",
);

# test top-level statement
hash sub tstTlsGetStack() {return get_all_thread_call_stacks(){gettid()}[0];}
nothing sub tstTlsFunction() {
    int i = tstTlsClosure();  #2
    while (i > 1) {  #3
        i--;   #4
    } #5
} #6

code tstTlsClosure = int sub() { #8
    int i = 1; #9
    return i; #10
}; #11
# end test top-level statement


class MyDebugProgram inherits DebugProgram {
    public {
        list traceLog;
        list actions;
        *softlist attach_only_tids;
        Counter handleCount();
    }

    constructor(): DebugProgram() {
        printf("MyDebugProgram::constructor()\n");
    }

    nothing setActions(list act, *softlist attach_only) {
        traceLog = ();
        actions = act;
        attach_only_tids = attach_only;
    }
    auto getAction(int func) {
        if (!actions) return;
        #printf("Act: %N\n", actions);
        if (!exists actions[0].func || actions[0].func == func) {
            #printf("Act2: %N\n", actions);
            return actions[0];
        }
    }
    handle(int func, ProgramControl pgm, reference runState, reference runToStatementId, *int statementId, *hash extra) {
        *hash location;
        handleCount.inc();
        if (statementId)
            location = pgm.getStatementIdInfo(statementId);
        #printf("%y: pgm: %y, %y, %y, %y, %y\n", func, pgm, runState, runToStatementId, location, extra);
        hash it = (
            'func': stResolve{func},
            'file': location.file,
            'line': location.start_line,
            'tid': gettid(),
            'runToStatementId': runToStatementId,
        );
        auto act = getAction(func);
        if (exists act.localVars) {
            hash v = get_local_vars(2); # get local vars from program being debugged
            hash vv;
            foreach string lv in (act.localVars) {
                vv{lv}= v{lv}.value;
            }
            it.localVars = vv;
        }
        it += extra;
        push traceLog, it;
        runToStatementId = 0;
        if (exists act) {
            shift actions;
            if (act.typeCode() == NT_HASH) {
                if (exists act.runState) {
                    runState = act.runState;
                }
                if (exists act.runToStatementId) {
                    runToStatementId = act.runToStatementId;
                }
            } else {
                runState = act;
            }
        }
        #printf("it:%y, runState:%d, runToStatementId:%d, act:%y\n", it, runState, runToStatementId, act);
    }

    onAttach(ProgramControl pgm, reference runState, reference runToStatementId) {
        if (attach_only_tids && !attach_only_tids.contains(gettid())) {
            #printf("Avoid attach %d\n", gettid());
            runState = DebugDetach;
        } else {
            handle(ST_ATTACH, pgm, \runState, \runToStatementId);
        }
    }

    onDetach(ProgramControl pgm, reference runState, reference runToStatementId) {
        handle(ST_DETACH, pgm, \runState, \runToStatementId);
    }

    onStep(ProgramControl pgm, int blockStatementId, *int statementId, *int breakpointId, reference flow, reference runState, reference runToStatementId) {
        int st = statementId ? ST_STEP : ST_BLOCK;
        auto act = getAction(st);
        if (exists act.flow) {
            #printf("loc:%y, flow: %y\n", statementId, act.flow);
            flow = act.flow;
        }
        handle(st, pgm, \runState, \runToStatementId, statementId ?? blockStatementId, {"bkptId": breakpointId});
    }

    onFunctionEnter(ProgramControl pgm, int statementId, reference runState, reference runToStatementId) {
        handle(ST_FUNC_ENTER, pgm, \runState, \runToStatementId, statementId);
    }

    onFunctionExit(ProgramControl pgm, int statementId, reference returnValue, reference runState, reference runToStatementId) {
        auto act = getAction(ST_FUNC_EXIT);
        if (exists act.retVal) {
            returnValue = act.retVal;
        }
        handle(ST_FUNC_EXIT, pgm, \runState, \runToStatementId, statementId, ("retVal": returnValue));
    }

    onException(ProgramControl pgm, int statementId, hash ex, reference dismiss, reference runState, reference runToStatementId) {
        auto act = getAction(ST_EXCEPTION);
        if (exists act.dismiss) {
            dismiss = act.dismiss;
        }
        #printf("Exception: %y\n", ex);
        handle(ST_EXCEPTION, pgm, \runState, \runToStatementId, statementId, ("ex": ex));
    }

    onExit(ProgramControl pgm, int statementId, reference returnValue, reference runState, reference runToStatementId) {
        auto act = getAction(ST_EXIT);
        if (exists act.retVal) {
            returnValue = act.retVal;
        }
        handle(ST_EXIT, pgm, \runState, \runToStatementId, statementId, ("retVal": returnValue));
    }

}

%exec-class DebugTest

class DebugTest inherits QUnit::Test {
    public {}


    private {
        hash stack;
        hash line;
        int main_tid;
        Mutex semaphore;
        Counter counter();
        ProgramControl thisProgram;
        MyDebugProgram debugProgram;
    }

    constructor() : Test("Debug Test", "1.0") {
        addTestCase("ProgramTest", \programTest());
        addTestCase("StatementTest", \statementTest());
        addTestCase("TLSStatementTest", \tlsStatementTest());
        addTestCase("BreakpointTest", \breakpointTest());
        addTestCase("SingleThreadDebugTest", \singleThreadDebugTest());
        addTestCase("SingleThreadBreakpointTest", \singleThreadBreakpointTest());
        addTestCase("MultiThreadBreakTest", \multiThreadBreakTest());   # problem in valrind because of timing ?
        #addTestCase("MultiThreadDebugTest", \multiThreadDebugTest());

        thisProgram = ProgramControl::getProgram();
        debugProgram = new MyDebugProgram();
        semaphore = new Mutex();
        main_tid = gettid();
        stack = tstGetStack();
        line.tstFunction2 = stack.line+1;
        line.tstFunction = line.tstFunction2+7;
        line.tstNoRetVal = line.tstFunction+12;
        line.tstLoop = line.tstNoRetVal+2;
        line.tstProgram = line.tstLoop+9;
        line.tstBreakProgram = line.tstProgram+14;

        hash tlsStack = tstTlsGetStack();
        line.tstTlsFunction = tlsStack.line+1;
        line.tstTlsClosure = tlsStack.line+8;

        #printf("line: %N\n", line);
        set_return_value(main());
    }

    # defined stuff to be executed and steps inspected by debugger
    # do not  add/delete lines
    hash tstGetStack() {return get_all_thread_call_stacks(){gettid()}[0];}
    int tstFunction2(int recurseCnt) {
        if (recurseCnt > 0) {
            return tstFunction2(recurseCnt-1)+1;
        } else {
            return 0;
        }
    }
    string tstFunction(int i, string s) {
        if (i > 0) {
            s += sprintf(",i:%d", i);
        } else {
            s = "";
        }
        while (i > 0) {
            i--;
        }
        tstFunction2(3);
        return "DONE";
    }
    tstNoRetVal() {
    }
    int tstLoop() {
        int res = 0;
        int i = 3;
        while (i > 0) {
            res += i;
            i--;
        }
        return res;
    }
    auto tstProgram() {
        int i = tstLoop();
        string s = i.type();
        int tid = gettid();
        string ret = tstFunction(1, "Func");
        tstNoRetVal();
        s = sprintf("%s", ret);  # just test if ret do not coredumps
        try {
            i = (tid+s.size()+ret.size()) / (tid-tid);
        } catch (hash ex) {
            s = ex.err;
        }
        return s;
    }
    bool tstBreakProgram() {
        int max_ms = 1000;
        int i=0;
        int stamp = clock_getmillis();
        while (clock_getmillis() - stamp < max_ms) {
            if (counter.getCount() > 0) {
                counter.dec();
            }
            usleep(1);  # yield
            i++;
        }
        return clock_getmillis() - stamp < max_ms;
    }
    # end of tested program

    assertTraceLog(string name, list traceLog) {
    #printf("actuallog: %N\n", debugProgram.traceLog);
        hash l = shift debugProgram.traceLog;
        testAssertionValue(name+':method check', l.func, "onAttach");

        int i = 0;
        while (my *hash el = shift traceLog) {
            *hash al = shift debugProgram.traceLog;
            if (!exists al) {
                testAssertionValue(name+':tracelog missing steps', el + traceLog, ());
                break;
            }
            if (exists el.tid) {
                testAssertionValue(sprintf(name+':tracelog %d tid', i), al.tid, el.tid);
            }
            if (exists el.func) {
                testAssertionValue(sprintf(name+':tracelog %d func', i), al.func, stResolve{el.func});
            }
            if (exists el.line) {
                testAssertionValue(sprintf(name+':tracelog %d line', i), al.line, el.line);
            }
            if (exists el.file) {
                testAssertionValue(sprintf(name+':tracelog %d file', i), al.file, el.file);
            }
            if (exists el.retVal) {
                testAssertionValue(sprintf(name+':tracelog %d retval', i), al.retVal, el.retVal);
            }
            if (exists el.bkptId) {
                testAssertionValue(sprintf(name+':tracelog %d bkptid', i), al.bkptId, el.bkptId);
            }
            if (exists el.localVars) {
                testAssertionValue(sprintf(name+':tracelog %d local vars', i), al.localVars, el.localVars);
            }
            if (exists el.ex) {
                testAssertionValue(sprintf(name+':tracelog %d exception err', i), al.ex.err, el.ex);
            }
            if (exists el.runToStatementId) {
                testAssertionValue(sprintf(name+':tracelog %d bkptid', i), al.runToStatementId, el.runToStatementId);
            }
            i++;
        }
        testAssertionValue(sprintf(name+':tracelog end check, i:%d', i), debugProgram.traceLog, ());
    }
    checkProgram(string name, list actions, list traceLog, auto expectedRet) {
        printf("checkProgram(%y)\n", name);
        debugProgram.setActions(actions);
#printf("Actions: %N\n", debugProgram.actions);
        debugProgram.addProgram(thisProgram);
        auto ret = tstProgram();
        debugProgram.removeProgram(thisProgram);
#printf("stack: %N\n", stack);
#printf("expected log: %N\n", traceLog);
        assertTraceLog(name, traceLog);
        testAssertionValue(sprintf(name+':return value'), ret, expectedRet);
        list l = debugProgram.getAllPrograms();
        testAssertionValue(sprintf(name+':getAllPrograms size 0'), l.size(), 0);
        debugProgram.addProgram(thisProgram);
        l = debugProgram.getAllPrograms();
        testAssertionValue(sprintf(name+':getAllPrograms size 1'), l.size(), 1);
        testAssertionValue(sprintf(name+':getAllPrograms programid'), l[0].getProgramId(), thisProgram.getProgramId());
        debugProgram.removeProgram(thisProgram);
        l = debugProgram.getAllPrograms();
        testAssertionValue(sprintf(name+':getAllPrograms size 0'), l.size(), 0);
    }

    debugBreakThread(int tid) {
        #printf("debugBreakThread running %d\n", tid);
        debugProgram.addProgram(thisProgram);
        counter.inc();  # to avoid race loop vs. attach
        if (counter.waitForZero(200ms) != 0) {   # leave some time to reach loop block
            printf("ERROR: loop not reached\n");
        }
        if (tid >= 0) {
            #printf("debugBreakThread breakProgramThread(%d)\n", tid);
            debugProgram.breakProgramThread(thisProgram, tid);
        } else {
            #printf("debugBreakThread breakProgram()\n");
            debugProgram.breakProgram(thisProgram);
        }
        #printf("debugBreakThread waiting for program finish\n");
        if (semaphore.lock(1200ms) < 0) {  # wait till tst program finishes because of break infinite loop  etc.
            printf("ERROR: semaphore locked\n");
        } else {
            #printf("debugBreakThread done unlock itself\n");
            semaphore.unlock();
        }
        debugProgram.removeProgram(thisProgram);
    }

    checkBreakProgram(string name, int tid, bool breakRes) {
        printf("checkBreakProgram(%d)\n", tid);
        debugProgram.waitForTerminationAndClear();
        debugProgram.setActions(list(DebugRun, ("flow": DebugFlowBreak, "runState": DebugDetach)), main_tid);
        semaphore.lock();
        counter.inc();
        background debugBreakThread(tid);
        auto res = tstBreakProgram();
        semaphore.unlock();
        debugProgram.waitForTerminationAndClear();
        testAssertionValue(sprintf(name+':program finished'), res, breakRes);  # do when debug is over not to make QUnit exception
        if (breakRes) {
            assertTraceLog(name, list((/*SB_BLOCK || SB_STEP*/"tid": main_tid  /*, "line": line.tstBreakProgram+9 might depend on funcEntry where break might not work*/ )));
        } else {
            assertTraceLog(name, list());
        }
    }

    singleThreadDebugTest() {
        list traceLog = (
            ("func": ST_STEP, "tid": main_tid, "file": stack.file),  # 0
            ("func": ST_FUNC_ENTER, "line": line.tstProgram+1),
            ("func": ST_BLOCK, "line": line.tstProgram+1),
            ("func": ST_STEP, "line": line.tstProgram+1),
            ("func": ST_FUNC_ENTER, "line": line.tstLoop+1),
            ("func": ST_BLOCK, "line": line.tstLoop+1),
            ("func": ST_STEP, "line": line.tstLoop+1),
            ("func": ST_STEP, "line": line.tstLoop+2),
            ("func": ST_STEP, "line": line.tstLoop+3), # while
            ("func": ST_BLOCK, "line": line.tstLoop+4),
            ("func": ST_STEP, "line": line.tstLoop+4), #10
            ("func": ST_STEP, "line": line.tstLoop+5),
            ("func": ST_BLOCK, "line": line.tstLoop+4),
            ("func": ST_STEP, "line": line.tstLoop+4),
            ("func": ST_STEP, "line": line.tstLoop+5),
            ("func": ST_BLOCK, "line": line.tstLoop+4),
            ("func": ST_STEP, "line": line.tstLoop+4),
            ("func": ST_STEP, "line": line.tstLoop+5),
            ("func": ST_STEP, "line": line.tstLoop+7),  # ret
            ("func": ST_FUNC_EXIT, "line": line.tstLoop+1),
            ("func": ST_STEP, "line": line.tstProgram+2),  #20
            ("func": ST_STEP, "line": line.tstProgram+3),
            ("func": ST_STEP, "line": line.tstProgram+4),
            ("func": ST_FUNC_ENTER, "line": line.tstFunction+1),
            ("func": ST_BLOCK, "line": line.tstFunction+1),
            ("func": ST_STEP, "line": line.tstFunction+1),
            ("func": ST_BLOCK, "line": line.tstFunction+2),
            ("func": ST_STEP, "line": line.tstFunction+2),
            ("func": ST_STEP, "line": line.tstFunction+6),
            ("func": ST_BLOCK, "line": line.tstFunction+7), # while
            ("func": ST_STEP, "line": line.tstFunction+7), #30
            ("func": ST_STEP, "line": line.tstFunction+9),
            ("func": ST_FUNC_ENTER, "line": line.tstFunction2+1),  #3
            ("func": ST_BLOCK, "line": line.tstFunction2+1),
            ("func": ST_STEP, "line": line.tstFunction2+1),
            ("func": ST_BLOCK, "line": line.tstFunction2+2),
            ("func": ST_STEP, "line": line.tstFunction2+2),
            ("func": ST_FUNC_ENTER, "line": line.tstFunction2+1),  #2
            ("func": ST_BLOCK, "line": line.tstFunction2+1),
            ("func": ST_STEP, "line": line.tstFunction2+1),
            ("func": ST_BLOCK, "line": line.tstFunction2+2), #40
            ("func": ST_STEP, "line": line.tstFunction2+2),
            ("func": ST_FUNC_ENTER, "line": line.tstFunction2+1),  #1
            ("func": ST_BLOCK, "line": line.tstFunction2+1),
            ("func": ST_STEP, "line": line.tstFunction2+1),
            ("func": ST_BLOCK, "line": line.tstFunction2+2),
            ("func": ST_STEP, "line": line.tstFunction2+2),
            ("func": ST_FUNC_ENTER, "line": line.tstFunction2+1),  #0
            ("func": ST_BLOCK, "line": line.tstFunction2+1),
            ("func": ST_STEP, "line": line.tstFunction2+1),
            ("func": ST_BLOCK, "line": line.tstFunction2+4), #50
            ("func": ST_STEP, "line": line.tstFunction2+4),
            ("func": ST_FUNC_EXIT, "line": line.tstFunction2+1, "retVal": 0), # 0
            ("func": ST_FUNC_EXIT, "line": line.tstFunction2+1, "retVal": 1), # 1
            ("func": ST_FUNC_EXIT, "line": line.tstFunction2+1, "retVal": 2), # 2
            ("func": ST_FUNC_EXIT, "line": line.tstFunction2+1, "retVal": 3), # 3
            ("func": ST_STEP, "line": line.tstFunction+10),
            ("func": ST_FUNC_EXIT, "line": line.tstFunction+1, "retVal": "DONE"),
            ("func": ST_STEP, "line": line.tstProgram+5),
            ("func": ST_FUNC_ENTER, "line": line.tstNoRetVal+0),  # when empty block then func and block has start_line exceptionally one line up
            ("func": ST_BLOCK, "line": line.tstNoRetVal+0),  #60
            ("func": ST_FUNC_EXIT, "line": line.tstNoRetVal+0),
            ("func": ST_STEP, "line": line.tstProgram+6),
            ("func": ST_STEP, "line": line.tstProgram+7),
            ("func": ST_BLOCK, "line": line.tstProgram+8),
            ("func": ST_STEP, "line": line.tstProgram+8),
            ("func": ST_EXCEPTION, "line": line.tstProgram+8, "ex": "DIVISION-BY-ZERO"),
            ("func": ST_BLOCK, "line": line.tstProgram+10),
            ("func": ST_STEP, "line": line.tstProgram+10),
            ("func": ST_STEP, "line": line.tstProgram+12),
            ("func": ST_FUNC_EXIT, "line": line.tstProgram+1),  #70
            ("func": ST_STEP),
        );
        list l;
        checkProgram("full trace", list(DebugStep), traceLog, "DIVISION-BY-ZERO");
        checkProgram("full trace dismiss exception", list(DebugStep, ("func": ST_EXCEPTION, "dismiss": True) ), splice(traceLog, 67, 2), "DONE");
        checkProgram("until return", list(DebugStep, ("func": ST_FUNC_ENTER), ("func": ST_FUNC_ENTER), ("func": ST_FUNC_ENTER, "runState": DebugUntilReturn), DebugStep), splice(traceLog, 24, 33), "DIVISION-BY-ZERO");
        l = splice(traceLog, 0, 66); # remove remove till exception
        l = splice(l, 1, 3);  # till func exit
        l = splice(l, 2);
        checkProgram("run till exception modify retVal", list(DebugRun, ("func": ST_EXCEPTION, "runState": DebugUntilReturn), ("func": ST_FUNC_EXIT, "retVal": "MODIFIED", "runState": DebugRun)), l, "MODIFIED");
        l = splice(traceLog, 4, 16); # remove tstLoop
        l = splice(l, 7, 35); # remove tstFunction
        l = splice(l, 8, 3);  # remove tstNoRetVal
        checkProgram("stepover", list(DebugStep, ("func": ST_FUNC_ENTER, "runState": DebugStepOver)), l, "DIVISION-BY-ZERO");
        l = splice(traceLog, 4, 16); # remove tstLoop
        l = splice(l, 7, 47);
        checkProgram("return", list(DebugStep, ("func": ST_FUNC_ENTER), DebugStep, DebugStepOver, DebugStep, DebugStep, ("func": ST_STEP, "flow": DebugFlowReturn, "runState": DebugStep)), l, NOTHING);
        l = splice(traceLog, 20);
        l = splice(l, 15, 4);
        checkProgram("loop continue", list(DebugStep, ("func": ST_FUNC_ENTER), ("func": ST_FUNC_ENTER),
            ("func": ST_BLOCK), # in loop now i = 3  ... add two steps via continue
            ("func": ST_BLOCK), DebugStep, ("flow": DebugFlowContinue),  # res+3, skip i--
            ("func": ST_BLOCK), DebugStep, ("flow": DebugFlowContinue, "runState": DebugUntilReturn),  # res+3, skip i--, # res+3+2+1
            ("func": ST_FUNC_EXIT, "retVal": 15, "runState": DebugDetach)),
            l, "DIVISION-BY-ZERO");
        l = splice(traceLog, 20);
        l = splice(l, 12, 7);
        checkProgram("loop break", list(DebugStep, ("func": ST_FUNC_ENTER), ("func": ST_FUNC_ENTER),
            ("func": ST_BLOCK), # in loop now i = 3
            ("func": ST_BLOCK), DebugStep, ("flow": DebugFlowBreak, "runState": DebugUntilReturn),  # res+3, skip i--
            ("func": ST_FUNC_EXIT, "retVal": 3, "runState": DebugDetach)),
            l, "DIVISION-BY-ZERO");
        checkProgram("detach", list(DebugStep, ("func": ST_FUNC_ENTER, "runState": DebugDetach)), splice(traceLog, 2), "DIVISION-BY-ZERO");
        /*l = ();
        for (my int i=0; i<traceLog.size()-2; i++) {  # why -2 ... exception has no checkAttach
            push l, ("func": ST_ATTACH);
        }*/
        checkProgram("do not attach", list(DebugDetach), list(), "DIVISION-BY-ZERO");

        int sid_tstLoop_4 = thisProgram.findStatementId('', line.tstLoop+4);
        int sid_tstFunction_1 = thisProgram.findStatementId('', line.tstFunction+1);
        int sid_tstProgram_12 = thisProgram.findStatementId('', line.tstProgram+12);

        l = (
            ("func": ST_STEP, "line": line.tstLoop+4, 'runToStatementId': sid_tstLoop_4),
            ("func": ST_STEP, "line": line.tstLoop+4, 'runToStatementId': sid_tstLoop_4),
            ("func": ST_FUNC_EXIT, "line": line.tstLoop+1),
            ("func": ST_STEP, "line": line.tstFunction+1, 'runToStatementId': sid_tstFunction_1),
            ("func": ST_EXCEPTION, "line": line.tstProgram+8, "ex": "DIVISION-BY-ZERO"),
        );
        checkProgram("run to statement", list(
            ('runState': DebugRun, 'runToStatementId': sid_tstLoop_4),
            ('runState': DebugUntilReturn, 'runToStatementId': sid_tstLoop_4),  # but stop at statement
            ('runState': DebugUntilReturn),
            ('runState': DebugRun, 'runToStatementId': sid_tstFunction_1),
            ('runState': DebugRun, 'runToStatementId': sid_tstProgram_12), # but stop on exception
            DebugRun,
            ), l, "DIVISION-BY-ZERO");
    }

    singleThreadBreakpointTest() {
        Breakpoint b1 = new Breakpoint();
        Breakpoint b2 = new Breakpoint();

        list traceLog2 = (
            ("func": ST_STEP, "line": line.tstFunction+1, "bkptId": b2.getBreakpointId()),
            ("func": ST_FUNC_EXIT, "line": line.tstFunction+1),
            ("func": ST_STEP, "line": line.tstProgram+5, "bkptId": b1.getBreakpointId(), "localVars": ("tid": gettid(), "s": "integer", "ret": "MODIFIED")),
        );
        list action2 = list(DebugRun, ("runState": DebugUntilReturn), ("func": ST_FUNC_EXIT, "retVal": "MODIFIED", "runState": DebugRun), ("localVars": ("tid", "s", "ret"), "runState": DebugDetach) );

        b1.setEnabled(True);
        b1.addThreadId(gettid());
        b1.assignProgram(thisProgram);
        int sid = thisProgram.findStatementId('', line.tstProgram+5);
        b1.assignStatement(sid);

        hash<StatementInfo> si = thisProgram.getStatementIdInfo(sid, True);
        testAssertionValue('getBreakpointId', b1.getBreakpointId(), si.breakpoints[0].getBreakpointId());

        b2.setEnabled(True);
        b2.addThreadId(gettid());
        b2.assignProgram(thisProgram);
        b2.assignStatement(thisProgram.findStatementId('', line.tstFunction+1));

        checkProgram("breakpoint b1(enable), b2(enable)", action2, traceLog2, "DIVISION-BY-ZERO");
        # disable breakpoint 1
        list traceLog3 = (
            ("func": ST_STEP, "line": line.tstProgram+5, "localVars": ("tid": gettid(), "s": "integer", "ret": "DONE"), "runState": DebugDetach),
        );
        list action3 = list(DebugRun, ("localVars": ("tid", "s", "ret"), "runState": DebugDetach) );
        b2.setEnabled(False);
        checkProgram("breakpoint b1(enable), b2(disable)", action3, traceLog3, "DIVISION-BY-ZERO");
        b2.setEnabled(True);
        b2.setPolicy(BreakpointPolicyReject);
        checkProgram("breakpoint b1(enable), b2(reject)", action3, traceLog3, "DIVISION-BY-ZERO");
        b2.clearThreadIds();
        b2.addThreadId(gettid()+1000);
        b2.setPolicy(BreakpointPolicyAccept);
        checkProgram("breakpoint b1(enable), b2(not accept)", action3, traceLog3, "DIVISION-BY-ZERO");
        b2.addThreadId(gettid());
        checkProgram("breakpoint b1(enable), b2(accept)", action2, traceLog2, "DIVISION-BY-ZERO");

        # breakpoint group
        b2.assignStatement(thisProgram.findStatementId('', line.tstFunction+6));
        b2.assignStatement(thisProgram.findStatementId('', line.tstFunction+9));
        checkProgram("breakpoint b1(enable), b2(accept,group3)", splice(action2, 1, 0, (DebugRun, DebugRun)), splice(traceLog2, 1, 0, (("func": ST_STEP, "line": line.tstFunction+6), ("func": ST_STEP, "line": line.tstFunction+9))), "DIVISION-BY-ZERO");
        b2.unassignStatement(thisProgram.findStatementId('', line.tstFunction+9));
        checkProgram("breakpoint b1(enable), b2(accept,group2)", splice(action2, 1, 0, list(DebugRun)), splice(traceLog2, 1, 0, (("func": ST_STEP, "line": line.tstFunction+6))), "DIVISION-BY-ZERO");
        b2.setEnabled(False);
        checkProgram("breakpoint b1(enable), b2(disable,group)", action3, traceLog3, "DIVISION-BY-ZERO");

        testAssertionValue('getBreakpointId', b1.getBreakpointId() > 0, True);
        testAssertionValue('getBreakpointId compare', b1.getBreakpointId()+1 == b2.getBreakpointId(), True);
        assertThrows('BREAKPOINT-ERROR', \Breakpoint::resolveBreakpointId(), list(0), '.resolveBreakpointId() no breakpoint');
        testAssertionValue('resolveBreakpointId', Breakpoint::resolveBreakpointId(b1.getBreakpointId()).getBreakpointId(), b1.getBreakpointId());
    }

    multiThreadBreakTest() {
        checkBreakProgram("break program", -1, True);
        checkBreakProgram("break thread", gettid(), True);
        checkBreakProgram("break unknown thread", gettid()+12, False);
    }

    multiThreadDebugTest() {
    }

    breakpointTest() {
        string name = "Breakpoint::";
        Breakpoint b = new Breakpoint();
        testAssertionValue(name+'getEnabled() default', b.getEnabled(), False);
        b.setEnabled(True);
        testAssertionValue(name+'setEnabled(True)', b.getEnabled(), True);
        testAssertionValue(name+'getPolicy() default', b.getPolicy(), BreakpointPolicyNone);
        b.setPolicy(BreakpointPolicyAccept);
        testAssertionValue(name+'setPolicy(BreakpointPolicyAccept)', b.getPolicy(), BreakpointPolicyAccept);
        b.setPolicy(BreakpointPolicyReject);
        testAssertionValue(name+'setPolicy(BreakpointPolicyReject)', b.getPolicy(), BreakpointPolicyReject);

        testAssertionValue(name+'getThreadIds()', b.getThreadIds(), ());
        b.setThreadIds(list(1,2,3,4,5,6,7,8,9,10,20,21,30));
        testAssertionValue(name+'setThreadIds((1,2,3,4,5,6,7,8,9,10,20,21,30))', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30));
        testAssertionValue(name+'isThreadId(0)', b.isThreadId(0), False);
        testAssertionValue(name+'isThreadId(1)', b.isThreadId(1), True);
        testAssertionValue(name+'isThreadId(5)', b.isThreadId(5), True);
        testAssertionValue(name+'isThreadId(10)', b.isThreadId(10), True);
        testAssertionValue(name+'isThreadId(11)', b.isThreadId(11), False);
        testAssertionValue(name+'isThreadId(10)', b.isThreadId(21), True);
        testAssertionValue(name+'isThreadId(10)', b.isThreadId(30), True);

        Breakpoint b2 = b;  # copy constructor test
        testAssertionValue(name+'setEnabled(True) copy', b2.getEnabled(), True);
        testAssertionValue(name+'setPolicy(BreakpointPolicyReject) copy', b2.getPolicy(), BreakpointPolicyReject);
        testAssertionValue(name+'setThreadIds((1,2,3,4,5,6,7,8,9,10,20,21,30)) copy', b2.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30));

        b.setEnabled(False);
        testAssertionValue(name+'setEnabled(False)', b.getEnabled(), False);
        b.setPolicy(BreakpointPolicyNone);
        testAssertionValue(name+'setPolicy(BreakpointPolicyNone)', b.getPolicy(), BreakpointPolicyNone);

        b.addThreadId(1);
        testAssertionValue(name+'addThreadId(1)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30));
        b.addThreadId(30);
        testAssertionValue(name+'addThreadId(30)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30));
        b.addThreadId(40);
        testAssertionValue(name+'addThreadId(40)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30,40));
        b.addThreadId(39);
        testAssertionValue(name+'addThreadId(39)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30,39,40));
        b.addThreadId(41);
        testAssertionValue(name+'addThreadId(41)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,30,39,40,41));
        testAssertionValue(name+'isThreadId(39)', b.isThreadId(39), True);
        testAssertionValue(name+'isThreadId(40)', b.isThreadId(40), True);
        testAssertionValue(name+'isThreadId(41)', b.isThreadId(41), True);
        b.removeThreadId(5);
        testAssertionValue(name+'removeThreadId(5)', b.getThreadIds(), list(1,2,3,4,6,7,8,9,10,20,21,30,39,40,41));
        b.removeThreadId(30);
        testAssertionValue(name+'removeThreadId(30)', b.getThreadIds(), list(1,2,3,4,6,7,8,9,10,20,21,39,40,41));
        b.removeThreadId(41);
        testAssertionValue(name+'removeThreadId(41)', b.getThreadIds(), list(1,2,3,4,6,7,8,9,10,20,21,39,40));
        b.removeThreadId(39);
        testAssertionValue(name+'removeThreadId(39)', b.getThreadIds(), list(1,2,3,4,6,7,8,9,10,20,21,40));
        b.addThreadId(5);
        testAssertionValue(name+'addThreadId(30)', b.getThreadIds(), list(1,2,3,4,5,6,7,8,9,10,20,21,40));

        b.setThreadIds((1));
        testAssertionValue(name+'setThreadIds((1))', b.getThreadIds(), list(1));

        b.clearThreadIds();
        testAssertionValue(name+'clearThreadIds()', b.getThreadIds(), ());

        b.assignProgram(thisProgram);
        b.setThreadIds((1,2));
        testAssertionValue(name+'setThreadIds((1))', b.getThreadIds(), list(1,2));

        testAssertionValue(name+'getStatementIds(())', b.getStatementIds(), list());

        int sid1 = thisProgram.findStatementId('', line.tstFunction2+1);
        int sid2 = thisProgram.findStatementId('', line.tstFunction+1);
        b.assignStatement(sid1);
        testAssertionValue(name+'getStatementIds(())', b.getStatementIds(), list(sid1));
        b.unassignStatement(sid1);
        testAssertionValue(name+'getStatementIds(())', b.getStatementIds(), list());
        b.assignStatement(sid1);
        b.assignStatement(sid2);
        testAssertionValue(name+'getStatementIds(())', b.getStatementIds(), list(sid1, sid2));

        b.unassignProgram();
        testAssertionValue(name+'getStatementIds(())', b.getStatementIds(), list());

        b.assignProgram(thisProgram);
        testAssertionValue(name+'setThreadIds((1))', b.getThreadIds(), list(1,2));

        b.setThreadIds(list());
        testAssertionValue(name+'setThreadIds(())', b.getThreadIds(), list());

        /*
        testAssertionValue(name+'setPolicy(BreakpointPolicyNone)', b.getPolicy(), BreakpointPolicyNone);
        assertThrows('BREAKPOINT-ERROR', \b.getThreadIds(), (), name+'.getThreadIds() no program');

        b.assignProgram(thisProgram); */
    }

    statementTest() {
        string name = "Program::";
        Program p();
        # stress statement test
        p.parsePending(
            "*string sub f1(int p, string s) {
                int a; int b;  #2
                a = 1; b = 2;  #3
                {
                a = a + b; doSleep(10); #5
                {
                    if (p > 0) {  #7
                    b = 1+  #8
                        1+
                        1+
                        1+
                        1;  #12
                    } #13
                }
                }
                return sprintf(\"%s,%d,%d,%d\", s, p, a, b);  #16
            }",
            "label-f1", NOTHING, "/usr/lib/my-source.q", 10, False);
        p.parsePending(
            "sub doSleep(int ms) {
                sleep(ms); #2
            }", "label-sleep", NOTHING, "/usr/lib/my-source.q", 100, False);
        p.parsePending("sub main() { f1(10, 'MAIN'); }", "main.q", NOTHING, NOTHING, NOTHING, False);
        p.parseCommit();

        ProgramControl pp = p.getProgram();

        hash h = pp.getSourceLabels();
        testAssertionValue(name+'getSourceLabels()', h, ('label-f1':('/usr/lib/my-source.q': 10), 'label-sleep': ('/usr/lib/my-source.q': 100)));

        h = pp.getSourceFileNames();
        testAssertionValue(name+'getSourceFileNames()', h, ('/usr/lib/my-source.q': ('label-f1': 10, 'label-sleep': 100), 'main.q': hash()));

        assertThrows('PROGRAM-STATEMENT-ERROR', \pp.findStatementId(), ("main.q", 10), name+'findStatementId("main.q", 10) line beyond');
        assertThrows('PROGRAM-STATEMENT-ERROR', \pp.findStatementId(), ("non-exist.q", 1), name+'findStatementId("no-exist.q", 1) non exist');

        int sid;
        pp = p.getProgram();
        sid = pp.findStatementId("main.q", 1);
        testAssertionValue(name+'findStatementId("main.q", 1)', sid > 0, True);

        hash info;
        info = pp.getStatementIdInfo(sid, True);
        testAssertionValue(name+'getStatementIdInfo("main.q", 1)', info, ('file': 'main.q', 'source': '', 'offset': 0, 'start_line': 1, 'end_line': 1, 'breakpoints': ()) );

        sid = pp.findStatementId("label-f1", 2);
        testAssertionValue(name+'findStatementId("label-f1", 2) label match', sid > 0, True);

        testAssertionValue(name+'findStatementId("/usr/lib/my-source.q", 12) full file', pp.findStatementId("/usr/lib/my-source.q", 12), sid);
        testAssertionValue(name+'findStatementId("/lib/my-source.q", 12) suffix', pp.findStatementId("/lib/my-source.q", 12), sid);
        testAssertionValue(name+'findStatementId("lib/my-source.q", 12) suffix', pp.findStatementId("lib/my-source.q", 12), sid);
        testAssertionValue(name+'findStatementId("my-source.q", 12) suffix', pp.findStatementId("my-source.q", 12), sid);
        assertThrows('PROGRAM-STATEMENT-ERROR', \pp.findStatementId(), ("ib/my-source.q", 102), name+'findStatementId("ib/my-source.q", 12) no suffix match');

        info = pp.getStatementIdInfo(sid);
        testAssertionValue(name+'getStatementIdInfo("label-f1", 1)', info, ('file': 'label-f1', 'source': '/usr/lib/my-source.q', 'offset': 10, 'start_line': 2, 'end_line': 2) );


        sid = pp.findStatementId("label-f1", 8);
        testAssertionValue(name+'findStatementId("label-f1", 8)', sid > 0, True);
        testAssertionValue(name+'findStatementId("label-f1", 11) multiline', pp.findStatementId("label-f1", 11), sid);
        info = pp.getStatementIdInfo(sid);
        testAssertionValue(name+'getStatementIdInfo("label-f1", 11)', info, ('file': 'label-f1', 'source': '/usr/lib/my-source.q', 'offset': 10, 'start_line': 8, 'end_line': 12) );

        sid = pp.findStatementId("label-sleep", 2);
        testAssertionValue(name+'findStatementId("label-sleep", 2)', sid > 0, True);
        testAssertionValue(name+'findStatementId("/usr/lib/my-source.q", 102) full file', pp.findStatementId("/usr/lib/my-source.q", 102), sid);
        info = pp.getStatementIdInfo(sid);
        testAssertionValue(name+'getStatementIdInfo("label-sleep", 2)', info, ('file': 'label-sleep', 'source': '/usr/lib/my-source.q', 'offset': 100, 'start_line': 2, 'end_line': 2) );
    }

    tlsStatementTest() {
        string name = "Program::";
        int sid;
        hash info;
        # test if top-level-statement has been properly parsed and indexed
        sid = thisProgram.findStatementId("", line.tstTlsFunction+1);
        info = thisProgram.getStatementIdInfo(sid);
        testAssertionValue(name+'findStatementId("", tlsFunction:1)', info.('start_line', 'end_line'), ('start_line': line.tstTlsFunction+1, 'end_line': line.tstTlsFunction+1));
        sid = thisProgram.findStatementId("", line.tstTlsFunction+2);
        info = thisProgram.getStatementIdInfo(sid);
        testAssertionValue(name+'findStatementId("", tlsFunction:2)', info.('start_line', 'end_line'), ('start_line': line.tstTlsFunction+2, 'end_line': line.tstTlsFunction+4));

        # is closure statement indexed ?
        sid = thisProgram.findStatementId("", line.tstTlsClosure+1);
        info = thisProgram.getStatementIdInfo(sid);
        testAssertionValue(name+'findStatementId("", tlsClosure:2)', info.('start_line', 'end_line'), ('start_line': line.tstTlsClosure+1, 'end_line': line.tstTlsClosure+1));
        sid = thisProgram.findStatementId("", line.tstTlsClosure+2);
        info = thisProgram.getStatementIdInfo(sid);
        testAssertionValue(name+'findStatementId("", tlsClosure:3)', info.('start_line', 'end_line'), ('start_line': line.tstTlsClosure+2, 'end_line': line.tstTlsClosure+2));

        assertThrows('PROGRAM-STATEMENT-ERROR', \thisProgram.findStatementId(), list("", line.tstTlsClosure-1), name+'.findStatementId() no tls statement');
    }

    programTest() {
        string name = "Program::";
        list l = ProgramControl::getAllPrograms();
        testAssertionValue(name+'getAllPrograms', l.size(), 3);
        testAssertionValue(name+'getProgramId', thisProgram.getProgramId() > 0, True);
        # NOTE: getAllPrograms() does not guarantee the return value order
        # ensure that the current program is in the list
        {
            int id;
            foreach ProgramControl p in (l) {
                if (p.getProgramId() == thisProgram.getProgramId()) {
                    id = p.getProgramId();
                    break;
                }
            }
            assertEq(thisProgram.getProgramId(), id);
        }
        assertThrows('PROGRAM-ERROR', \ProgramControl::resolveProgramId(), list(0), name+'.resolveProgramId() no program');
        testAssertionValue(name+'resolveProgramId', ProgramControl::resolveProgramId(thisProgram.getProgramId()).getProgramId(), thisProgram.getProgramId());
    }
}

