#####################################################################################
#
#  Copyright (c) Microsoft Corporation. All rights reserved.
#
#  This source code is subject to terms and conditions of the Shared Source License
#  for IronPython. A copy of the license can be found in the License.html file
#  at the root of this distribution. If you can not locate the Shared Source License
#  for IronPython, please send an email to ironpy@microsoft.com.
#  By using this source code in any fashion, you are agreeing to be bound by
#  the terms of the Shared Source License for IronPython.
#
#  You must not remove this notice, or any other, from this software.
#
######################################################################################

# test cases to verify the thread safety of the IronPython engine
from Util.Debug import *
from System import *
from System.Threading import *


class MyOC:
    one = 'one'
    two = 'two'
    three = 'three'
    four = 'four'
    five = 'five'
    six = 'six'

class MyUserType(object):
    one = 'one'
    two = 'two'
    three = 'three'
    four = 'four'
    five = 'five'
    six = 'six'

class BaseUserTypeOne(object):
    pass

class BaseUserTypeTwo(object):
    pass

class DerivedUserType(BaseUserTypeOne):
    pass

    
    
go = ManualResetEvent(False)

# setup a table used for transforming loop iterations into identifiers.
# 1 becomes A, 11 becomes AA, 12 becomes AB, 123 becomes ABC, etc...

identityTable = [chr(x) for x in range(256)]
transTable = ''
for x in identityTable: 
    if ord(x) >= ord('0') and ord(x) <= ord('9'): 
        transTable = transTable + chr(ord('A') + ord(x) - ord('0'))
    else: 
        transTable = transTable + x

# base test infrastructure, reader thread, writer thread, and test
# runner
def reader():
    try:
        try:
            go.WaitOne()
            for i in xrange(loopCnt): readerWorker(i)
        except Exception, e:
            print 'Test failed, unexpected exception from reader ', e
            AreEqual(True, False)
    finally:
        global readerAlive
        readerAlive = False

class writer:
    def __init__(self, writer, index):
        self.writer = writer
        self.index = index
    def __call__(self, *args):
        try:
            try:
                go.WaitOne()  
                for i in xrange(loopCnt): self.writer(i, self.index)
            except Exception, e:
                print 'Test failed (writer through exception)', e          
                AreEqual(True, False)
        finally:
            global writerAlive
            writerAlive = False

def runTest():
    global writerAlive
    global readerAlive
    writerAlive = True
    readerAlive = True
    
    a = Thread(reader)
    a.IsBackground = True
    a.Start()
    
    if isinstance(writerWorker, tuple):
        b = []
        index = 0
        for wr in writerWorker:
            th = Thread(writer(wr, index))
            th.IsBackground = True
            th.Start()        
            b.append(th)
            index = index + 1
    else:
        b = [Thread(writer(writerWorker, 0))]
        b[0].IsBackground = True
        b[0].Start()
    
    go.Set()
    
    a.Join()
    for th in b: th.Join()

# individual test cases, setup reader/writer workers
# and then call run test    


# OldClass test cases

def Reader_Dir(i):
    x = dir(oc)

def Reader_CheckOneKey(i):
    AreEqual(getattr(oc, 'outsideFuncEnv'), outsideFuncEnvValue)
    
def Reader_HasAttr(i):
    attrName = str(i).translate(transTable)
    hasattr(oc, attrName)

def Reader_VerifyAllPresent(i):
    # verify that we can find all the previous values in the dictionary
    global writerAlive
    if not writerAlive: return  # writer takes signifcantly less time, stop checking once we're no longer experiencing contention
    
    global prevValues
    for x in prevValues.keys():
        #print 'checking ', x
        AreEqual(hasattr(oc, x), True)
        AreEqual(getattr(oc, x), prevValues[x])
    for x in dir(oc):
        if not prevValues.has_key(x):
            prevValues[x] = getattr(oc, x)
            
# generic writer, just keeps adding more and more attributes
def Writer_Generic(i, writerIndex):
    attrName = str(i).translate(transTable)
    x = object()
    setattr(oc, attrName, x)
    AreEqual(getattr(oc, attrName), x)

# generic writer, adds  an attribute and then removes it.
def Writer_Generic_Delete(i, writerIndex):
    attrName = str(i).translate(transTable)
    x = object()
    setattr(oc, attrName, x)
    AreEqual(getattr(oc, attrName), x)
    delattr(oc, attrName)

# adds attributes to the class, deletes them in batches of 50
def Writer_Generic_BatchDelete(i, writerIndex):
    attrName = str(i).translate(transTable)
    x = object()
    setattr(oc, attrName, x)
    AreEqual(getattr(oc, attrName), x)
    if i != 0 and i % 50 == 0:
        for x in xrange(i-50, i):
            attrName = str(x).translate(transTable)
            delattr(oc, attrName)
    
    
def Reader_ExtraKeys(i):
    global oc
    if (i % 6) == 0:
        # create a new instance every 6 iterations (6 is the number
        # of custom keys per old-instance dict)
        oc = MyOC()
    if hasattr(oc, 'abc'): AreEqual(oc.abc, 'abc')
    if hasattr(oc, 'abcd'): AreEqual(oc.abcd, 'abcd')
    if hasattr(oc, 'abcde'): AreEqual(oc.abcde, 'abcde')
    if hasattr(oc, 'abcdef'): AreEqual(oc.abcdef, 'abcdef')
    if hasattr(oc, 'abcdefg'): AreEqual(oc.abcdefg, 'abcdefg')
    if hasattr(oc, 'abcdefgh'): AreEqual(oc.abcdefgh, 'abcdefgh')
    
def Writer_ExtraKeys(i, writerIndex):
    if i % 6 == 0: oc.abc = 'abc'
    elif i % 6 == 1: oc.abcd = 'abcd'
    elif i % 6 == 2: oc.abcde = 'abcde'
    elif i % 6 == 3: oc.abcdef = 'abcdef'
    elif i % 6 == 4: oc.abcdefg = 'abcdefg'
    elif i % 6 == 5: oc.abcdefgh = 'abcdefgh'
    
    
def Init_OldClass(loopCnt):
    global oc, outsideFuncEnvValue, loopCnt, prevValues
    prevValues = {}

    outsideFuncEnvValue = object()
    oc = MyOC()
    
    # fill in some attributes so we don't hit extra values...
    for id in range(loopCnt, loopCnt+20):
        setattr(oc, str(id).translate(transTable), object())
    
    # now set a value that's outside of the extra values
    setattr(oc, 'outsideFuncEnv', outsideFuncEnvValue)
    

def Nop(i):
    pass

# List Test Cases
    
def List_Writer(i, writerIndex):
    myList.append(i)
    
def List_Writer_Extend(i, writerIndex):
    myList.extend([0,1,2,3,4,5,6,7,8,9])
    
def List_Reader(i):
    global lastRead
    if myList: lastRead = myList[-1]
        
def List_Clear(i, writerIndex):
    if i % 50 == 0:
        if myList: del myList[:]

def List_Slice_Set(i, writerIndex):
    if i % 50 == 0:
        myList[:] = []

def List_Index_Writer(i, writerIndex):
    global lastRead
    if myList: myList[-1] = lastRead

def Init_List_Sort(loopCnt):
    global listOfLists1,listOfLists2
    
    listOfLists1 = []
    listOfLists2 = []
    for x in range(200): 
        listOfLists1.append([])
        listOfLists2.append([])
    for x in listOfLists1:
        for y in listOfLists2:
            x.append(y)
            y.append(x)

def List_Sorter(i, writerIndex):
    global listOfLists1, listOfLists2
    if writerIndex == 1: listOfLists1.sort()    
    else: listOfLists2.sort()

def Init_List(loopCnt):
    global myList, lastRead
    myList = []
    lastRead = 0

def PostCondition_List(expectedLength):
    # create a closer around the inner function w/ the expected length
    def PostCondition_List_Inner():
        AreEqual(len(myList), expectedLength)
    return PostCondition_List_Inner

# user type test cases

# verify mro / base classes update atomically
def UserType_Write_Bases(i, writerIndex):
    if BaseUserTypeOne in myUserType.__bases__:
        myUserType.__bases__ = (BaseUserTypeTwo,)
    else:
        myUserType.__bases__ = (BaseUserTypeOne,)

def UserType_Read_BasesAndMro(i):
    bases = myUserType.__bases__
    Assert(bases == (BaseUserTypeOne,) or bases == (BaseUserTypeTwo,))
    mro = myUserType.__mro__
    Assert(mro == (DerivedUserType, BaseUserTypeOne, object) or mro == (DerivedUserType, BaseUserTypeTwo, object))

def Init_UserType_Bases(loopCnt):
    global myUserType
    myUserType = DerivedUserType
    
def Init_UserType(loopCnt):    
    global oc, outsideFuncEnvValue, loopCnt, prevValues
    prevValues = {}
    
    outsideFuncEnvValue = object()
    oc = MyUserType()
    
    # set a value that's outside of the extra values
    setattr(oc, 'outsideFuncEnv', outsideFuncEnvValue)
    
def Init_NewTypeMaker(loopCnt):
    global baseTypes
    baseTypes = (object,)

def CreateType(i, writerIndex):
    global baseTypes
        
    # we keep deriving a new type from a new combination of base types - always
    # object and an ever-growing list of old-style classes.  Thread 0 is responsible
    # for pushing a new base type in.  This causes all the creation threads to hit
    # contention and wait for a type to be published.
    type('foo', baseTypes, {})
    
    if writerIndex == 0:
        class foo: pass
        baseTypes = baseTypes + (foo,)



testCases = [ # initialization, reader, writer, test loopCnt, post-condition.  
              #    Multiple writes can be provided w/ a tuple   
              
              # new type maker tests
              (Init_NewTypeMaker, Nop, (CreateType, CreateType, CreateType, CreateType, CreateType, CreateType), 100),
              
              # user type tests
              (Init_UserType, Reader_ExtraKeys, (Writer_ExtraKeys,Writer_ExtraKeys), 100000),
              (Init_UserType, Reader_CheckOneKey, Writer_Generic_BatchDelete, 10000), 
              (Init_UserType, Reader_CheckOneKey, Writer_Generic, 10000), 
              (Init_UserType, Reader_CheckOneKey, Writer_Generic_Delete, 10000), 
              (Init_UserType, Reader_Dir, Writer_Generic_BatchDelete, 10000),             
              (Init_UserType, Reader_VerifyAllPresent, Writer_Generic, 10000),
              (Init_UserType, Reader_Dir, Writer_Generic_Delete, 10000),
              (Init_UserType, Reader_HasAttr, Writer_Generic, 10000),             
              (Init_UserType_Bases, UserType_Read_BasesAndMro, UserType_Write_Bases, 10000),
              
              # list tests
              
              (Init_List_Sort, Nop, (List_Sorter, List_Sorter), 1000),
              (Init_List, List_Reader, (List_Index_Writer, List_Slice_Set), 100000),           
              (Init_List, List_Reader, (List_Index_Writer, List_Clear), 100000),           
              (Init_List, List_Reader, (List_Writer), 10000, PostCondition_List(10000)),           
              (Init_List, List_Reader, (List_Index_Writer, List_Writer_Extend), 10000, PostCondition_List(10000*10)),           
              (Init_List, List_Reader, (List_Writer, List_Index_Writer), 10000, PostCondition_List(10000)),           
              (Init_List, Nop, (List_Writer_Extend, List_Writer_Extend), 10000, PostCondition_List(10000*2*10)),           
              (Init_List, Nop, (List_Writer, List_Writer, List_Writer), 10000, PostCondition_List(10000*3)),           
              
              # old-style class tests
              (Init_OldClass, Reader_ExtraKeys, (Writer_ExtraKeys,Writer_ExtraKeys), 100000),
              (Init_OldClass, Reader_CheckOneKey, Writer_Generic_BatchDelete, 10000), 
              (Init_OldClass, Reader_CheckOneKey, Writer_Generic, 10000), 
              (Init_OldClass, Reader_CheckOneKey, Writer_Generic_Delete, 10000), 
              (Init_OldClass, Reader_Dir, Writer_Generic_BatchDelete, 10000),             
              (Init_OldClass, Reader_VerifyAllPresent, Writer_Generic, 10000),
              (Init_OldClass, Reader_Dir, Writer_Generic_Delete, 10000),
              (Init_OldClass, Reader_HasAttr, Writer_Generic, 10000),             
            ]

def doOneTest(test):
    """runs a single test, argument is a testCase tuple"""
    print 'running test', test
    global loopCnt, writerWorker, readerWorker
    
    # call init function
    loopCnt = test[3]
    test[0](loopCnt)
    
    # set our workers
    readerWorker = test[1]
    writerWorker = test[2]
    
    # set our loopcnt
    
    # run the test
    runTest()

    if len(test) >= 5: test[4]()

if __name__ == '__main__':
    import sys
    
    if len(sys.argv) == 1:
        # run all tests
        for test in testCases:
            doOneTest(test)
    else:
        # run one test
        toRun = list(sys.argv)
        del toRun[0]
        for x in toRun:
            doOneTest(testCases[int(x)])
