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

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

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

%exec-class ThreadObjectTest

# this class implements a background thread that does not extend the scope of the object
# and is shut down cleanly when the object goes out of scope - this allows objects
# to have a natural scope and also a service thread that's managed with the natural
# lifecycle of the object
class TransparentObjectThreadTest {
    private:internal {
        Counter end(1);
        bool stop;
    }

    constructor() {
        # we have to let the test method dereference the object before finishing the method
        # otherwise there is a race condition where the destructor is run in the background
        # thread and will wait on the end counter before it can be decremented
        Counter start(1);
        background test(self, start);
        start.waitForZero();
    }

    destructor() {
        stop = True;
        end.waitForZero();
    }

    static test(TransparentObjectThreadTest t, Counter start) {
        # let the object go out of scope naturally
        TransparentObjectThreadTest self := t;
        remove t;
        # signal the constructor to continue
        start.dec();
        # signal the object's end counter when done
        on_exit self.end.dec();

        # loop until we are signaled to stop
        while (!self.stop) {
            usleep(50ms);
        }
    }
}

class ThreadObjectTest inherits QUnit::Test {
    private {
        int threads;
        int iters;
    }
    constructor() : QUnit::Test("Thread object test", "1.0") {
        addTestCase("Test Counter", \testCounter());
        addTestCase("Test Queue", \testQueue());
        addTestCase("Test Thread", \testThread());
        addTestCase("Test TransparentObjectThreadTest", \transparentObjectThreadTest());
        set_return_value(main());
    }

    globalSetUp() {
        threads = 5;
        iters = 2000;
    }

    testCounter() {
        new CounterTest(threads, iters, self);
    }

    testQueue() {
        new QueueTest(threads, iters, self);
    }

    testThread() {
        new ThreadTest(threads, iters, self);
    }

    transparentObjectThreadTest() {
        TransparentObjectThreadTest t();
        assertTrue(True);
    }
}

class CounterTest {
    private {
        int threads;
        int iters;
        any obj;
        Counter c();
    }
    constructor(int nthreads, int niters, ThreadObjectTest t) {
        threads = nthreads;
        iters = niters;
        obj.key.500.hello = 0;

        do_threads();
        c.waitForZero();
        t.assertTrue(!obj.key.500.hello, "counter");
    }

    private do_threads() {
        while (threads--) {
            c.inc();
            background add();
            c.inc();
            background subtract();
        }
    }

    private add() {
        for (int i = 0; i < iters; i++)
            obj.key.500.hello++;
        c.dec();
    }

    private subtract() {
        for (int i = 0; i < iters; i++)
            obj.key.500.hello--;
        c.dec();
    }
}

class QueueTest {
    private {
        Queue q();
        Counter x();
    }
    constructor(int nthreads, int niters, ThreadObjectTest t) {
        for (int i; i < nthreads; i++) {
            x.inc();
            background qt(niters);
        }
        int c = nthreads * niters;
        while (c--)
            if (rand() % 2)
                q.pop();
            else
                q.get();

        x.waitForZero();
        t.assertTrue(!q.size(), "queue");
    }

    private qt(int niters) {
        for (int i; i < niters; i++)
            q.push(sprintf("tid-%d-%d", gettid(), i));
        x.dec();
    }
}

my int thread_tid;

class ThreadTest inherits Mutex {
    private {
        hash data;
        int threads;
        int iters;

        RWLock drw();
        Gate g();
        Counter c();

        hash gdata;
        hash rwdata;
    }
    constructor(int nthreads, int niters, ThreadObjectTest t) {
        threads = nthreads;
        iters = niters;
        while (threads) {
            c.inc();
            background worker();
            threads--;
        }
        #sleep(1);
        #printf("num_threads:%y\nthread_list:%y\ngettid:%y\n", num_threads(), thread_list(), gettid());
        #printf("get_all_thread_data:%N\nget_all_thread_call_stacks:%N\n", get_all_thread_data(), get_all_thread_call_stacks());
        c.waitForZero();
        t.assertTrue(True, "thread object tests");
    }
    *hash getData(list l) {
        lock();
        on_exit unlock();
        return data{l};
    }
    setData(hash h) {
        lock();
        on_exit unlock();
        data += h;
    }
    *hash getRWData(list l) {
        drw.readLock();
        on_exit
            drw.readUnlock();
        return rwdata{l};
    }
    setRWData(hash h) {
        drw.writeLock();
        on_exit
            drw.writeUnlock();
        rwdata += h;
    }
    *hash getGateData(list l) {
        g.enter();
        on_exit g.exit();
        return gdata{l};
    }
    setGateData(hash h) {
        g.enter();
        on_exit g.exit();
        gdata+= h;
    }
    worker() {
        thread_tid = gettid();
        #printf("worker start:%y, var:%y\n", gettid(), thread_tid);
        for (int i = 0; i < iters; i++) {
            #if (!(i % 1000))
            #    printf("TID %3d: %d/%d\n", gettid(), i, iters);
            int c = rand() % 6;
            string key1 = sprintf("key%d", rand() % 10);
            string key2 = sprintf("key%d", (rand() % 10) + 10);
            string key3 = sprintf("key%d", rand() % 20);
            if (!c) {
                hash h = (
                    key1: rand() % 10,
                    key2: rand() % 10,
                    );
                setData(h);
                continue;
            }
            if (c == 1) {
                getData((key1, key2, key3));
                continue;
            }
            if (c == 2) {
                hash h = (
                    key1: rand() % 10,
                    key2: rand() % 10,
                    );
                setRWData(h);
                continue;
            }
            if (c == 3) {
                getRWData((key1, key2, key3));
                continue;
            }
            if (c == 4) {
                hash h = (
                    key1: rand() % 10,
                    key2: rand() % 10,
                    );
                setGateData(h);
                continue;
            }
            if (c == 5) {
                getGateData((key1, key2, key3));
                continue;
            }
        }
        /*
        printf("worker sleep:%y\n", gettid());
        sleep(3);
        */
        c.dec();
        /*
        printf("worker end:%y, var:%y\n", gettid(), thread_tid);
        printf("get_all_thread_data():%y\n", get_all_thread_data());
        */
    }
}
