/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Test: y2ktmo
 *
 * Description:
 *   This test tests the interval time facilities in NSPR for Y2K
 *   compliance.  All the functions that take a timeout argument
 *   are tested: PR_Sleep, socket I/O (PR_Accept is taken as a
 *   representative), PR_Poll, PR_WaitCondVar, PR_Wait, and
 *   PR_CWait.  A thread of each thread scope (local, global, and
 *   global bound) is created to call each of these functions.
 *   The test should be started at the specified number of seconds
 *   (called the lead time) before a Y2K rollover test date.  The
 *   timeout values for these threads will span over the rollover
 *   date by at least the specified number of seconds.  For
 *   example, if the lead time is 5 seconds, the test should
 *   be started at time (D - 5), where D is a rollover date, and
 *   the threads will time out at or after time (D + 5).  The
 *   timeout values for the threads are spaced one second apart.
 *
 *   When a thread times out, it calls PR_IntervalNow() to verify
 *   that it did wait for the specified time.  In addition, it
 *   calls a platform-native function to verify the actual elapsed
 *   time again, to rule out the possibility that PR_IntervalNow()
 *   is broken.  We allow the actual elapsed time to deviate from
 *   the specified timeout by a certain tolerance (in milliseconds).
 */

#include "nspr.h"
#include "plgetopt.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(XP_UNIX)
#  include <sys/time.h> /* for gettimeofday */
#endif
#if defined(WIN32)
#  if defined(WINCE)
#    include <windows.h>
#  else
#    include <sys/types.h>
#    include <sys/timeb.h> /* for _ftime */
#  endif
#endif

#define DEFAULT_LEAD_TIME_SECS 5
#define DEFAULT_TOLERANCE_MSECS 500

static PRBool debug_mode = PR_FALSE;
static PRInt32 lead_time_secs = DEFAULT_LEAD_TIME_SECS;
static PRInt32 tolerance_msecs = DEFAULT_TOLERANCE_MSECS;
static PRIntervalTime start_time;
static PRIntervalTime tolerance;

#if defined(XP_UNIX)
static struct timeval start_time_tv;
#endif
#if defined(WIN32)
#  if defined(WINCE)
static DWORD start_time_tick;
#  else
static struct _timeb start_time_tb;
#  endif
#endif

static void SleepThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif

  if (PR_Sleep(timeout) == PR_FAILURE) {
    fprintf(stderr, "PR_Sleep failed\n");
    exit(1);
  }
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  if (debug_mode) {
    fprintf(stderr, "Sleep thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

static void AcceptThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif
  PRFileDesc* sock;
  PRNetAddr addr;
  PRFileDesc* accepted;

  sock = PR_NewTCPSocket();
  if (sock == NULL) {
    fprintf(stderr, "PR_NewTCPSocket failed\n");
    exit(1);
  }
  memset(&addr, 0, sizeof(addr));
  addr.inet.family = PR_AF_INET;
  addr.inet.port = 0;
  addr.inet.ip = PR_htonl(PR_INADDR_ANY);
  if (PR_Bind(sock, &addr) == PR_FAILURE) {
    fprintf(stderr, "PR_Bind failed\n");
    exit(1);
  }
  if (PR_Listen(sock, 5) == PR_FAILURE) {
    fprintf(stderr, "PR_Listen failed\n");
    exit(1);
  }
  accepted = PR_Accept(sock, NULL, timeout);
  if (accepted != NULL || PR_GetError() != PR_IO_TIMEOUT_ERROR) {
    fprintf(stderr, "PR_Accept did not time out\n");
    exit(1);
  }
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  if (PR_Close(sock) == PR_FAILURE) {
    fprintf(stderr, "PR_Close failed\n");
    exit(1);
  }
  if (debug_mode) {
    fprintf(stderr, "Accept thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

static void PollThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif
  PRFileDesc* sock;
  PRNetAddr addr;
  PRPollDesc pd;
  PRIntn rv;

  sock = PR_NewTCPSocket();
  if (sock == NULL) {
    fprintf(stderr, "PR_NewTCPSocket failed\n");
    exit(1);
  }
  memset(&addr, 0, sizeof(addr));
  addr.inet.family = PR_AF_INET;
  addr.inet.port = 0;
  addr.inet.ip = PR_htonl(PR_INADDR_ANY);
  if (PR_Bind(sock, &addr) == PR_FAILURE) {
    fprintf(stderr, "PR_Bind failed\n");
    exit(1);
  }
  if (PR_Listen(sock, 5) == PR_FAILURE) {
    fprintf(stderr, "PR_Listen failed\n");
    exit(1);
  }
  pd.fd = sock;
  pd.in_flags = PR_POLL_READ;
  rv = PR_Poll(&pd, 1, timeout);
  if (rv != 0) {
    fprintf(stderr, "PR_Poll did not time out\n");
    exit(1);
  }
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  if (PR_Close(sock) == PR_FAILURE) {
    fprintf(stderr, "PR_Close failed\n");
    exit(1);
  }
  if (debug_mode) {
    fprintf(stderr, "Poll thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

static void WaitCondVarThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif
  PRLock* ml;
  PRCondVar* cv;

  ml = PR_NewLock();
  if (ml == NULL) {
    fprintf(stderr, "PR_NewLock failed\n");
    exit(1);
  }
  cv = PR_NewCondVar(ml);
  if (cv == NULL) {
    fprintf(stderr, "PR_NewCondVar failed\n");
    exit(1);
  }
  PR_Lock(ml);
  PR_WaitCondVar(cv, timeout);
  PR_Unlock(ml);
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  PR_DestroyCondVar(cv);
  PR_DestroyLock(ml);
  if (debug_mode) {
    fprintf(stderr, "wait cond var thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

static void WaitMonitorThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif
  PRMonitor* mon;

  mon = PR_NewMonitor();
  if (mon == NULL) {
    fprintf(stderr, "PR_NewMonitor failed\n");
    exit(1);
  }
  PR_EnterMonitor(mon);
  PR_Wait(mon, timeout);
  PR_ExitMonitor(mon);
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  PR_DestroyMonitor(mon);
  if (debug_mode) {
    fprintf(stderr, "wait monitor thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

static void WaitCMonitorThread(void* arg) {
  PRIntervalTime timeout = (PRIntervalTime)arg;
  PRIntervalTime elapsed;
#if defined(XP_UNIX) || defined(WIN32)
  PRInt32 timeout_msecs = PR_IntervalToMilliseconds(timeout);
  PRInt32 elapsed_msecs;
#endif
#if defined(XP_UNIX)
  struct timeval end_time_tv;
#endif
#if defined(WIN32) && !defined(WINCE)
  struct _timeb end_time_tb;
#endif
  int dummy;

  PR_CEnterMonitor(&dummy);
  PR_CWait(&dummy, timeout);
  PR_CExitMonitor(&dummy);
  elapsed = (PRIntervalTime)(PR_IntervalNow() - start_time);
  if (elapsed + tolerance < timeout || elapsed > timeout + tolerance) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#if defined(XP_UNIX)
  gettimeofday(&end_time_tv, NULL);
  elapsed_msecs = 1000 * (end_time_tv.tv_sec - start_time_tv.tv_sec) +
                  (end_time_tv.tv_usec - start_time_tv.tv_usec) / 1000;
#endif
#if defined(WIN32)
#  if defined(WINCE)
  elapsed_msecs = GetTickCount() - start_time_tick;
#  else
  _ftime(&end_time_tb);
  elapsed_msecs = 1000 * (end_time_tb.time - start_time_tb.time) +
                  (end_time_tb.millitm - start_time_tb.millitm);
#  endif
#endif
#if defined(XP_UNIX) || defined(WIN32)
  if (elapsed_msecs + tolerance_msecs < timeout_msecs ||
      elapsed_msecs > timeout_msecs + tolerance_msecs) {
    fprintf(stderr, "timeout wrong\n");
    exit(1);
  }
#endif
  if (debug_mode) {
    fprintf(stderr, "wait cached monitor thread (scope %d) done\n",
            PR_GetThreadScope(PR_GetCurrentThread()));
  }
}

typedef void (*NSPRThreadFunc)(void*);

static NSPRThreadFunc threadFuncs[] = {SleepThread,       AcceptThread,
                                       PollThread,        WaitCondVarThread,
                                       WaitMonitorThread, WaitCMonitorThread};

static PRThreadScope threadScopes[] = {PR_LOCAL_THREAD, PR_GLOBAL_THREAD,
                                       PR_GLOBAL_BOUND_THREAD};

static void Help(void) {
  fprintf(stderr, "y2ktmo test program usage:\n");
  fprintf(stderr, "\t-d           debug mode         (FALSE)\n");
  fprintf(stderr, "\t-l <secs>    lead time          (%d)\n",
          DEFAULT_LEAD_TIME_SECS);
  fprintf(stderr, "\t-t <msecs>   tolerance          (%d)\n",
          DEFAULT_TOLERANCE_MSECS);
  fprintf(stderr, "\t-h           this message\n");
} /* Help */

int main(int argc, char** argv) {
  PRThread** threads;
  int num_thread_funcs = sizeof(threadFuncs) / sizeof(NSPRThreadFunc);
  int num_thread_scopes = sizeof(threadScopes) / sizeof(PRThreadScope);
  int i, j;
  int idx;
  PRInt32 secs;
  PLOptStatus os;
  PLOptState* opt = PL_CreateOptState(argc, argv, "dl:t:h");

  while (PL_OPT_EOL != (os = PL_GetNextOpt(opt))) {
    if (PL_OPT_BAD == os) {
      continue;
    }
    switch (opt->option) {
      case 'd': /* debug mode */
        debug_mode = PR_TRUE;
        break;
      case 'l': /* lead time */
        lead_time_secs = atoi(opt->value);
        break;
      case 't': /* tolerance */
        tolerance_msecs = atoi(opt->value);
        break;
      case 'h':
      default:
        Help();
        return 2;
    }
  }
  PL_DestroyOptState(opt);

  if (debug_mode) {
    fprintf(stderr, "lead time: %d secs\n", lead_time_secs);
    fprintf(stderr, "tolerance: %d msecs\n", tolerance_msecs);
  }

  start_time = PR_IntervalNow();
#if defined(XP_UNIX)
  gettimeofday(&start_time_tv, NULL);
#endif
#if defined(WIN32)
#  ifdef WINCE
  start_time_tick = GetTickCount();
#  else
  _ftime(&start_time_tb);
#  endif
#endif
  tolerance = PR_MillisecondsToInterval(tolerance_msecs);

  threads = PR_Malloc(num_thread_scopes * num_thread_funcs * sizeof(PRThread*));
  if (threads == NULL) {
    fprintf(stderr, "PR_Malloc failed\n");
    exit(1);
  }

  /* start to time out 5 seconds after a rollover date */
  secs = lead_time_secs + 5;
  idx = 0;
  for (i = 0; i < num_thread_scopes; i++) {
    for (j = 0; j < num_thread_funcs; j++) {
      threads[idx] = PR_CreateThread(
          PR_USER_THREAD, threadFuncs[j], (void*)PR_SecondsToInterval(secs),
          PR_PRIORITY_NORMAL, threadScopes[i], PR_JOINABLE_THREAD, 0);
      if (threads[idx] == NULL) {
        fprintf(stderr, "PR_CreateThread failed\n");
        exit(1);
      }
      secs++;
      idx++;
    }
  }
  for (idx = 0; idx < num_thread_scopes * num_thread_funcs; idx++) {
    if (PR_JoinThread(threads[idx]) == PR_FAILURE) {
      fprintf(stderr, "PR_JoinThread failed\n");
      exit(1);
    }
  }
  PR_Free(threads);
  printf("PASS\n");
  return 0;
}
