def compareSymbol(settings, testName, name, newSym, expSym, newSymbols, expSymbols, errors):
  newVal = newSym.value
  newLiNo = newSym.liNo
  expVal = expSym.value
  expLiNo = expSym.liNo
  if name in symMustChangeBetweenHands and not newSymbolsFinalPrevHand is None and newVal == newSymbolsFinalPrevHand.getValue(name):
    createErrorEntry(testName, errors.symbols, msg="Symbol '" + name + "' should change value between hands but does not", nLiNo=newLiNo)
    return True
  if name in symMustBeSameDuringHand and not newSymbolsPrevDumpThisHand is None and newVal != newSymbolsPrevDumpThisHand.getValue(name):
    createErrorEntry(testName, errors.symbols, msg="Symbol '" + name + "' should not change value during hand but does", nLiNo=newLiNo)
    return True
  #fixme: what if we dumped multiple times in one heardbeat (e.g. prefold call and alli call, or wait and alli, or 2 alli because of illegal action)???
  if name in symMustChangeEachHeardbeat and not newSymbolsPrevDumpThisHand is None and newVal == newSymbolsPrevDumpThisHand.getValue(name):
    createErrorEntry(testName, errors.symbols, msg="Symbol '" + name + "' should change each heartbeat but does not", nLiNo=newLiNo)
    return True
  if name in symMustChangePerBetround and not newSymbolsPrevBetround is None and newVal == newSymbolsPrevBetround.getValue(name):
    createErrorEntry(testName, errors.symbols, msg="Symbol '" + name + "' should change each betround but does not", nLiNo=newLiNo)
    return True
  if name in symMustBeSameDuringBetround and not newSymbolsPrevDumpThisHand is None and newSymbols.getValue('betround') ==  newSymbolsPrevDumpThisHand.getValue('betround') and newVal != newSymbolsPrevDumpThisHand.getValue(name):
    createErrorEntry(testName, errors.symbols, msg="Symbol '" + name + "' should not change value during betround but does", nLiNo=newLiNo)
    return True
  for prefix in symDontRequireSameValuePrefixes:
    if name.startswith(prefix):
      return True
    
  #convert card values between different OH versions if needed
  for p in cardSymbolPrefix:
    if name.startswith(p):
      newVal = convertCardValue(settings, name, newVal, newSymbols)
  #return newVal == expVal
  return floatEq(newVal, expVal)
def compareSymbols(settings, testName, newSymbols, expSymbols, errors):
  #fixme: bad idea? config value to allow change? for sure since symbols can be added or removed to OH
  if len(newSymbols.keys()) != len(expSymbols.keys()):
    createErrorEntry(testName, errors.symbols, msg='Number of symbols does not match', nLiNo=newSymbols.liNo, eLiNo=expSymbols.liNo)
  #do some extra eval on betround since we rely on it!
  if not newSymbolsPrevDumpThisHand is None:
    if newSymbols.getValue('betround') ==  newSymbolsPrevDumpThisHand.getValue('betround') and not isSameBoard(newSymbols, newSymbolsPrevDumpThisHand):
      createErrorEntry(testName, errors.symbols, msg='Board cards changed during betround', nLiNo=newSymbols.liNo)
    elif newSymbols.getValue('betround') !=  newSymbolsPrevDumpThisHand.getValue('betround') and isSameBoard(newSymbols, newSymbolsPrevDumpThisHand):
      createErrorEntry(testName, errors.symbols, msg='Board cards the same but is new betround', nLiNo=newSymbols.liNo)
      
  for n in newSymbols.keys():
    
    #to compare with earlier OH versions with different symbols
    #and some symbols are new or do no longer exist
    ignoreSym = False
    for p in settings.ignoreSymbolsPrefix:
      if n.startswith(p):
        ignoreSym = True
        break
    if ignoreSym or n in settings.ignoreSymbolsExact:
      continue
    
    newNV = newSymbols.get(n)
    expNV = expSymbols.get(n)
    newVal = newNV.value
    newLiNo = newNV.liNo
    expVal = None
    expLiNo = expSymbols.liNo
    if not expNV is None:
      expVal = expNV.value
      expLiNo = expNV.liNo
      if not compareSymbol(settings, testName, n, newNV, expNV, newSymbols, expSymbols, errors):
        createErrorEntry(testName, errors.symbols, msg="Symbol '" + n + "' does not match: '" + str(newVal) + "' != '" + str(expVal) + "'", nLiNo=newLiNo, eLiNo=expLiNo)
    else:
      createErrorEntry(testName, errors.symbols, msg="Symbol '" + n + "' missing in expected output log", nLiNo=newLiNo, eLiNo=expSymbols.liNo)
def compareStates(testName, newState, expState, errors):
  if len(newState.keys()) != len(expState.keys()):
    createErrorEntry(testName, errors.state, msg='Number of entries in state does not match', nLiNo=newState.liNo, eLiNo=expState.liNo)
  for n in newState.keys():
    newNV = newState.get(n)
    expNV = expState.get(n)
    newVal = newNV.value
    newLiNo = newNV.liNo
    expVal = None
    expLiNo = None
    if not expNV is None:
      expVal = expNV.value
      expLiNo = expNV.liNo
      if expVal != newVal:
        createErrorEntry(testName, errors.state, msg="State entry '" + n + "' does not match: '" + str(newVal) + "' != '" + str(expVal) + "'", nLiNo=newLiNo, eLiNo=expLiNo)
    else:
      createErrorEntry(testName, errors.state, msg="State entry '" + n + "' missing in expected output log", nLiNo=newLiNo, eLiNo=expState.liNo)
def handleSymbolsAndStates(settings, testName, newLineEntries, expLineEntries, errors):
  newSymbolsPrevDumpThisHand = None
  newSymbolsPrevBetround = None
  curBetround = -1
  
  newSymStatePairList = parseNamedValues(testName, newLineEntries, False, errors)
  expSymStatePairList = parseNamedValues(testName, expLineEntries, True, errors)
  
  #print("DEBUG: len(newLineEntries)=%i len(expLineEntries)=%i len(newSymStatePairList)=%i len(expSymStatePairList)=%i" % (len(newLineEntries), len(expLineEntries), len(newSymStatePairList), len(expSymStatePairList))) 
  
  castSymbolValuesToFloat(newSymStatePairList, expSymStatePairList)
  
  newAllInCaseSymPairList = hashAndFindAllInStates(newSymStatePairList)
  expAllInCaseSymPairList = hashAndFindAllInStates(expSymStatePairList)
  
  #fixme: the first one is empty!!!!
  #if len(newAllInCaseSymPairList) == 0:
  #  createErrorEntry(testName, errors.fatal, msg='No all-in messages in this test case', nLiNo=newLineEntries[0].liNo, eLiNo=expLineEntries[0].liNo)
  #  return
  
  if len(newAllInCaseSymPairList) != len(expAllInCaseSymPairList):
    if len(newLineEntries) > 0 and  len(expLineEntries) > 0:
        createErrorEntry(testName, errors.fatal, msg='Number of all-in messages in this test case do not match', nLiNo=newLineEntries[0].liNo, eLiNo=expLineEntries[0].liNo)
    elif len(newLineEntries) > 0:
        createErrorEntry(testName, errors.fatal, msg='Number of all-in messages in this test case do not match', nLiNo=newLineEntries[0].liNo)
    else:
        createErrorEntry(testName, errors.fatal, msg='Number of all-in messages in this test case do not match')
  
  for i in range(0, min(len(newAllInCaseSymPairList), len(expAllInCaseSymPairList))):
    newSymbols = newAllInCaseSymPairList[i][0]
    expSymbols = expAllInCaseSymPairList[i][0]
    newState = newAllInCaseSymPairList[i][1]
    expState = expAllInCaseSymPairList[i][1]
    compareStates(testName, newState, expState, errors)
    compareSymbols(settings, testName, newSymbols, expSymbols, errors)
    newSymbolsPrevDumpThisHand = newSymbols
    if curBetround + 0.1 < newSymbols.getValue('betround'):
      newSymbolsPrevBetround = newSymbols
      curBetround = newSymbols.getValue('betround')
    
  newSymbolsFinalPrevHand = newSymbolsPrevDumpThisHand
def parseNamedValues(testName, lineEntries, isExpectedLog, errors):
  inParseSymbolMode = False
  inParseStateMode = False
  curSymbolContainer = None
  curContainer = None
  newLiNo = None
  expLiNo = None
  containerPairList = []
  inExpStr = ''
  if isExpectedLog:
    inExpStr = '(in expected log!): '
  for e in lineEntries:
    #ensure we log line numbers for the correct log file
    if not isExpectedLog:
      newLiNo = e.liNo
      expLiNo = None
    else:
      newLiNo = -1
      expLiNo = e.liNo
    mSym = beginSymbolDumpRe.match(e.line)
    mState = beginStateDumpRe.match(e.line)
    if mSym or mState:
      if inParseSymbolMode or inParseStateMode:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + 'Next Dump seems to start inside another dump', nLiNo=newLiNo, eLiNo=expLiNo)
      if mSym:
        inParseSymbolMode = True
        inParseStateMode = False
        curContainer = NamedValueContainer(int(mSym.group(1)), bool(mSym.group(2)), e.liNo)
      elif mState:
        inParseSymbolMode = False
        inParseStateMode = True
        curContainer = NamedValueContainer(int(mState.group(1)), bool(mState.group(2)), e.liNo)
      continue
    
    if endSymbolDumpRe.match(e.line):
      if not inParseSymbolMode or inParseStateMode:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + 'Unexpected Symbol Dump end marker', nLiNo=newLiNo, eLiNo=expLiNo)
      #we add it to result if we have state too
      inParseSymbolMode = False
      inParseStateMode = False
      curSymbolContainer = curContainer
      curContainer = None
      continue
      
    if endStateDumpRe.match(e.line):
      if inParseSymbolMode or not inParseStateMode:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + 'Unexpected State Dump end marker', nLiNo=newLiNo, eLiNo=expLiNo)
      if curSymbolContainer is None:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + "Fatal: State without previous matching Symbols", nLiNo=newLiNo, eLiNo=expLiNo)
        curSymbolContainer = NamedValueContainer(curContainer.inHandMsgNum, curContainer.isAllInMsg, curContainer.liNo)
      if curSymbolContainer.isAllInMsg != curContainer.isAllInMsg:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + "Fatal: 'isAllInMsg' of state and symbols not matching", nLiNo=newLiNo, eLiNo=expLiNo)
      inParseSymbolMode = False
      inParseStateMode = False
      containerPairList.append((curSymbolContainer, curContainer))
      curSymbolContainer = None
      curContainer = None
      continue
    
    if inParseSymbolMode or inParseStateMode:
      m = namedValueLineRe.match(e.line)
      if m:
        curContainer.add(m.group(1), m.group(2), e.liNo)
      else:
        createErrorEntry(testName, errors.fatal, msg=inExpStr + 'In Dump and cannot parse line as named value', nLine=e.line, nLiNo=newLiNo, eLiNo=expLiNo)
  
  if inParseSymbolMode or inParseStateMode:
    createErrorEntry(testName, errors.fatal, msg=inExpStr + 'Dump seems to have no end inside test case', nLiNo=newLiNo, eLiNo=expLiNo)
  return containerPairList