/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.tests.org.apache.harmony.kernel.dalvik;

import java.lang.reflect.Field;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import junit.framework.Assert;
import junit.framework.TestCase;
import sun.misc.Unsafe;

/**
 * Tests for the <code>park()</code> functionality of {@link Unsafe}.
 */
public class ThreadsTest extends TestCase {
    private static Unsafe UNSAFE = null;
    static {
        /*
         * Set up {@link #UNSAFE}. This subverts the access check to
         * get the unique Unsafe instance. We can do this because
         * there's no security manager installed when running the
         * test.
         */
        try {
            Field field = Unsafe.class.getDeclaredField("THE_ONE");
            field.setAccessible(true);

            UNSAFE = (Unsafe) field.get(null);
        } catch (NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        }
    }

    /** Test the case where the park times out. */
    public void test_parkFor_1() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(2);
        Parker parker = new Parker(barrier, false, 500);
        Thread parkerThread = new Thread(parker);
        Thread waiterThread =
            new Thread(new WaitAndUnpark(barrier, 1000, parkerThread));

        parkerThread.start();
        waiterThread.start();
        parker.assertDurationIsInRange(500);
        waiterThread.join();
        parkerThread.join();
    }

    /** Test the case where the unpark happens before the timeout. */
    public void test_parkFor_2() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(2);
        Parker parker = new Parker(barrier, false, 1000);
        Thread parkerThread = new Thread(parker);
        Thread waiterThread =
            new Thread(new WaitAndUnpark(barrier, 300, parkerThread));

        parkerThread.start();
        waiterThread.start();
        parker.assertDurationIsInRange(300);
        waiterThread.join();
        parkerThread.join();
    }

    /** Test the case where the thread is preemptively unparked. */
    public void test_parkFor_3() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(1);
        Parker parker = new Parker(barrier, false, 1000);
        parker.disableRetry();
        Thread parkerThread = new Thread(parker);

        UNSAFE.unpark(parkerThread);
        parkerThread.start();
        parker.assertDurationIsInRange(0);
        parkerThread.join();
    }

    /** Test the case where the park times out. */
    public void test_parkUntil_1() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(2);
        Parker parker = new Parker(barrier, true, 500);
        Thread parkerThread = new Thread(parker);
        Thread waiterThread =
            new Thread(new WaitAndUnpark(barrier, 1000, parkerThread));

        parkerThread.start();
        waiterThread.start();
        parker.assertDurationIsInRange(500);
        waiterThread.join();
        parkerThread.join();
    }

    /** Test the case where the unpark happens before the timeout. */
    public void test_parkUntil_2() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(2);
        Parker parker = new Parker(barrier, true, 1000);
        Thread parkerThread = new Thread(parker);
        Thread waiterThread =
            new Thread(new WaitAndUnpark(barrier, 300, parkerThread));

        parkerThread.start();
        waiterThread.start();
        parker.assertDurationIsInRange(300);
        waiterThread.join();
        parkerThread.join();
    }

    /** Test the case where the thread is preemptively unparked. */
    public void test_parkUntil_3() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(1);
        Parker parker = new Parker(barrier, true, 1000);
        parker.disableRetry();
        Thread parkerThread = new Thread(parker);

        UNSAFE.unpark(parkerThread);
        parkerThread.start();
        parker.assertDurationIsInRange(0);
        parkerThread.join();
    }

    // TODO: Add more tests.

    /**
     * Helper <code>Runnable</code> for tests, which parks for or until
     * the indicated value, noting the duration of time actually parked.
     */
    private static class Parker implements Runnable {

        private static final long NANOS_PER_MILLI = 1_000_000L;

        private final CyclicBarrier barrier;

        /** whether {@link #amount} is milliseconds to wait in an
         * absolute fashion (<code>true</code>) or nanoseconds to wait
         * in a relative fashion (<code>false</code>) */
        private final boolean absolute;

        private boolean retryDisabled;

        /** amount to wait (see above) */
        private final long amount;

        /** whether the run has completed */
        private boolean completed;

        /** recorded start time */
        private long startNanos;

        /** recorded end time */
        private long endNanos;

        /**
         * Construct an instance.
         *
         * @param absolute whether to use an absolute time or not; in
         * either case, this constructor takes a duration to park for
         * @param parkMillis the number of milliseconds to be parked
         */
        public Parker(CyclicBarrier barrier, boolean absolute, long parkMillis) {
            this.barrier = barrier;
            this.absolute = absolute;
            // this.retryDisabled = false;
            if (parkMillis < 10) {
              // Doesn't work well with our retry logic, and likely to be flakey.
              throw new AssertionError("Unexpectedly short park timeout.");
            }
            // Multiply by 1000000 because parkFor() takes nanoseconds.
            this.amount = absolute ? parkMillis : parkMillis * NANOS_PER_MILLI;
        }

        public void disableRetry() {
          retryDisabled = true;
        }

        public void run() {
            try {
                barrier.await(60, TimeUnit.SECONDS);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
            boolean absolute = this.absolute;
            long amount = this.amount;
            long startNs = System.nanoTime();
            long start = System.currentTimeMillis();

            for (int i = 0; i < 2; ++i) {
                if (absolute) {
                    UNSAFE.park(true, start + amount);
                } else {
                    UNSAFE.park(false, amount);
                }
                // park() may wake up spuriously. For our implementation, this
                // should be unlikely, except there are cases in which the
                // initial attempt returns immediately. Try a second time, but
                // only in that case.
                if (retryDisabled || System.currentTimeMillis() - start > 1L) {
                  break;
                }
            }

            long endNs = System.nanoTime();

            synchronized (this) {
                startNanos = startNs;
                endNanos = endNs;
                completed = true;
                notifyAll();
            }
        }

        /**
         * Wait for the test to complete and return the duration.
         *
         * @param maxWaitMillis the maximum amount of time to
         * wait for the test to complete
         * @return the duration in milliseconds
         */
        public long getDurationMillis(long maxWaitMillis) {
            synchronized (this) {
                if (! completed) {
                    try {
                        wait(maxWaitMillis);
                    } catch (InterruptedException ignored) {
                    }
                    if (! completed) {
                        Assert.fail("parker hung for more than " + maxWaitMillis + " ms");
                    }
                }

                return (endNanos - startNanos) / NANOS_PER_MILLI;
            }
        }

        /**
         * Asserts that the actual duration is within 150-200 msecs of the
         * given expected time.
         * Observe that small errors in either direction are normal here; for a
         * number of tests the value will be too small if the Parker thread
         * starts late, e.g. because it doesn't get a time slice soon enough.
         *
         * @param expectedMillis the expected duration, in milliseconds
         */
        public void assertDurationIsInRange(long expectedMillis) {
            long minimum = Math.max(expectedMillis - 150L, 0L);
            long maximum = expectedMillis + 200L;
            long waitMillis = Math.max(expectedMillis * 10L, 1000L);
            long duration = getDurationMillis(waitMillis);

            if (duration < minimum) {
                Assert.fail("expected duration: " + expectedMillis +
                            " minimum duration: " + minimum +
                            " actual duration too short: " + duration);
            } else if (duration > maximum) {
                Assert.fail("expected duration: " + expectedMillis +
                            " maximum duration: " + maximum +
                            " actual duration too long: " + duration);
            }
        }
    }

    /**
     * Helper <code>Runnable</code> for tests, which waits for the
     * specified amount of time and then unparks an indicated thread.
     */
    private static class WaitAndUnpark implements Runnable {
        private final CyclicBarrier barrier;
        private final long waitMillis;
        private final Thread thread;

        public WaitAndUnpark(CyclicBarrier barrier, long waitMillis, Thread thread) {
            this.barrier = barrier;
            this.waitMillis = waitMillis;
            this.thread = thread;
        }

        public void run() {
            try {
                barrier.await(60, TimeUnit.SECONDS);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
            try {
                Thread.sleep(waitMillis);
            } catch (InterruptedException ex) {
                throw new RuntimeException("shouldn't happen", ex);
            }

            UNSAFE.unpark(thread);
        }
    }
}
