{%skiptarget=$nothread }
{$ifdef fpc}
{$mode objfpc}
{$h+}
{$endif}

uses
{$ifdef unix}
  cthreads,
{$endif}
  SysUtils, Classes;

var
  lock: TMultiReadExclusiveWriteSynchronizer;
  event1, event2: prtlevent;
  gcount: longint;
  gotdeadlockexception,
  waiting: boolean;

type
  terrorcheck = class(tthread)
    procedure execute; override;
  end;

  tcounter = class(tthread)
   private
    flock: TMultiReadExclusiveWriteSynchronizer;
    flocalcount: longint;
   public
    constructor create;
    property localcount: longint read flocalcount;
  end;

  treadcounter = class(tcounter)
    procedure execute; override;
  end;
  
  twritecounter = class(tcounter)
    procedure execute; override;
  end;
  
  treadwritecounter = class(tcounter)
   private
    ftrywriteupgrade: boolean;
   public
    constructor create(trywriteupgrade: boolean);
    procedure execute; override;
  end;

  tdeadlock1 = class(tthread)
    procedure execute; override;
  end;

  tdeadlock2 = class(tthread)
    procedure execute; override;
  end;

  tdoublereadonewrite1 = class(tthread)
    procedure execute; override;
  end;

  tdoublereadonewrite2 = class(tthread)
    procedure execute; override;
  end;

  twrongthreadendacquire = class(tthread)
    ftestwrongreadrelease: boolean;
    constructor create(testwrongreadrelease: boolean);
    procedure execute; override;
  end;

  twrongthreadendrelease = class(tthread)
    ftestwrongreadrelease: boolean;
    constructor create(testwrongreadrelease: boolean);
    procedure execute; override;
  end;

  tdoublewrite = class(tthread)
    fsecondwritethread: boolean;
    constructor create(secondwritethread: boolean);
    procedure execute; override;
  end;


constructor tcounter.create;
  begin
    { create suspended }
    inherited create(true);
    freeonterminate:=false;
    flock:=lock;
    flocalcount:=0;
  end;
  
procedure treadcounter.execute;
  var
    i: longint;
    l: longint;
    r: longint;
  begin
    for i:=1 to 100000 do
      begin
        lock.beginread;
        inc(flocalcount);
        l:=gcount;
        { guarantee at least one sleep }
        if i=50000 then
          sleep(20+random(30))
        else if (random(10000)=0) then
          sleep(20);
        { this must cause data races/loss at some point }
        gcount:=l+1;
        lock.endread;
        r:=random(30000);
        if (r=0) then
          sleep(30);
      end;
  end;


procedure twritecounter.execute;
  var
    i: longint;
    l: longint;
    r: longint;
  begin
    for i:=1 to 500 do
      begin
        lock.beginwrite;
        inc(flocalcount);
        l:=gcount;
        { guarantee at least one sleep }
        if i=250 then
          sleep(20+random(30))
        else if (random(100)=0) then
          sleep(20);
        { we must be exclusive }
        if gcount<>l then
          begin
            writeln('error 1');
            halt(1);
          end;
        gcount:=l+1;
        lock.endwrite;
        r:=random(30);
        if (r>28) then
          sleep(r);
      end;
  end;


constructor treadwritecounter.create(trywriteupgrade: boolean);
  begin
    ftrywriteupgrade:=trywriteupgrade;
    inherited create;
  end;


procedure treadwritecounter.execute;
  var
    i: longint;
    l: longint;
    r: longint;
  begin
    for i:=1 to 100000 do
      begin
        lock.beginread;
        if ftrywriteupgrade and
           ((i=50000) or
            (random(10000)=0)) then
          begin
            inc(flocalcount);
            lock.beginwrite;
            l:=gcount;
            { guarantee at least one sleep }
            if i=50000 then
              sleep(20+random(30))
            else if (random(5)=0) then
              sleep(20);
            lock.beginwrite;
            gcount:=l+1;
            lock.endwrite;
            lock.endwrite;
          end;
        lock.endread;
        r:=random(30000);
        if (r=0) then
          sleep(30);
      end;
  end;


procedure tdeadlock1.execute;
  var
    localgotdeadlockexception: boolean;
  begin
    localgotdeadlockexception:=false;
    lock.beginread;
    RTLEventSetEvent(event2);
    RTLEventWaitFor(event1);
    try
      lock.beginwrite;
    except
      localgotdeadlockexception:=true;
      gotdeadlockexception:=true;
    end;
    if not localgotdeadlockexception then
      lock.endwrite;
    lock.endread;
  end;


procedure tdeadlock2.execute;
  var
    localgotdeadlockexception: boolean;
  begin
    localgotdeadlockexception:=false;
    lock.beginread;
    RTLEventSetEvent(event1);
    RTLEventWaitFor(event2);
    try
      lock.beginwrite;
    except
      localgotdeadlockexception:=true;
      gotdeadlockexception:=true;
    end;
    if not localgotdeadlockexception then
      lock.endwrite;
    lock.endread;
  end;


procedure tdoublereadonewrite1.execute;
  begin
    // 1)
    lock.beginread;
    // 2)
    RTLEventSetEvent(event2);
    // 5)
    RTLEventWaitFor(event1);
    { ensure tdoublereadonewrite2 has time to get stuck in beginwrite }
    sleep(500);
    // 6)
    lock.beginread;
    // 7)
    lock.endread;
    // 8)
    lock.endread;
  end;


procedure tdoublereadonewrite2.execute;
  begin
    // 3)
    RTLEventWaitFor(event2);
    // 4)
    RTLEventSetEvent(event1);
    // 4a -- block until after 8)
    lock.beginwrite;
    // 9)
    lock.endwrite;
  end;


constructor twrongthreadendacquire.create(testwrongreadrelease: boolean);
  begin
    ftestwrongreadrelease:=testwrongreadrelease;
    inherited create(false);
  end;


procedure twrongthreadendacquire.execute;
  begin
    if ftestwrongreadrelease then
      lock.beginread
    else
      lock.beginwrite;
    RTLEventSetEvent(event1);
    RTLEventWaitFor(event2);
    try
      if ftestwrongreadrelease then
        lock.endread
      else
        lock.endwrite;
    except
      halt(30);
    end;
  end;


constructor twrongthreadendrelease.create(testwrongreadrelease: boolean);
  begin
    ftestwrongreadrelease:=testwrongreadrelease;
    inherited create(false);
  end;


procedure twrongthreadendrelease.execute;
  var
    caught: boolean;
  begin
    RTLEventWaitFor(event1);
    caught:=false;
    try
      if ftestwrongreadrelease then
        lock.endread
      else
        lock.endwrite;
    except
      caught:=true;
    end;
    RTLEventSetEvent(event2);
    if not caught then
      halt(40);
  end;


constructor tdoublewrite.create(secondwritethread: boolean);
  begin
    fsecondwritethread:=secondwritethread;
    inherited create(false);
  end;


procedure tdoublewrite.execute;
  begin
    if fsecondwritethread then
      begin
        RTLEventWaitFor(event1);
        if lock.beginwrite then
          halt(50);
      end
    else
      begin
        if not lock.beginwrite then
          halt(51);
        RTLEventSetEvent(event1);
        // give the other thread the time to get to its beginwrite call
        Sleep(500);
      end;
    lock.endwrite;
  end;


procedure terrorcheck.execute;
begin
  { make sure we don't exit before this thread has initialised, since    }
  { it can allocate memory in its initialisation, which would cause      }
  { problems for heaptrc as it goes over the memory map in its exit code }
  waiting:=true;
  { avoid deadlocks/bugs from causing this test to never quit }
  sleep(1000*60);
  writeln('error 4');
  halt(4);
end;


var
  r1,r2,r3,r4,r5,r6: treadcounter;
  w1,w2,w3,w4: twritecounter;
  rw1,rw2,rw3: treadwritecounter;
  d1: tdeadlock1;
  d2: tdeadlock2;
  dr1: tdoublereadonewrite1;
  dr2: tdoublereadonewrite2;
  wr1: twrongthreadendacquire;
  wr2: twrongthreadendrelease;
  dw1, dw2: tdoublewrite;
  caught: boolean;
begin
  waiting:=false;
  terrorcheck.create(false);
  randomize;
  lock:=TMultiReadExclusiveWriteSynchronizer.create;
  event1:=RTLEventCreate;
  event2:=RTLEventCreate;

  { verify that the lock is recursive }
  if not lock.beginwrite then
    halt(10);
  if not lock.beginwrite then
    halt(11);
  lock.endwrite;
  lock.endwrite;

  { verify that we can upgrade a read lock to a write lock }
  lock.beginread;
  if not lock.beginwrite then
    halt(12);
  lock.endwrite;
  lock.endread;

  { verify that owning a write lock does not prevent getting a read lock }
  if not lock.beginwrite then
    halt(13);
  lock.beginread;
  lock.endread;
  lock.endwrite;

  { verify that calling endread without beginread throws an exception }
  caught:=false;
  try
    lock.endread;
  except
    caught:=true;
  end;
  if not caught then
    halt(14);

  { verify that calling endwrite without beginwrite throws an exception }
  caught:=false;
  try
    lock.endwrite;
  except
    caught:=true;
  end;
  if not caught then
    halt(15);


  { first try some writers }
  w1:=twritecounter.create;
  w2:=twritecounter.create;
  w3:=twritecounter.create;
  w4:=twritecounter.create;
  w1.resume;
  w2.resume;
  w3.resume;
  w4.resume;
  w1.waitfor;
  w2.waitfor;
  w3.waitfor;
  w4.waitfor;
  
  { must not have caused any data races }
  if (gcount<>w1.localcount+w2.localcount+w3.localcount+w4.localcount) then
    begin
      writeln('error 2');
      halt(2);
    end;

  w1.free;
  w2.free;
  w3.free;
  w4.free;

  { mixed readers and writers with proper synchronisation }
  gcount:=0;
  rw1:=treadwritecounter.create(true);
  rw2:=treadwritecounter.create(false);
  rw3:=treadwritecounter.create(false);

  rw1.resume;
  rw2.resume;
  rw3.resume;

  rw1.waitfor;
  rw2.waitfor;
  rw3.waitfor;

  { must not have caused any data races }
  if (gcount<>rw1.localcount+rw2.localcount+rw3.localcount) then
    begin
      writeln('error 5');
      halt(5);
    end;

  RTLEventResetEvent(event1);
  RTLEventResetEvent(event2);

  { check deadlock detection }
  d1:=tdeadlock1.create(false);
  d2:=tdeadlock2.create(false);

  d1.waitfor;
  d2.waitfor;
  if not gotdeadlockexception then
    halt(6);

  d1.free;
  d2.free;


  { check that a waiting writer does not block a reader trying to get
    a recursive read lock it already holds }
  dr1:=tdoublereadonewrite1.create(false);
  dr2:=tdoublereadonewrite2.create(false);

  dr1.waitfor;
  dr2.waitfor;

  dr1.free;
  dr2.free;

  { check that releasing a lock in another thread compared to where it
    was acquired causes an exception }
  wr1:=twrongthreadendacquire.create(true);
  wr2:=twrongthreadendrelease.create(true);
  wr1.waitfor;
  wr2.waitfor;
  wr1.free;
  wr2.free;

  wr1:=twrongthreadendacquire.create(false);
  wr2:=twrongthreadendrelease.create(false);
  wr1.waitfor;
  wr2.waitfor;
  wr1.free;
  wr2.free;

  dw1:=tdoublewrite.create(false);
  dw2:=tdoublewrite.create(true);
  dw1.waitfor;
  dw2.waitfor;
  dw1.free;
  dw2.free;

  RTLEventDestroy(event1);
  RTLEventDestroy(event2);

  lock.free;

  while not waiting do
    sleep(20);
end.
