###############################################################################
#
# tkt-72905c9a77.eagle --
#
# Written by Joe Mistachkin.
# Released to the public domain, use at your own risk!
#
###############################################################################

package require Eagle
package require Eagle.Library
package require Eagle.Test

runTestPrologue

###############################################################################

package require System.Data.SQLite.Test
runSQLiteTestPrologue

###############################################################################

#
# NOTE: Make sure that SQLite core library is completely shutdown prior to
#       starting any of the tests in this file.
#
shutdownSQLite $test_channel

###############################################################################

#
# NOTE: This value is needed as part of the test result; therefore, it must be
#       set outside of the test setup.
#
if {[haveSQLiteObjectCommand]} then {
  set id [object invoke Interpreter.GetActive NextId]
} else {
  set id [clock seconds]
}

###############################################################################

#
# NOTE: *WARNING* This test has been extremely carefully designed; however, it
#       is still quite sensitive to machine timing, resource availability, etc.
#       This test MAY pass even if the bug under test has not been fixed (or
#       has been regressed somehow).  However, due to the unpredictable nature
#       of race conditions, it really is the best that can be done.  This test
#       will only work as intended if the version of System.Data.SQLite being
#       tested is 1.0.77.0 or higher.
#
runTest {test tkt-72905c9a77-1.1 {StaticIsInitialized race condition} -setup {
  set fileName tkt-72905c9a77-1.1.db
} -body {
  set dataSource [file join [getDatabaseDirectory] $fileName]

  unset -nocomplain results errors

  set code [compileCSharpWith [subst {
    using System;
    using System.Data.SQLite;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Text;
    using System.Threading;

    namespace _Dynamic${id}
    {
      public static class Test${id}
      {
        public static string GetTraceOutput()
        {
          //
          // NOTE: Create a memory stream to capture all the trace output for
          //       this test.
          //
          MemoryStream memoryStream = new MemoryStream();

          //
          // NOTE: Create the trace listener using the memory stream we just
          //       created.
          //
          using (TraceListener listener = new TextWriterTraceListener(
              memoryStream))
          {
            //
            // NOTE: Add the trace listener to the collection of active trace
            //       listeners (for this application domain).
            //
            Trace.Listeners.Add(listener);

            //
            // NOTE: Attempt to lookup the type for the private SQLite3 class
            //       in the System.Data.SQLite assembly.  We need the type in
            //       order to lookup the primary method used for this test (via
            //       reflection).  This is only necessary because the method
            //       under test is private and cannot normally be executed from
            //       C# directly.  If this fails, the following statement will
            //       throw a NullReferenceException, which is fine as that will
            //       cause the whole test to fail.
            //
            Type type = Type.GetType(
                "System.Data.SQLite.SQLite3, System.Data.SQLite");

            //
            // NOTE: Attempt to lookup the method object for the private method
            //       we need for this test.  If this fails, the first attempt
            //       to invoke the method using this variable will throw a
            //       NullReferenceException, which is fine as that will cause
            //       the whole test to fail.
            //
            MethodInfo methodInfo = type.GetMethod("StaticIsInitialized",
                BindingFlags.Static | BindingFlags.NonPublic);

            //
            // NOTE: Create the event that will be used to synchronize all the
            //       created threads so that they start doing their actual test
            //       "work" at approximately the same time.
            //
            using (ManualResetEvent goEvent = new ManualResetEvent(false))
            {
              //
              // NOTE: Create 4 threads for each processor on the machine.
              //       Under normal circumstances, this should give us a good
              //       chance of triggering the race condition being tested.
              //       However, since this test was originally designed on a
              //       machine with 4 processors, limit the total number of
              //       threads to 16.
              //
              int count = Math.Min(4 * Environment.ProcessorCount, 16);

              //
              // NOTE: Create a (reusable) delegate that will contain the code
              //       that each created thread is to execute.
              //
              ThreadStart threadStart = delegate()
              {
                try
                {
                  //
                  // NOTE: Wait forever for the "GO" signal so that all threads
                  //       can start working at approximately the same time.
                  //
                  goEvent.WaitOne();

                  //
                  // NOTE: Create a pseudorandom number generator suitable for
                  //       waiting a random number of milliseconds between each
                  //       attempt to cause the race condition being tested on
                  //       a given thread.
                  //
                  Random random = new Random();

                  //
                  // NOTE: Force the SQLiteLog.StaticIsInitialized method to
                  //       be repeatedly called on every thread right away to
                  //       thoroughly test its locking semantics.  Also, use a
                  //       random delay, in milliseconds, between zero and the
                  //       number of test threads squared after each attempt.
                  //
                  for (int index = 0; index < (count * count); index++)
                  {
                    methodInfo.Invoke(null, null);
                    Thread.Sleep(random.Next(0, (count * count)));
                  }

                  //
                  // NOTE: Create and open a connection and use it to log a
                  //       test message just to make sure that the logging
                  //       system is initialized and in working order.
                  //
                  using (SQLiteConnection connection = new SQLiteConnection(
                      "Data Source=${dataSource};[getTestProperties]"))
                  {
                    connection.Open();
                    connection.LogMessage(0, "TEST ${id}");
                  }
                }
                catch (Exception e)
                {
                  //
                  // NOTE: We caught an exception.  Since this will impact the
                  //       captured trace output, this will cause the test to
                  //       fail (just as it should).
                  //
                  Trace.WriteLine(String.Format("CAUGHT: {0}", e));
                }
              };

              //
              // NOTE: Create the array of thread objects.
              //
              Thread\[\] thread = new Thread\[count\];

              //
              // NOTE: Create each of the test threads with a suitable stack
              //       size.  We must specify a stack size here because the
              //       default one for the process would be the same as the
              //       parent executable (the Eagle shell), which is 16MB,
              //       too large to be useful.
              //
              for (int index = 0; index < count; index++)
              {
                thread\[index\] = new Thread(threadStart, 1048576);

                //
                // NOTE: Name each thread for a better debugging experience.
                //
                thread\[index\].Name = String.Format(
                    "[file rootname ${fileName}] #{0}", index);
              }

              //
              // NOTE: Force logging to be initialized now; otherwise, there is
              //       no way for the native SQLite library to impact the trace
              //       listener we are monitoring for output.
              //
              SQLiteLog.Initialize();

              //
              // NOTE: Start all the threads now.  They should not actually do
              //       any of the test "work" until we signal the event.
              //
              for (int index = 0; index < count; index++)
                thread\[index\].Start();

              //
              // NOTE: Send the signal that all threads should start doing
              //       their test "work" now.
              //
              goEvent.Set(); /* GO */

              //
              // NOTE: Wait forever for each thread to finish its test "work"
              //       and then die.
              //
              for (int index = 0; index < count; index++)
                thread\[index\].Join();
            }

            //
            // NOTE: *REQUIRED* Force all the trace listeners to be flushed to
            //       disk now so that we do not lose any output.  Without this
            //       method call, loss of trace output was observed.
            //
            Trace.Flush();

            //
            // NOTE: The trace listener used by this test can be removed now
            //       as all the trace output should have been flushed to the
            //       memory stream now.
            //
            Trace.Listeners.Remove(listener);

            //
            // NOTE: Return a string containing all the trace output we saw
            //       (from all threads) during the above test code.
            //
            return Encoding.UTF8.GetString(memoryStream.ToArray());
          }
        }

        ///////////////////////////////////////////////////////////////////////

        public static void Main()
        {
          // do nothing.
        }
      }
    }
  }] true true true results errors System.Data.SQLite.dll]

  list $code $results \
      [expr {[info exists errors] ? $errors : ""}] \
      [expr {$code eq "Ok" ? [catch {
        object invoke _Dynamic${id}.Test${id} GetTraceOutput
      } result] : [set result ""]}] [string map [list \r\n \n] $result]
} -cleanup {
  cleanupDb $fileName

  unset -nocomplain result code results errors dataSource fileName
} -constraints \
[fixConstraints {fail.false eagle command.object monoBug28\
buildConfiguration.Release !defineConstant.System.Data.SQLite.INTEROP_LOG\
command.sql compile.DATA SQLite System.Data.SQLite compileCSharp}] -match \
regexp -result [appendArgs \
"^Ok System#CodeDom#Compiler#CompilerResults#\\d+ \\{\\} 0 \\{" \
[string repeat "SQLite message \\(0\\): TEST $id\\n" \
[expr {min(4 * [info processors], 16)}]] "\\}\$"]}

###############################################################################

unset -nocomplain id

###############################################################################

runSQLiteTestEpilogue
runTestEpilogue
