Index: /exampleparties/randomparty/requirements.txt
===================================================================
--- /exampleparties/randomparty/requirements.txt	(revision 70)
+++ /exampleparties/randomparty/requirements.txt	(revision 71)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/68/geniuswebcore/dist/geniusweb-1.1.1.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/70/geniuswebcore/dist/geniusweb-1.1.2.tar.gz
 mypy
 
Index: /exampleparties/randomparty/setup.py
===================================================================
--- /exampleparties/randomparty/setup.py	(revision 70)
+++ /exampleparties/randomparty/setup.py	(revision 71)
@@ -3,10 +3,10 @@
 setup(
     name='randomparty',
-    version='1.1.1',    
+    version='1.1.2',    
     description='A python3 party that places random bids with sufficient utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
     packages=['randomparty'],
-    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/68/geniuswebcore/dist/geniusweb-1.1.1.tar.gz"],
+    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/70/geniuswebcore/dist/geniusweb-1.1.2.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/stupidparty/requirements.txt
===================================================================
--- /exampleparties/stupidparty/requirements.txt	(revision 70)
+++ /exampleparties/stupidparty/requirements.txt	(revision 71)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/68/geniuswebcore/dist/geniusweb-1.1.1.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/70/geniuswebcore/dist/geniusweb-1.1.2.tar.gz
 mypy
 
Index: /exampleparties/stupidparty/setup.py
===================================================================
--- /exampleparties/stupidparty/setup.py	(revision 70)
+++ /exampleparties/stupidparty/setup.py	(revision 71)
@@ -3,10 +3,10 @@
 setup(
     name='stupidparty',
-    version='1.1.1',    
+    version='1.1.2',    
     description='A example python3 party module',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
     packages=['stupidparty'],
-    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/68/geniuswebcore/dist/geniusweb-1.1.1.tar.gz"],
+    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/70/geniuswebcore/dist/geniusweb-1.1.2.tar.gz"],
     py_modules=['party'],
 
Index: /geniuswebcore/geniusweb/actions/Action.py
===================================================================
--- /geniuswebcore/geniusweb/actions/Action.py	(revision 70)
+++ /geniuswebcore/geniusweb/actions/Action.py	(revision 71)
@@ -27,2 +27,4 @@
 		pass
 	
+
+	
Index: /geniuswebcore/geniusweb/events/ActionEvent.py
===================================================================
--- /geniuswebcore/geniusweb/events/ActionEvent.py	(revision 70)
+++ /geniuswebcore/geniusweb/events/ActionEvent.py	(revision 71)
@@ -32,3 +32,3 @@
 	def __eq__(self, other):
 		return super().__eq__(other) and \
-			self._actions==other._action
+			self._action==other._action
Index: /geniuswebcore/geniusweb/events/NegotiationEvent.py
===================================================================
--- /geniuswebcore/geniusweb/events/NegotiationEvent.py	(revision 70)
+++ /geniuswebcore/geniusweb/events/NegotiationEvent.py	(revision 71)
@@ -2,7 +2,11 @@
 
 from pyson.JsonSubTypes import JsonSubTypes
+from pyson.JsonTypeInfo import Id, As
+from pyson.JsonTypeInfo import JsonTypeInfo
 
-
-@JsonSubTypes([ "ActionEvent","SessionStarted","TournamentStarted"])
+@JsonSubTypes([ "geniusweb.events.ActionEvent.ActionEvent",
+			    "geniusweb.events.SessionStarted.SessionStarted",
+			    "geniusweb.events.TournamentStarted.TournamentStarted"])
+@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
 class NegotiationEvent (ABC):
 	'''
Index: /geniuswebcore/geniusweb/events/TournamentStarted.py
===================================================================
--- /geniuswebcore/geniusweb/events/TournamentStarted.py	(revision 70)
+++ /geniuswebcore/geniusweb/events/TournamentStarted.py	(revision 71)
@@ -1,64 +1,25 @@
-package geniusweb.events;
+from geniusweb.events.ProtocolEvent import ProtocolEvent
 
-public class TournamentStarted extends ProtocolEvent {
-	private Long numberOfSessions;
+class TournamentStarted (ProtocolEvent):
 
-	@SuppressWarnings("unused") // deserialization
-	private TournamentStarted() {
-		numberOfSessions = null;
-	}
+	def __init__(self, numberOfSessions:int=0,time:int=0):
+		'''
+		@param numberOfSessions the total number of sessions in the tournament
+		@param time              the current timestamp
+	    '''
+		super().__init__(time)
+		self._numberOfSessions = numberOfSessions
+	
+	def getNumberOfSessions(self) -> int:
+		return self._numberOfSessions
 
-	/**
-	 * 
-	 * @param numberOfSessions the total number of sessions in the tournament
-	 */
-	public TournamentStarted(Long numberOfSessions) {
-		this.numberOfSessions = numberOfSessions;
-	}
-
-	/**
-	 * 
-	 * @param numberOfSessions the total number of sessions in the tournament
-	 * @param now              the current timestamp
-	 */
-	public TournamentStarted(Long numberOfSessions, Long now) {
-		super(now);
-		this.numberOfSessions = numberOfSessions;
-	}
-
-	public Long getNumberOfSessions() {
-		return numberOfSessions;
-	}
-
-	@Override
-	public String toString() {
-		return "TournamentStarted[" + getTime() + "," + numberOfSessions + "]";
-	}
-
-	@Override
-	public int hashCode() {
-		final int prime = 31;
-		int result = super.hashCode();
-		result = prime * result + ((numberOfSessions == null) ? 0
-				: numberOfSessions.hashCode());
-		return result;
-	}
-
-	@Override
-	public boolean equals(Object obj) {
-		if (this == obj)
-			return true;
-		if (!super.equals(obj))
-			return false;
-		if (getClass() != obj.getClass())
-			return false;
-		TournamentStarted other = (TournamentStarted) obj;
-		if (numberOfSessions == null) {
-			if (other.numberOfSessions != null)
-				return false;
-		} else if (!numberOfSessions.equals(other.numberOfSessions))
-			return false;
-		return true;
-	}
-
-}
+	def __repr__(self) -> str:
+		return "TournamentStarted[" + str(self.getTime()) + "," + str(self._numberOfSessions) + "]"
+	
+	def __hash__(self):
+		return hash((self._time, self._numberOfSessions))
+	
+	def __eq__(self, other) -> bool:
+		return super().__eq__(other) and self.getTime()==other.getTime() \
+				and self._numberOfSessions==other._numberOfSessions
+				
Index: /geniuswebcore/geniusweb/inform/Settings.py
===================================================================
--- /geniuswebcore/geniusweb/inform/Settings.py	(revision 70)
+++ /geniuswebcore/geniusweb/inform/Settings.py	(revision 71)
@@ -1,3 +1,2 @@
-from geniusweb.actions.PartyId import PartyId
 from geniusweb.inform.Inform import Inform
 from geniusweb.progress.Progress import Progress 
@@ -5,4 +4,5 @@
 from geniusweb.references.ProfileRef import ProfileRef
 from geniusweb.references.ProtocolRef import ProtocolRef
+from geniusweb.actions.PartyId import PartyId
 
 
Index: /geniuswebcore/geniusweb/protocol/NegoSettings.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 70)
+++ /geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 71)
@@ -9,5 +9,6 @@
 
 @JsonSubTypes( ["geniusweb.protocol.session.saop.SAOPSettings.SAOPSettings",
-			"geniusweb.protocol.session.mopac.MOPACSettings.MOPACSettings" ])
+			"geniusweb.protocol.session.mopac.MOPACSettings.MOPACSettings",
+			"geniusweb.protocol.session.learn.LearnSettings.LearnSettings" ])
 @JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT)
 class NegoSettings(ABC):
Index: /geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 70)
+++ /geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 71)
@@ -10,5 +10,5 @@
 from geniusweb.protocol.session.SessionState import SessionState
 from geniusweb.references.PartyWithProfile import PartyWithProfile
-from geniusweb.utils import toTuple
+from geniusweb.utils import toTuple, val
 
 
@@ -17,5 +17,5 @@
 
 
-class DefaultSessionState (SessionState, Generic[S,P]):
+class DefaultSessionState (SessionState, Generic[P,S]):
 	'''
 	The default current state of the session. immutable.
@@ -81,5 +81,5 @@
 		@param actions1       the new {@link Action}s
 		@param conns          the new connected {@link PartyId}s
-		@param progress1      the new {@link Progress}
+		@param progress1      the new {@link Progress}, can be None still
 		@param settings1      the new {@link SessionSettings}. Normally this is
 		                      constant during a session.
@@ -133,5 +133,5 @@
 
 	def WithDeadlineReached(self)->P :
-		return self.With(self._actions, self._connections, self._progress, self._settings, 
+		return self.With(self._actions, self._connections, val(self._progress), self._settings, 
 				self._partyprofiles, self._error) 
 
@@ -140,5 +140,5 @@
 		newconn = list(self._connections)
 		newconn.remove(party);
-		return self.With(self._actions, newconn, self._progress, \
+		return self.With(self._actions, newconn, val(self._progress), \
 				self._settings, self._partyprofiles, self._error)
 
@@ -149,5 +149,5 @@
 		'''
 		assert isinstance(e, ProtocolException)
-		return self.With(self._actions, self._connections, self._progress, 
+		return self.With(self._actions, self._connections, val(self._progress), 
 						self._settings, self._partyprofiles, e)
 
@@ -159,5 +159,5 @@
 		newprofiles = dict(self.getPartyProfiles())
 		newprofiles[connection]= partyprofile
-		return self.With(self.getActions(), newconns, self.getProgress(), self.getSettings(),
+		return self.With(self.getActions(), newconns, self._progress, self.getSettings(),
 				newprofiles, None)
 
@@ -189,5 +189,5 @@
 		newactions = list(self.getActions())
 		newactions.append(action)
-		return self.With(newactions, self._connections, self._progress, 
+		return self.With(newactions, self._connections, val(self._progress), 
 				self._settings, self._partyprofiles,self._error)
 
Index: /geniuswebcore/geniusweb/protocol/session/learn/Learn.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/session/learn/Learn.py	(revision 71)
+++ /geniuswebcore/geniusweb/protocol/session/learn/Learn.py	(revision 71)
@@ -0,0 +1,278 @@
+from datetime import datetime
+import logging
+from threading import Timer
+import threading
+from time import time, sleep
+from typing import Optional, List
+
+from tudelft.utilities.listener.DefaultListenable import DefaultListenable
+from tudelft.utilities.listener.Listener import Listener
+from tudelft.utilities.repository.NoResourcesNowException import NoResourcesNowException
+from tudelft_utilities_logging.Reporter import Reporter
+from uri.uri import URI
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+from geniusweb.inform.Finished import Finished
+from geniusweb.inform.Inform import Inform
+from geniusweb.inform.Settings import Settings
+from geniusweb.progress.ProgressFactory import ProgressFactory
+from geniusweb.protocol.CurrentNegoState import CurrentNegoState
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnections import ProtocolToPartyConnections
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.learn.LearnState import LearnState
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProtocolRef import ProtocolRef
+from geniusweb.utils import val
+
+
+MIN_SLEEP_TIME = 1000
+MAX_SLEEP_TIME = 60000
+TIME_MARGIN = 20 # ms extra delay after deadline
+LEARN = ProtocolRef(URI("Learn"))
+	 
+class Learn (DefaultListenable[ProtocolEvent],SessionProtocol):
+	'''
+	The Learn protocol allows parties to learn until the deadline set in the
+	{@link LearnSettings}.
+	'''
+	_deadlinetimer:Optional[Timer] = None
+# 	private volatile AtomicBoolean isFinishedInfoSent = new AtomicBoolean(
+# 			false);
+	_conns = ProtocolToPartyConnections([])
+	_synclock = threading.RLock()
+	_isFinishedInfoSent = False 
+
+	def __init__(self, state:LearnState, logger:Reporter ):
+		super().__init__()
+		self._state = state
+		self._log = logger
+
+	def start(self, connectionfactory:ProtocolToPartyConnFactory ):
+		try:
+			self._connect(connectionfactory)
+			self._setDeadline()
+			self._setupParties()
+		except Exception as e:
+			self._handleError("Failed to start up session", None, e)
+
+	def getDescription(self)->str:
+		return "Sends all parties the Settings. Parties can start learning immediately but must respect the deadline. When party is done, it should send LearningDone."
+
+	def getState(self)->NegoState :
+		return self._state
+
+
+	def getRef(self)-> ProtocolRef:
+		return LEARN
+
+	def addParticipant(self, party:PartyWithProfile ) :
+		raise ValueError(
+				"Dynamic joining a negotiation is not allowed in LEARN")
+
+	#####################################################################
+	# private functions. Some are protected only, for testing purposes
+	#####################################################################
+	def _connect(self, connectionfactory:ProtocolToPartyConnFactory ):
+		'''
+		step 1 in protocol: connect all involved parties and start the clock.
+		This always "succeeds" with a valid (but possibly final) state
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@param connectionfactory the connectionfactory for making party
+                			 connections
+		
+		@throws InterruptedException if the connection procedure is unterrupted
+		
+		@throws IOException  if this fails to properly conect to the
+                             parties, eg interrupted or server not
+                             responding..
+		'''
+		with self._synclock:
+			participants:List[PartyWithProfile] = self._state.getSettings().getAllParties()
+			
+			parties = [parti.getParty().getPartyRef() for parti in participants ]
+			connections:Optional[List[ProtocolToPartyConn]]  = None
+			self._log.log(logging.INFO, "LEARN connect " + str(parties))
+			while connections == None:
+				try:
+					connections = connectionfactory.connectAll(parties) # type:ignore
+				except NoResourcesNowException as e:
+					waitms = (e.getLater().timestamp()-time())*1000
+					self._log.log(logging.INFO,
+							"No resources available to run session, waiting"+ str(waitms))
+					sleep(min(MAX_SLEEP_TIME,
+							max(MIN_SLEEP_TIME, waitms))/1000)
+			# need val because mypy fails to see that connections can't be None here.
+			for i  in range(len(participants)):
+				self._conns = self._conns.With(val(connections)[i])
+				self._setState(self._state.WithParty(val(connections)[i].getParty(),
+						participants[i]))
+
+	def  _actionRequest(self, partyconn:ProtocolToPartyConn , action:Action ) :
+		'''
+		This is called when one of the party connections does an action.
+		Synchronized so that we always handle only 1 action at a time.
+		
+		@param partyconn the connection on which the action came in.
+		@param action    the {@link Action} taken by some party
+		'''	
+		with self._synclock:
+			if action == None:
+				err = partyconn.getError()
+				if err == None:
+					err = ProtocolException("Party sent a null action",
+							partyconn.getParty())
+				self._handleError(str(partyconn) + "Protocol error", partyconn.getParty(),
+						err) #type:ignore
+				return
+	
+			try:
+				self._setState(self._state.WithAction(partyconn.getParty(), action))
+			except Exception as  e:
+				self._handleError("failed to handle action " + str(action),
+						partyconn.getParty(), e)
+
+
+
+
+	def _setupParties(self):
+		'''
+		step 2 in protocol: listen to connections and send settings to the
+		parties.
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@throws ProtocolException if a party does not follow the protocol
+		'''
+		this=self
+		class MyListener(Listener[Action]):
+			def notifyChange(self, act:Action):
+				this._actionRequest(act)
+
+		
+		with self._synclock:
+
+			for conn in self._conns:
+				conn.addListener(MyListener())
+	
+			for connection in self._conns:
+				try:
+					self._sendSettings(connection)
+				except ConnectionError as e:
+					raise ProtocolException("Failed to initialize",
+							connection.getParty(), e)
+
+
+	def _sendSettings(self, connection:ProtocolToPartyConn ):
+		'''
+		Inform a party about its settings
+		
+		@param connection
+		@throws IOException if party got disconnected
+		'''
+		with self._synclock:
+			partyid = connection.getParty()
+			profile = self._state.getPartyProfiles()[partyid].getProfile()
+			params = self._state.getPartyProfiles()[partyid].getParty().getParameters()
+			if profile == None:
+				raise ValueError(
+						"Missing profile for party " + str(connection.getReference()))
+			connection.send(Settings(connection.getParty(), profile, self.getRef(),
+					val(self._state.getProgress()), params))
+
+	def _setDeadline(self):
+		'''
+		Set state to proper deadline. Starts the timer tasks. This tasks triggers
+		a call to handleError when the session times out.
+		'''
+		with self._synclock:
+			now = int(time()*1000)
+			deadline = self._state.getSettings().getDeadline()
+			self._setState(self._state.WithProgress(ProgressFactory.create(deadline, now)))
+			duration=(deadline.getDuration() + TIME_MARGIN)/1000.0
+			self._deadlinetimer = Timer(duration, self._timertask);
+
+			# set timer TIME_MARGIN after real deadline to ensure we're not too early
+			duration=(deadline.getDuration() + TIME_MARGIN)/1000.0
+			self._deadlinetimer = Timer( duration, self._timertask)
+			self._deadlinetimer.start()
+			self._log.log(logging.INFO, "SAOP deadline set to "
+					+ datetime.utcfromtimestamp( (duration+now)/1000. ).strftime('%Y/%m/%d %H:%M:%S'))
+
+	def _timertask(self):
+		if not self._state.isFinal(1000.*time()):
+			self._log.log(logging.CRITICAL,
+					"BUG. Deadline timer has triggered but state is not final")
+		self._log.log(logging.INFO,
+				"LEARN deadline reached. Terminating session.")
+		self._finish()
+
+
+	def _handleError(self, message:str, party:Optional[PartyId], e:Exception):
+		'''
+		Update state to include the given error and finishes up the session.
+		
+		@param message The message to attach to the error
+		@param party   the party where the error occured
+		@param e       the exception that occured.
+		'''
+		with self._synclock:
+			if isinstance(e,  ProtocolException):
+				self._setState(self._state.WithException( e))
+			else:
+				self._setState(self._state.WithException(ProtocolException(message, party, e)))
+			self._log.log(logging.WARNING, "LEARN protocol intercepted error due to party "\
+					+ str(party) + ":" + message, e)
+
+	def _setState(self, newstate:LearnState) :
+		'''
+		Sets the new state. If the new state is final, the finish-up procedure is
+		executed.
+		
+		@param newstate the new state.
+		'''
+		with self._synclock:
+			now = int(1000*time())
+			if self._state.isFinal(now):
+				self._finish()
+				return
+			self._state = newstate;
+			if newstate.isFinal(now):
+				self._finish()
+				
+	def _finish(self):
+		'''
+		Called when we reach final state. Cancels deadline timer. Send finished
+		info to all parties, notify current nego state as final and set
+		{@link #isFinishedInfoSent}. Double calls are automatically ignored.
+		'''
+		with self._synclock:
+			if self._deadlinetimer :
+				self._deadlinetimer.cancel()
+				self._deadlinetimer = None
+			if self._isFinishedInfoSent:
+				return;
+			self._isFinishedInfoSent=True
+			finished = Finished(self._state.getAgreements());
+			for conn in self._conns:
+				self._sendFinish(conn, finished)
+			self.notifyListeners(CurrentNegoState(self._state))
+
+	
+	def _sendFinish(self, connection:ProtocolToPartyConn , finished:Inform ):
+		try:
+			connection.send(finished)
+			connection.close()
+		except Exception as e:
+			self._log.log(logging.INFO, "Failed to send Finished to " + str(connection), e)
+
+
Index: /geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py	(revision 71)
+++ /geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py	(revision 71)
@@ -0,0 +1,121 @@
+from typing import List
+from uuid import UUID
+
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.deadline.Deadline import Deadline
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class LearnSettings(SessionSettings):
+	'''
+	Settings for learn protocol.
+	'''
+
+	def __init__(self, participants:List[TeamInfo] , deadline:Deadline) :
+		'''
+		@param participants the list of {@link PartyWithProfile} in clockwise
+		        order. There must be at least 2 to run the SAOP
+		        protocol. But SAOP can be initialized with less, for
+		        use in TournamentSettings.
+		        <table>
+		        <caption>Required parameters</caption>
+		        <tr>
+		        <td>persistentstate</td>
+		        <td>a {@link FileLocation}, serialized as json eg
+		        <code>"b29f3bf5-1dc4-499e-a676-7b2dbb864a03"</code> ,
+		        where the party's persistant state is stored; this
+		        must match the persistantstate used in earlier calls
+		        for e.g. {@link SAOPSettings}.</td>
+		        </tr>
+		
+		        <tr>
+		        <td>negotiationdata</td>
+		        <td>a List of {@link FileLocation}s, serialized as
+		        json, matching the negotiationdata used in earlier
+		        calls for eg {@link SAOPSettings}.</td>
+		        </tr>
+		        </table>
+		@param deadline     the deadline of the negotiation.
+		'''
+		self._participants = participants
+		self._deadline = deadline
+		if participants == None:
+			raise ValueError("participants must not be null")
+		if deadline == None:
+			raise ValueError("deadline must not be null")
+
+		for pwithp in self.getAllParties():
+			params = pwithp.getParty().getParameters()
+			statestr = params.get("persistentstate")
+			if not isinstance(statestr , str):
+				raise ValueError(
+						"persistentstate parameter containing UUID string is required, but found "\
+								+ str(statestr))
+			# check that it contains a UUID. Don't store it, we don't need
+			# it.
+			UUID(statestr)
+
+			# and check the negotiationdata
+			negotiationsstr = params.get("negotiationdata")
+			if not isinstance(negotiationsstr, list):
+				raise ValueError(
+					"negotiationdata parameter containing a List is required")
+
+			for negostr in  negotiationsstr:
+				if not isinstance(negostr,str):
+					raise ValueError(
+						"The list in negotiationdata must contain UUID strings but found "\
+							+ str(negostr))
+				UUID(negostr)
+
+	def getMaxRunTime(self)->float:
+		return self._deadline.getDuration() / 1000.
+
+	def getProtocol(self, logger:Reporter )->SessionProtocol :
+		from geniusweb.protocol.session.learn.LearnState import LearnState
+		from geniusweb.protocol.session.learn.Learn import Learn
+		return Learn(LearnState([],[],None, self), logger)
+
+	def getTeams(self)->List[TeamInfo] :
+		return list(self._participants)
+
+	def getParticipants(self) -> List[TeamInfo] :
+		return list(self._participants)
+
+	def getDeadline(self)->Deadline :
+		'''
+		@return the deadline for this negotiation
+		'''
+		return self._deadline
+
+	def getAllParties(self)->List[PartyWithProfile] :
+		return  [particip.getParties()[0]
+			 for particip in self._participants ]
+
+	def getTeamSize(self)->int:
+		return 1
+
+	def __repr__(self):
+		return "LearnSettings" + str(self._participants) +\
+			"," + str(self._deadline) + "]"
+
+	def __hash__(self):
+		return hash((self._deadline, tuple(self._participants)))
+
+	
+	def __eq__(self, other) -> bool:
+		return isinstance(other, self.__class__) and \
+			self._participants == other._participants and \
+			self._deadline==other._deadline 
+
+	def With(self, team:TeamInfo)->SessionSettings :
+		if team.getSize() != 1:
+			raise ValueError(
+					"Team must be size 1 but found " + str(team))
+		newparts = list(self._participants)
+		newparts.append(team)
+		return LearnSettings(newparts, self._deadline)
Index: /geniuswebcore/geniusweb/protocol/session/learn/LearnState.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/session/learn/LearnState.py	(revision 71)
+++ /geniuswebcore/geniusweb/protocol/session/learn/LearnState.py	(revision 71)
@@ -0,0 +1,99 @@
+from typing import List, Dict, Set, Optional
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.LearningDone import LearningDone
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.progress.Progress import Progress
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.session.DefaultSessionState import DefaultSessionState
+from geniusweb.protocol.session.SessionResult import SessionResult
+from geniusweb.protocol.session.learn.LearnSettings import LearnSettings
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+class LearnState (DefaultSessionState["LearnState", "LearnSettings"]):
+	'''
+	Stores the state for Learn protocol. Immutable.
+	'''
+
+	def __init__(self, actions:List[Action] , conns:List[PartyId] ,
+			progress:Optional[Progress] , settings:LearnSettings ,
+			partyprofiles:Dict[PartyId, PartyWithProfile]={}, 
+			e:Optional[ProtocolException]=None ):
+		'''
+		@param actions       the actions done by the parties
+		@param conns         the existing party connections. we assume ownership
+		            of this so it should not be modified although
+		            connections may of course break. If null, default
+		            empty list is used. Can be less than 2 parties in
+		            the first phases of the setup.
+		@param progress      the {@link Progress} line. can be null
+		@param settings      the {@link LearnSettings}
+		@param partyprofiles map with the {@link PartyWithProfile} for connected
+		            parties. 
+		@param e    the {@link ProtocolException}, or null if none
+		            occurred.
+		'''
+		
+		super().__init__(actions, conns, progress, settings, partyprofiles, e)
+
+# 	def LearnState(LearnSettings settings) {
+# 		'''
+# 		Creates the initial state from the given settings and progress=null
+# 		
+# 		@param settings the {@link LearnSettings}
+# 		'''
+# 		this(Collections.emptyList(), Collections.emptyList(), null, settings,
+# 				null, null);
+# 
+# 	}
+
+	def With(self, actions:List[Action] , conns:List[PartyId],
+			progr:Optional[Progress] ,  settings:LearnSettings, 
+			partyprofiles:Dict[PartyId, PartyWithProfile] , 
+			e:Optional[ProtocolException] ) -> "LearnState" :
+		return LearnState(actions, conns, progr, settings, partyprofiles,
+				e)
+
+	def getAgreements(self)->Agreements :
+		return Agreements()
+
+	def getResult(self)->SessionResult :
+		return SessionResult(self.getPartyProfiles(), self.getAgreements(),
+				{}, None)
+
+	def isFinal(self, currentTimeMs:int) ->bool:
+		done = self._getDoneLearning()
+		# if done is empty we're probably still starting up.
+		return super().isFinal(currentTimeMs)\
+				or (len(done)!=0 and set(self.getConnections()).issubset(done))
+
+	def WithException(self, e:ProtocolException) ->"LearnState" :
+		'''
+		@param e the error that occured
+		@return a new state with the error set.
+		'''
+		return LearnState(self.getActions(), self.getConnections(), self.getProgress(),
+				self.getSettings(), self.getPartyProfiles(), e)
+
+	def checkAction(self, actor:PartyId , action:Action ) ->Optional[str]:
+		'''
+		@param actor  the known real actor that did this action
+		@param action an {@link Action}
+		@return null if action seems ok, or message explaining why not.
+		'''
+		msg = super().checkAction(actor, action)
+		if msg != None:
+			return msg
+		if actor in self._getDoneLearning():
+			return "actor already was done learning"
+		if not isinstance(action, LearningDone):
+			return "Action " + str(action) + " is not allowed"
+		return None
+
+	def _getDoneLearning(self)->Set[PartyId] :
+		'''
+		@return set of parties that reported they are done with learning.
+		'''
+		return set([party.getActor() for party in self.getActions()  ])
+	
Index: /geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py
===================================================================
--- /geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 70)
+++ /geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 71)
@@ -18,6 +18,5 @@
 from geniusweb.references.PartyWithProfile import PartyWithProfile
 
-#CHECK WHY DO THE TYPE VARIABLES HAVE TO BE SWAPPED HERE?
-class SAOPState (DefaultSessionState[ "SAOPSettings","SAOPState"]): 
+class SAOPState (DefaultSessionState[ "SAOPState","SAOPSettings"]):  
 	'''
 	Immutable.
Index: /geniuswebcore/geniusweb/references/Parameters.py
===================================================================
--- /geniuswebcore/geniusweb/references/Parameters.py	(revision 70)
+++ /geniuswebcore/geniusweb/references/Parameters.py	(revision 71)
@@ -7,5 +7,5 @@
 from pyson.JsonGetter import JsonGetter
 from pyson.JsonValue import JsonValue
-from geniusweb.utils import toStr
+from geniusweb.utils import toStr, HASH
 
 
@@ -113,5 +113,5 @@
 	
 	def __hash__(self):
-		return sum( [ hash(k) + 31 * hash(v) for (k,v) in self._params.items()])
+		return HASH(self._params)
 	
 	def __eq__(self, other):
Index: /geniuswebcore/geniusweb/simplerunner/BasicConnection.py
===================================================================
--- /geniuswebcore/geniusweb/simplerunner/BasicConnection.py	(revision 70)
+++ /geniuswebcore/geniusweb/simplerunner/BasicConnection.py	(revision 71)
@@ -103,5 +103,5 @@
 					this.setError(e)
 				this._handler = None
-				print("BasicConnection closed");
+				#print("BasicConnection closed");
 		
 		handlerThread=MyHandlerThread()
Index: /geniuswebcore/geniusweb/utils.py
===================================================================
--- /geniuswebcore/geniusweb/utils.py	(revision 70)
+++ /geniuswebcore/geniusweb/utils.py	(revision 71)
@@ -2,14 +2,23 @@
 A few simple but often needed tools.
 '''
+from _collections_abc import dict_items #type:ignore
 from typing import TypeVar, Optional
 
 
-def toStr(d:dict)->str:
+def toStr(d)->str:
     '''
-    prettyprint dict, using str (not repr) for elements in dict.
-    This tostring function looks similar to how java prints it,
+    prettyprint a "primitive" like dict or list, 
+    similar to how java prints it.
+    In java, strings are NOT quoted.
     '''
-    return "{"+ (", ".join([str(k)+"="+str(v) for (k,v) in d.items()]))+"}"
-
+    if isinstance(d, dict):
+        return "{"+ (", ".join([toStr(k)+"="+toStr(v) for (k,v) in d.items()]))+"}"
+    if isinstance(d, list):
+        return "["+(", ".join([toStr(v) for v in d])) + "]"
+    if isinstance(d, tuple):
+        return "("+(", ".join([toStr(v) for v in d]))+")"
+    if isinstance(d, set):
+        return "{"+(", ".join([toStr(v) for v in d]))+"}"
+    return str(d)
 
 def toTuple(d:dict) -> tuple:
@@ -28,2 +37,13 @@
         raise ValueError("Value is not set")
     return v 
+
+def HASH(obj) -> int:
+    '''
+    hashes built-in objects, even dict and list of built-in objects.
+    '''
+    if isinstance(obj, list) or isinstance(obj, set) or\
+        isinstance(obj, tuple) or isinstance(obj, dict_items):
+        return 31+sum([HASH(e) for e in obj])
+    if isinstance(obj, dict):
+        return HASH(obj.items())
+    return hash(obj)
Index: /geniuswebcore/requirements.txt
===================================================================
--- /geniuswebcore/requirements.txt	(revision 70)
+++ /geniuswebcore/requirements.txt	(revision 71)
@@ -9,2 +9,3 @@
 https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/export/294/unitpy/dist/unitpy-1.0.0.tar.gz
 coverage
+websocket-server=0.6.0
Index: /geniuswebcore/setup.py
===================================================================
--- /geniuswebcore/setup.py	(revision 70)
+++ /geniuswebcore/setup.py	(revision 71)
@@ -3,5 +3,5 @@
 setup(
     name='geniusweb',
-    version='1.1.1',    
+    version='1.1.2',    
     description='GeniusWeb glue code to connect python3 parties',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
Index: /geniuswebcore/test/geniusweb/events/ActionEventTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/events/ActionEventTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/events/ActionEventTest.py	(revision 71)
@@ -0,0 +1,52 @@
+import unittest
+
+from pyson.ObjectMapper import ObjectMapper
+from geniusweb.events.ActionEvent import ActionEvent 
+from unitpy.GeneralTests import GeneralTests
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.actions.EndNegotiation import EndNegotiation
+from typing import List
+import json
+from geniusweb.events.NegotiationEvent import NegotiationEvent
+from unittest.mock import Mock
+
+class ActionEventTest(unittest.TestCase, GeneralTests[ActionEvent]):
+    NOW = 101
+    pyson = ObjectMapper()
+    string = "{\"ActionEvent\":{\"action\":{\"EndNegotiation\":{\"actor\":\"party1\"}},\"time\":101}}"
+    pid = PartyId("party1");
+    action = EndNegotiation(pid);
+    evt = ActionEvent(action, NOW);
+    evt1 = ActionEvent(action, NOW);
+    evtb = ActionEvent(action, NOW + 1);
+    
+    def getGeneralTestData(self) -> List[List[ActionEvent]]:
+        return [[self.evt, self.evt1], [self.evtb]]
+                
+    def getGeneralTestStrings(self):
+        return "ActionEvent\\[.*" + str(self.NOW) + ".*EndNegotiation.*\\]",\
+               "ActionEvent\\[.*" + str(self.NOW + 1) + ".*EndNegotiation.*\\]"
+   
+    def testSmoke(self):
+        action1 = Mock(EndNegotiation)
+        evt = ActionEvent(action1, 0)
+    
+    def testserialize(self):
+        json1 = self.pyson.toJson(self.evt)
+        print(json1)
+        self.assertEqual(json.loads(self.string), json1)
+
+    def testdeserializeTestReadActionEvent(self):
+        evt1 = self.pyson.parse(json.loads(self.string), ActionEvent)
+        print(evt1)
+        # compare fields, as evt is a derived/new inner class
+        self.assertEqual(EndNegotiation, type(evt1.getAction()))
+        self.assertEqual(101, evt1.getTime())
+    
+    def testdeserializeTestReadEvent(self): 
+        self.assertEqual(self.evt1, self.pyson.parse(json.loads(self.string), ActionEvent))
+        evt1 = self.pyson.parse(json.loads(self.string), NegotiationEvent)
+        self.assertEqual(ActionEvent, type(evt1))
+        ev1 = evt1
+        self.assertEqual(EndNegotiation, type(ev1.getAction()))
+        self.assertEqual(101, ev1.getTime())
Index: /geniuswebcore/test/geniusweb/events/SessionStartedTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/events/SessionStartedTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/events/SessionStartedTest.py	(revision 71)
@@ -0,0 +1,57 @@
+import unittest
+from pyson.ObjectMapper import ObjectMapper
+from unitpy.GeneralTests import GeneralTests
+from geniusweb.events.SessionStarted import SessionStarted
+from geniusweb.actions.PartyId import PartyId
+from typing import List
+from geniusweb.events.NegotiationEvent import NegotiationEvent
+import json
+
+class SessionStartedTest(unittest.TestCase, GeneralTests[SessionStarted]):
+
+    pyson = ObjectMapper()
+
+    SESSIONNR = 1
+    NOW = 101
+    NOW1 = NOW + 1
+
+    PARTY1 = PartyId("party1")
+    PARTY2 = PartyId("party2")
+
+    parties = [PARTY1, PARTY2]
+
+    sessionStartedLater = SessionStarted(SESSIONNR, parties, NOW1)
+    othersessionStarted = SessionStarted(SESSIONNR + 1, parties, NOW)
+
+    sessionStarted = SessionStarted(SESSIONNR, parties, NOW)
+    sessionStarted1 = SessionStarted(SESSIONNR, parties, NOW)
+    # jackson expected format.
+    sessionstartedstring = "{\"SessionStarted\":{\"sessionNumber\":1,\"parties\":[\"party1\",\"party2\"],\"time\":101}}"
+    # "{\"sessionstarted\":{\"time\":101,\"sessionNumber\":1,\"parties\":[\"party1\",\"party2\"]}}";
+    expectedstring = "SessionStarted.*101.*"
+
+    def getGeneralTestData(self) -> List[List[SessionStarted]]:
+        mylist = []
+        mylist.append([self.sessionStarted, self.sessionStarted1])
+        mylist.append([self.sessionStartedLater])
+        mylist.append([self.othersessionStarted])
+        return mylist
+
+    def getGeneralTestStrings(self) -> List[str]:
+        return ["SessionStarted.*" + str(self.SESSIONNR) + ".*" + str(self.NOW) + ".*",\
+                "SessionStarted.*" + str(self.SESSIONNR) + ".*" + str(self.NOW1) + ".*",\
+                "SessionStarted.*" + str(self.SESSIONNR + 1) + ".*" + str(self.NOW) + ".*"]
+
+    def testSerialize(self):
+        print(self.sessionstartedstring)
+        self.assertEqual(json.loads(self.sessionstartedstring), self.pyson.toJson(self.sessionStarted))
+
+    def testDeserialize(self):
+        evt = self.pyson.parse(json.loads(self.sessionstartedstring), NegotiationEvent)        
+        self.assertEqual(SessionStarted, type(evt))
+        event = evt
+        # compare fields, as sessionended is a derived/new inner class
+        self.assertEqual(self.NOW, event.getTime())
+        self.assertEqual(self.SESSIONNR, event.getSessionNumber())
+        self.assertEqual(2, len(event.getParties()))
+
Index: /geniuswebcore/test/geniusweb/events/TournamentStartedTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/events/TournamentStartedTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/events/TournamentStartedTest.py	(revision 71)
@@ -0,0 +1,52 @@
+from geniusweb.events.TournamentStarted import TournamentStarted
+import unittest
+from unitpy.GeneralTests import GeneralTests
+from pyson.ObjectMapper import ObjectMapper
+from typing import List
+from geniusweb.events.NegotiationEvent import NegotiationEvent
+import json
+
+class TournamentStartedTest(unittest.TestCase, GeneralTests[TournamentStarted]):
+    pyson = ObjectMapper()
+    
+    SESSIONNR = 1
+    NOW = 101
+    NOW1 = NOW + 1
+    NSESSIONS = 5
+
+    tournamentStarted = TournamentStarted(NSESSIONS, NOW)
+    tournamentStarteda = TournamentStarted(NSESSIONS, NOW)
+    otherTournamentStarted = TournamentStarted(NSESSIONS + 1, NOW)
+    tournamentStartedLater = TournamentStarted(NSESSIONS, NOW1)
+    
+    tournamentstartedstring = '{"TournamentStarted":{"time":101,"numberOfSessions":5}}'
+    
+    def getGeneralTestData(self) -> List[List[TournamentStarted]]:
+        mylist = []
+        mylist.append([self.tournamentStarted, self.tournamentStarteda])
+        mylist.append([self.otherTournamentStarted])
+        mylist.append([self.tournamentStartedLater])
+        return mylist
+
+    def getGeneralTestStrings(self) -> List[str]:
+        return ["TournamentStarted\\[.*" + str(self.NOW) + ".*" + str(self.NSESSIONS) + ".*\\]",\
+                "TournamentStarted\\[.*" + str(self.NOW) + ".*" + str(self.NSESSIONS + 1) + ".*\\]",\
+                "TournamentStarted\\[.*" + str(self.NOW1) + ".*" + str(self.NSESSIONS) + ".*\\]"]
+
+    def testserializeTournamentStarted(self):
+        print(self.tournamentstartedstring)
+        self.assertEqual(json.loads(self.tournamentstartedstring), self.pyson.toJson(self.tournamentStarted))
+ 
+    def testdeserializeTournamentStarted(self):
+        evt = self.pyson.parse(json.loads(self.tournamentstartedstring), NegotiationEvent)
+        self.assertEqual(TournamentStarted, type(evt))
+        event = evt
+        # compare fields, as sessionended is a derived/new inner class
+        self.assertEqual(self.NSESSIONS, event.getNumberOfSessions())
+        self.assertEqual(self.NOW, event.getTime())
+
+    def testNullTime(self):
+        TournamentStarted(self.NSESSIONS, 0)
+
+    def testNegativeTime(self):
+        self.assertRaises(ValueError, lambda: TournamentStarted(self.NSESSIONS, -1))
Index: /geniuswebcore/test/geniusweb/inform/ActionDoneTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/inform/ActionDoneTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/inform/ActionDoneTest.py	(revision 71)
@@ -10,39 +10,42 @@
 from geniusweb.inform.Voting import Voting
 from unitpy.GeneralTests import GeneralTests
+from geniusweb.profileconnection import ProfileConnectionFactory
+from typing import List
+import json
 
 
-class ActionDoneTest(unittest.TestCase):
+class ActionDoneTest(unittest.TestCase, GeneralTests[ActionDone]):
 
-    pyson=ObjectMapper()
-    pid1=PartyId("party1")
-    pid2=PartyId("party2")
+    pyson = ObjectMapper()
+    id1 = PartyId("party1")
+    id2 = PartyId("party2")
     
-    endnego=EndNegotiation(pid1)
-    endnego1=EndNegotiation(pid1)
-    endnego2=EndNegotiation(pid2)
-    
-    actiondone=ActionDone(endnego)
-    actiondone1=ActionDone(endnego1)
-    actiondone2=ActionDone(endnego2)
+    act1 = EndNegotiation(id1)
+    act2 = EndNegotiation(id2)
+     
+    done1 = ActionDone(act1)
+    done1a = ActionDone(act1)
+    done2 = ActionDone(act2)
     
     actiondonejson = {"ActionDone":{"action":{"EndNegotiation":{"actor":"party1"}}}}
     
-    def testSerializeDeserialize(self):
-        
-        print(str(self.pyson.toJson(self.actiondone)))
-        self.assertEqual(self.actiondonejson, self.pyson.toJson(self.actiondone))
+    def getGeneralTestData(self) -> List[List[ActionDone]]:
+        return [[self.done1, self.done1a], [self.done2]]
+    
+    def getGeneralTestStrings(self) -> List[str]:
+        return ["ActionDone.*EndNegotiation.*party1.*",
+                "ActionDone.*EndNegotiation.*party2.*"]
+
+    def testSerializeAccept(self):
+        json1 = self.pyson.toJson(self.done1)
+        print(json1)
+        self.assertEqual(self.actiondonejson, json1)
         
     def testDeserialize(self):
-        self.assertEqual(self.actiondone, self.pyson.parse(self.actiondonejson, Inform))
+        self.assertEqual(self.done1, self.pyson.parse(self.actiondonejson, Inform))
         
-    
-    def testRepr(self):
-        self.assertEqual("ActionDone[EndNegotiation[party1]]", repr(self.actiondone))
-        
-    def testEqual(self):
-        self.assertEqual(self.actiondone, self.actiondone1)
-        self.assertNotEqual(self.actiondone, self.actiondone2)
-        self.assertEqual(hash(self.actiondone), hash(self.actiondone1))
-        self.assertNotEqual(hash(self.actiondone), hash(self.actiondone2))
+    def testGet(self):
+        self.assertEqual(self.act1, self.done1.getAction())
         
         
+        
Index: /geniuswebcore/test/geniusweb/inform/OptInTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/inform/OptInTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/inform/OptInTest.py	(revision 71)
@@ -13,7 +13,9 @@
 from geniusweb.issuevalue.Bid import Bid
 from geniusweb.issuevalue.DiscreteValue import DiscreteValue
+from unitpy.GeneralTests import GeneralTests
+from typing import List
 
 
-class OptInTest(unittest.TestCase):
+class OptInTest(unittest.TestCase, GeneralTests[OptIn]):
     maxDiff = None
         
@@ -36,10 +38,10 @@
     asJson="{\"OptIn\":{\"votes\":[{\"Votes\":{\"actor\":\"partyA\",\"votes\":[{\"Vote\":{\"actor\":\"partyA\",\"bid\":{\"issuevalues\":{\"iss\":\"val1\"}},\"minPower\":2,\"maxPower\":9}}]}},{\"Votes\":{\"actor\":\"partyB\",\"votes\":[{\"Vote\":{\"actor\":\"partyB\",\"bid\":{\"issuevalues\":{\"iss\":\"val2\"}},\"minPower\":2,\"maxPower\":9}},{\"Vote\":{\"actor\":\"partyB\",\"bid\":{\"issuevalues\":{\"iss\":\"val1\"}},\"minPower\":2,\"maxPower\":9}}]}}]}}"
 
-    def testGeneralTestData(self) :
-        self.assertEqual(self.optIn1, self.optIn1a)
-        self.assertNotEqual(self.optIn1, self.optIn2)
-        self.assertEqual(hash(self.optIn1), hash(self.optIn1a))
-        self.assertNotEqual(hash(self.optIn1), hash(self.optIn2))
-
+    def getGeneralTestData(self) -> List[List[OptIn]] :
+        return [[self.optIn1, self.optIn1a], [self.optIn2]]
+        
+    def getGeneralTestStrings(self) -> List[str]:
+        return ["OptIn.*Votes.*Vote.*partyA.*iss.*val1.*2.*Votes.*Vote.*partyB.*iss.*val2.*.*2.*Vote.*partyB.*iss.*val1.*2.*", 
+                "OptIn.*Votes.*Vote.*partyA.*iss.*val.*2.*"]
 
     def testSerialize(self):
@@ -53,3 +55,6 @@
         print(p);
         self.assertEqual(self.optIn1, p)
+        
+    def testOneVotePerParty(self):
+        self.assertRaises(ValueError, lambda: OptIn([self.votesA, self.votesA]))
 
Index: /geniuswebcore/test/geniusweb/inform/SettingsTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/inform/SettingsTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/inform/SettingsTest.py	(revision 71)
@@ -17,5 +17,5 @@
 class SettingsTest(unittest.TestCase, GeneralTests[Settings]):
 
-    id = PartyId("party1")
+    id1 = PartyId("party1")
     id2 = PartyId("party2")
 
@@ -43,10 +43,10 @@
     settings2 = Parameters().With("a", 1)
 
-    negoinfo1 = Settings(id, profile1, protocol1, progress1, settings1)
-    negoinfo1a = Settings(id, profile1, protocol1a, progress1a,settings1)
-    negoinfo2 = Settings(id, profile1, protocol2, progress1, settings1)
-    negoinfo3 = Settings(id, profile1, protocol1, progress2, settings1)
+    negoinfo1 = Settings(id1, profile1, protocol1, progress1, settings1)
+    negoinfo1a = Settings(id1, profile1, protocol1a, progress1a,settings1)
+    negoinfo2 = Settings(id1, profile1, protocol2, progress1, settings1)
+    negoinfo3 = Settings(id1, profile1, protocol1, progress2, settings1)
     negoinfo4 = Settings(id2, profile1, protocol1, progress2, settings1)
-    negoinfo5 = Settings(id, profile1, protocol1, progress1, settings2)
+    negoinfo5 = Settings(id1, profile1, protocol1, progress1, settings2)
 
     def getGeneralTestData(self)-> List[List[Settings]] :
Index: /geniuswebcore/test/geniusweb/inform/YourTurnTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/inform/YourTurnTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/inform/YourTurnTest.py	(revision 71)
@@ -10,5 +10,5 @@
 
     pyson=ObjectMapper()
-    yourturn  = YourTurn()
+    yourturn = YourTurn()
     yourturnjson:dict= {'YourTurn': {}}
     
@@ -21,3 +21,5 @@
     def testDeserialize(self):
         self.assertEqual(self.yourturn, self.pyson.parse(self.yourturnjson, Inform))
-        
+    
+    def testrepr(self):
+        self.assertEqual(repr(self.yourturn),"YourTurn")
Index: /geniuswebcore/test/geniusweb/issuevalue/BidTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/issuevalue/BidTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/issuevalue/BidTest.py	(revision 71)
@@ -65,4 +65,7 @@
         issuevalues = None;
         self.assertRaises(ValueError, lambda: Bid(issuevalues))
+        
+    def testUnkownIssue(self):
+        self.assertEqual(None, self.bid.getValue("NotAnIssue")) 
 
     def testBidSimple(self):
Index: /geniuswebcore/test/geniusweb/issuevalue/DiscreteValueTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/issuevalue/DiscreteValueTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/issuevalue/DiscreteValueTest.py	(revision 71)
@@ -12,7 +12,8 @@
 
 class DiscreteValueTest(unittest.TestCase, GeneralTests[DiscreteValue]):
-    value =  DiscreteValue("a");
-    value1 =  DiscreteValue("a");
-    valueb =  DiscreteValue("b");
+    value =  DiscreteValue("a")
+    value1 =  DiscreteValue("a")
+    valueb =  DiscreteValue("b")
+
     serialized = "\"a\"";
     jackson =  ObjectMapper();
@@ -21,4 +22,7 @@
         print(str(self.jackson.toJson(self.value)))
         self.assertEqual(json.loads(self.serialized), self.jackson.toJson(self.value))
+    
+    def testEmpty(self):
+        self.assertRaises(ValueError, lambda:DiscreteValue(""))
         
     def testDeserialize(self) :
@@ -28,5 +32,4 @@
         self.assertEqual(self.value, self.jackson.parse(json.loads(self.serialized), Value))
         
-
     def getGeneralTestData(self)->List[List[DiscreteValue]] :
         return [ [self.value, self.value1], [self.valueb]]
Index: /geniuswebcore/test/geniusweb/party/CapabilitiesTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/party/CapabilitiesTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/party/CapabilitiesTest.py	(revision 71)
@@ -4,30 +4,36 @@
 
 from geniusweb.party.Capabilities import Capabilities       
+from unitpy.GeneralTests import GeneralTests
+from typing import List
 
 
-class PartyTest(unittest.TestCase):
+class CapabilitiesTest(unittest.TestCase, GeneralTests[Capabilities]):
     pyson = ObjectMapper()
-    cap= Capabilities( set([ "SAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
-    cap1= Capabilities( set([ "SAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
-    cap2= Capabilities( set([ "SHAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
-    cap3= Capabilities( set([ "SAOP"]), set(['geniusweb.profile.Profile']))
+    
     capjson= {"behaviours":["SAOP"], "profiles":["geniusweb.profile.utilityspace.LinearAdditive"]}
     
+    capabilities1 = Capabilities( set([ "SAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
+    capabilities1a= Capabilities( set([ "SAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
+    capabilities2 = Capabilities( set([ "SEB"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
+    capabilities3 = Capabilities( set([ "SEB", "SAOP"]), set(['geniusweb.profile.utilityspace.LinearAdditive']))
+    capabilities4 = Capabilities( set([ "SAOP"]), set(['geniusweb.profile.PartialOrdering']))
+    
+    def getGeneralTestData(self) -> List[List[Capabilities]]:
+        return [[self.capabilities1, self.capabilities1a], [self.capabilities2], [self.capabilities3], [self.capabilities4]]
+   
+    def getGeneralTestStrings(self) -> List[str]:
+        return ["Capabilities.*geniusweb.profile.utilityspace.LinearAdditive.*SAOP.*",
+                "Capabilities.*geniusweb.profile.utilityspace.LinearAdditive.*SEB.*",
+                "Capabilities.*geniusweb.profile.utilityspace.LinearAdditive.*S.*,.*S.*",
+                "Capabilities.*geniusweb.profile.PartialOrdering.*SAOP.*"]
+        
     def testSerialize(self):
-        print(str(self.pyson.toJson(self.cap)))
-        self.assertEqual(self.capjson, self.pyson.toJson(self.cap))
+        print(str(self.pyson.toJson(self.capabilities1)))
+        self.assertEqual(self.capjson, self.pyson.toJson(self.capabilities1))
         
     def testDeserialize(self):
-        self.assertEqual(self.cap, self.pyson.parse(self.capjson, Capabilities))
+        self.assertEqual(self.capabilities1, self.pyson.parse(self.capjson, Capabilities))
         
-    def testRepr(self):
-        self.assertEqual("Capabilities[{'geniusweb.profile.utilityspace.LinearAdditive'},{'SAOP'}]", repr(self.cap))
+    def testbehaviours(self):
+        self.assertEqual(set(["SEB", "SAOP"]), self.capabilities3.getBehaviours())
         
-    def testEqual(self):
-        self.assertEqual(self.cap, self.cap1)
-        self.assertNotEqual(self.cap, self.cap2)
-        self.assertNotEqual(self.cap, self.cap3)
-        self.assertEqual(hash(self.cap), hash(self.cap1))
-        self.assertNotEqual(hash(self.cap), hash(self.cap2))
-        self.assertNotEqual(hash(self.cap), hash(self.cap3))
-        
Index: /geniuswebcore/test/geniusweb/profileconnection/ProfileServerStub.py
===================================================================
--- /geniuswebcore/test/geniusweb/profileconnection/ProfileServerStub.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/profileconnection/ProfileServerStub.py	(revision 71)
@@ -0,0 +1,33 @@
+import logging
+from threading import Thread
+from time import sleep
+
+from websocket_server import WebsocketServer  # type:ignore
+
+
+def new_client(client, server):
+    print("client connected!")
+    with open('test/resources/jobs/jobs1.json') as f:
+        text = f.read()
+    server.send_message_to_all(text)
+
+class ProfileServerStub(Thread):
+    '''
+    Run a fake profileserver for a simple test
+    '''
+    def run(self):
+        self.server = WebsocketServer(host='127.0.0.1',port=8080,  
+                loglevel=logging.INFO, key="key.pem", cert="cert.pem")
+        self.server.set_fn_new_client(new_client)
+        self.server.run_forever()
+
+    def stop(self):
+        self.server.shutdown()
+
+if __name__ == '__main__':   
+    server=ProfileServerStub()
+    server.start()
+    sleep(2)
+    print("bla")
+    server.stop()
+    print("stopped")
Index: /geniuswebcore/test/geniusweb/profileconnection/WebSocketProfileConnectorTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/profileconnection/WebSocketProfileConnectorTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/profileconnection/WebSocketProfileConnectorTest.py	(revision 71)
@@ -15,4 +15,5 @@
     DefaultWebSocketContainer
 from geniusweb.profileconnection.WebsocketProfileConnector import WebsocketProfileConnector
+from test.geniusweb.profileconnection.ProfileServerStub import ProfileServerStub
 
 
@@ -68,9 +69,13 @@
         TO RUN THIS, ENSURE YOU HAVE A RUNNING PROFILESSERVER ON LOCALHOST
         '''
-        profint= WebsocketProfileConnector(URI("ws://localhost:8080/profilesserver/websocket/get/party/party1"), self.reporter, DefaultWebSocketContainer() )
-        sleep(1)
-        profile=profint.getProfile()
-        self.assertTrue(profile)
-        profint.close()
+        server=ProfileServerStub()
+        server.start()
+        try:
+            profint= WebsocketProfileConnector(URI("ws://localhost:8080/profilesserver/websocket/get/party/party1"), self.reporter, DefaultWebSocketContainer() )
+            sleep(1)
+            profile=profint.getProfile()
+            self.assertTrue(profile)
+            profint.close()
+        finally:
+            server.stop()
         
-        
Index: /geniuswebcore/test/geniusweb/protocol/session/learn/LearnSettingsTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/protocol/session/learn/LearnSettingsTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/protocol/session/learn/LearnSettingsTest.py	(revision 71)
@@ -0,0 +1,154 @@
+import json
+from typing import List
+import unittest
+from unittest.mock import Mock
+
+from pyson.ObjectMapper import ObjectMapper
+from tudelft_utilities_logging.Reporter import Reporter
+from unitpy.GeneralTests import GeneralTests
+from uri.uri import URI
+
+from geniusweb.deadline.DeadlineRounds import DeadlineRounds
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.protocol.session.learn.LearnSettings import LearnSettings
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProfileRef import ProfileRef
+
+
+class LearnSettingsTest (unittest.TestCase, GeneralTests[LearnSettings]):
+	jackson = ObjectMapper()
+	
+	serialized = "{\"LearnSettings\":{\"participants\":[{\"TeamInfo\":{\"parties\":[{\"party\":{\"partyref\":\"party1\",\"parameters\":{\"persistentstate\":\"6bb5f909-0079-43ac-a8ac-a31794391074\",\"negotiationdata\":[\"12b5f909-0079-43ac-a8ac-a31794391012\"]}},\"profile\":\"http://prof1\"}]}}],\"deadline\":{\"DeadlineRounds\":{\"rounds\":10,\"durationms\":10000}}}}"
+
+	def setUp(self):
+		self.params = Parameters()
+		self.params = self.params.With("persistentstate",
+				"6bb5f909-0079-43ac-a8ac-a31794391074")
+		self.params = self.params.With("negotiationdata",
+				["12b5f909-0079-43ac-a8ac-a31794391012"])
+
+		# define participants
+		self.participants = self.createTeam(self.params, 1)
+
+		# participants2
+# 		ProfileRef profile2 = new ProfileRef("http://prof2");
+# 		PartyWithProfile partywithp2 = new PartyWithProfile(party1, profile2);
+# 		List<PartyWithProfile> team2pp = Arrays.asList(partywithp2);
+# 		TeamInfo team2 = new TeamInfo(team2pp);
+		self.participants2 = self.createTeam(self.params, 2)
+
+		self.deadline = DeadlineRounds(10, 10000)
+		self.deadline2 = DeadlineRounds(20, 10000)
+
+		self.settings1 =  LearnSettings(self.participants, self.deadline)
+		self.settings1a =  LearnSettings(self.participants, self.deadline)
+		self.settings2 =  LearnSettings(self.participants2, self.deadline)
+		self.settings3 =  LearnSettings(self.participants, self.deadline2)
+
+
+	def createTeam(self, params:Parameters,  partynr:int) -> List[TeamInfo] :
+		'''
+		@param params  the parameters for the party
+		@param partynr the serialnr for the party, eg 1 or 2.
+		@return a List of TeamInfo with just 1 team.
+		'''
+		party1ref = PartyRef(URI("party" + str(partynr)))
+		party1 = PartyWithParameters(party1ref, params)
+		profile1 = ProfileRef(URI("http://prof" + str(partynr)))
+		partywithp1 = PartyWithProfile(party1, profile1)
+		team1pp = [partywithp1]
+		team1 = TeamInfo(team1pp)
+		return [team1]
+
+	def getGeneralTestData(self) ->List[List[LearnSettings]]:
+		return [[self.settings1, self.settings1a],[self.settings2], [self.settings3]]
+
+	def getGeneralTestStrings(self)->List[str]:
+		return [
+				"LearnSettings.TeamInfo.*PartyWithProfile.*PartyRef.*party1.*persistentstate=6b.*, negotiationdata=\\[12.*..*ProfileRef.*prof1.*,DeadlineRounds.*10,10000.*",
+				"LearnSettings.TeamInfo.*PartyWithProfile.*PartyRef.*party2.*persistentstate=6b.*, negotiationdata=\\[12.*..*ProfileRef.*prof2.*,DeadlineRounds.*10,10000.*",
+				"LearnSettings.TeamInfo.*PartyWithProfile.*PartyRef.*party1.*persistentstate=6b.*, negotiationdata=\\[12.*..*ProfileRef.*prof1.*,DeadlineRounds.*20,10000.*"]
+
+	def testNullParticipants(self):
+		self.assertRaises(ValueError, lambda:LearnSettings(None, self.deadline))
+
+	def testNullDeadline(self):
+		self.assertRaises(ValueError, lambda:LearnSettings(self.participants, None))
+
+	def testgetMaxRuntimeTest(self):
+		self.assertEqual(10, self.settings1.getMaxRunTime(), 0.00000001)
+
+	def testgetProtocolTest(self):
+		# this will throw if not Learn protocol or another problem.
+		protocol = self.settings1.getProtocol(Mock(Reporter))
+
+
+	def testbadPersistentTest(self):
+		# hack. #1933.
+		newparams = self.params.With("persistentstate",
+				"\"notproperUUID\"")
+		participants = self.createTeam(newparams, 1)
+
+		self.assertRaises(ValueError, lambda:LearnSettings(participants, self.deadline))
+
+	def testpersistentNotStringTest(self):
+		newparams = self.params.With("persistentstate", 32)
+		participants = self.createTeam(newparams, 1)
+
+		self.assertRaises(ValueError, lambda: LearnSettings(participants, self.deadline))
+
+	def testpersistentWithoutQuotesTest(self) :
+		newparams = self.params.With("persistentstate",
+				"6bb5f909-0079-43ac-a8ac-a31794391074")
+		participants = self.createTeam(newparams, 1)
+
+		LearnSettings(participants, self.deadline)
+
+	def testpersistentExtraQuoteTest(self) :
+		newparams = self.params.With("persistentstate",
+				"\"6bb5f909-0079-43ac-a8ac-a31794391074\"")
+		participants = self.createTeam(newparams, 1)
+
+		self.assertRaises(ValueError, lambda:LearnSettings(participants, self.deadline))
+
+	def testnegotiationDataNotList(self):
+		newparams = self.params.With("negotiationdata",
+				"\"12b5f909-0079-43ac-a8ac-a31794391012\"")
+		participants = self.createTeam(newparams, 1)
+		self.assertRaises(ValueError, lambda:LearnSettings(participants, self.deadline))
+
+	def testnegotiationDataBadContents(self):
+		newparams = self.params.With("negotiationdata", "\"bad\"")
+		participants = self.createTeam(newparams, 1)
+		self.assertRaises(ValueError, lambda: LearnSettings(participants, self.deadline))
+
+	def testnegotiationDataNotString(self):
+		newparams = self.params.With("negotiationdata", 12)
+		participants = self.createTeam(newparams, 1)
+		self.assertRaises(ValueError, lambda:LearnSettings(participants,self.deadline))
+
+	def testgetTeamsTest(self):
+		self.assertEqual(self.participants, self.settings1.getTeams())
+		self.assertEqual(1, self.settings1.getTeamSize())
+
+	def testgetDeadlineTest(self):
+		self.assertEqual(self.deadline, self.settings1.getDeadline())
+
+	def testwithTeamTest(self):
+		newset = self.settings1.With(self.participants2[0])
+		self.assertEqual(2, len(newset.getTeams()))
+
+	def testDeserialize(self) :
+		obj = self.jackson.parse(json.loads(self.serialized), SessionSettings)
+		print(obj)
+		self.assertEqual(self.settings1, obj)
+
+	def testSerialize(self):
+		jsonobj = self.jackson.toJson(self.settings1);
+		print(jsonobj)
+		self.assertEqual(json.loads(self.serialized), jsonobj)
+
Index: /geniuswebcore/test/geniusweb/protocol/session/learn/LearnStateTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/protocol/session/learn/LearnStateTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/protocol/session/learn/LearnStateTest.py	(revision 71)
@@ -0,0 +1,124 @@
+import unittest
+from unittest.mock import Mock
+
+from uri.uri import URI
+
+from geniusweb.actions.EndNegotiation import EndNegotiation
+from geniusweb.actions.LearningDone import LearningDone
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.deadline.DeadlineTime import DeadlineTime
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.progress.Progress import Progress
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.protocol.session.learn.LearnSettings import LearnSettings
+from geniusweb.protocol.session.learn.LearnState import LearnState
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProfileRef import ProfileRef
+
+
+class LearnStateTest(unittest.TestCase):
+
+	PARTY2 = "party2"
+	PARTY1 = "party1"
+	party1id =  PartyId(PARTY1)
+	party2id =  PartyId(PARTY2)
+	deadline =  DeadlineTime(10000)
+	partyprofile1 = Mock(PartyWithProfile)
+	partyprofile2 = Mock(PartyWithProfile)
+	progressnotdone = Mock(Progress)
+	progressdone = Mock(Progress);
+
+	def setUp(self):
+		self.params = Parameters()
+		self.params = self.params.With("persistentstate",
+				"6bb5f909-0079-43ac-a8ac-a31794391074")
+		self.params = self.params.With("negotiationdata",
+				["12b5f909-0079-43ac-a8ac-a31794391012"])
+
+		
+		self.progressnotdone.isPastDeadline=Mock(return_value=False)
+		self.progressdone.isPastDeadline=Mock(return_value=True)
+
+		# define team1
+		party1ref = PartyRef(URI(self.PARTY1))
+		party1 = PartyWithParameters(party1ref, self.params)
+		profile1 = ProfileRef(URI("http://prof1"))
+		partywithp1 = PartyWithProfile(party1, profile1)
+		team1pp = [partywithp1]
+		self.team1 = TeamInfo(team1pp)
+
+		# define team2
+		party2ref = PartyRef(URI(self.PARTY2))
+		party2 = PartyWithParameters(party2ref, self.params)
+		profile2 = ProfileRef(URI("http://prof2"))
+		partywithp2 = PartyWithProfile(party2, profile2)
+		team2pp = [partywithp2]
+		self.team2 = TeamInfo(team2pp)
+
+		participants = [self.team1, self.team2]
+		settings = LearnSettings(participants, self.deadline)
+		self.state =LearnState([],[],None, settings)
+
+		self.connectedstate = self.state.WithParty(self.party1id, self.partyprofile1)\
+				.WithParty(self.party2id, self.partyprofile2).WithProgress(self.progressnotdone)
+
+
+	def testsmokeTestNull(self):
+		self.assertRaises(ValueError, lambda:LearnState([],[],None, None))
+
+	def testsmokeTest(self):
+		settings = Mock(LearnSettings)
+		LearnState([],[],None,settings)
+
+	def testgetAgreementsTest(self):
+		self.assertEqual(Agreements(), self.state.getAgreements())
+
+	def testisFinalTest(self):
+		# initial state is false, because we check
+		# that there are no learningDone reports
+		self.assertFalse(self.state.isFinal(0))
+		# not final because there are connected parties not yet done.
+		self.assertFalse(self.connectedstate.isFinal(0))
+		finalstate = self.connectedstate.WithProgress(self.progressdone)
+		self.assertTrue(finalstate.isFinal(0))
+
+		errorstate = self.connectedstate\
+				.WithException(ProtocolException("test", self.party1id))
+		self.assertTrue(errorstate.isFinal(0))
+
+	def testcheckActionTest(self):
+		# check basic illegal actions in Learning
+		self.assertNotEqual(None, self.connectedstate.checkAction(self.party1id, None))
+		self.assertNotEqual(None, self.connectedstate.checkAction(PartyId("unknown"), None))
+		self.assertNotEqual(None, self.connectedstate.checkAction(self.party1id,
+				EndNegotiation(self.party1id)))
+		self.assertNotEqual(None,self.connectedstate.checkAction(self.party1id,
+				LearningDone(self.party2id)))
+
+		# check a good action is allowed
+		self.assertEqual(None,self.connectedstate.checkAction(self.party1id,
+				LearningDone(self.party1id)))
+
+		# no learning twice not ok
+		learned = self.connectedstate.WithAction(self.party1id,
+				LearningDone(self.party1id))
+		self.assertNotEqual(None, 
+				learned.checkAction(self.party1id, LearningDone(self.party1id)))
+
+	def testLearnDoneActionTest(self):
+		learned = self.connectedstate.WithAction(self.party1id,
+				LearningDone(self.party1id))
+
+	def testisFinalWhenAllLearned(self):
+		learned = self.connectedstate\
+			.WithAction(self.party1id, LearningDone(self.party1id))\
+			.WithAction(self.party2id, LearningDone(self.party2id))
+		self.assertTrue(learned.isFinal(0))
+
+	def testgetResultTest(self):
+		res = self.connectedstate.getResult()
+		self.assertEqual(0, len(res.getAgreements().getMap()))
Index: /geniuswebcore/test/geniusweb/protocol/session/learn/LearnTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/protocol/session/learn/LearnTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/protocol/session/learn/LearnTest.py	(revision 71)
@@ -0,0 +1,137 @@
+from time import sleep, time
+from typing import cast
+import unittest
+from unittest.mock import Mock
+
+from tudelft_utilities_logging.Reporter import Reporter
+from uri.uri import URI
+
+from geniusweb.actions.EndNegotiation import EndNegotiation
+from geniusweb.actions.LearningDone import LearningDone
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.deadline.DeadlineTime import DeadlineTime
+from geniusweb.inform.Finished import Finished
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.protocol.session.learn.Learn import Learn
+from geniusweb.protocol.session.learn.LearnSettings import LearnSettings
+from geniusweb.protocol.session.learn.LearnState import LearnState
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProfileRef import ProfileRef
+
+
+class LearnTest(unittest.TestCase):
+
+	reporter = Mock(Reporter)
+	params = Parameters()
+	deadline = DeadlineTime(1800)
+	party1id = PartyId("party1")
+	party2id = PartyId("party2")
+
+	def setUp(self):
+		self.conn1 = Mock(ProtocolToPartyConn)
+		self.conn2 = Mock(ProtocolToPartyConn)
+		self.params = self.params.With("persistentstate",
+				"6bb5f909-0079-43ac-a8ac-a31794391074")
+		self.params = self.params.With("negotiationdata",
+				["12b5f909-0079-43ac-a8ac-a31794391012"])
+
+	def testsmokeTest(self):
+		Learn(Mock(LearnState), Mock(Reporter))
+
+	def testgetDescrTest(self):
+		l = Learn(Mock(LearnState), Mock(Reporter))
+
+		self.assertNotEqual(None, l.getDescription())
+
+	def testgetStateTest(self) :
+		state = Mock(LearnState)
+		l = Learn(state, Mock(Reporter))
+		self.assertEqual(state, l.getState())
+
+	def testgetRefTest(self):
+		l = Learn(Mock(LearnState), Mock(Reporter))
+		self.assertEqual("Learn", str(l.getRef().getURI().getPath()))
+
+	def testfinalStateNotificationTest(self):
+		# check that listeners get notified when session ends.
+		pass
+
+	def testStartStopBasic(self) :
+		learn = self.createBasicLearn()
+		factory = self.createFactory()
+		learn.start(factory)
+		#extra, check if start worked ok
+		self.assertEqual(None, learn.getState().getError())
+		self.assertFalse(learn.getState().isFinal(time()*1000))
+		sleep(2.000)
+		self.assertTrue(learn.getState().isFinal(time()*1000))
+
+	def testStartStopLearn(self):
+		learn = self.createBasicLearn();
+		factory = self.createFactory()
+		learn.start(factory)
+		self.assertFalse(learn.getState().isFinal(time()*1000))
+
+		# Instead of mocking connectin we call actionRequest directly
+		learn._actionRequest(self.conn1, LearningDone(self.party1id))
+		self.assertFalse(learn.getState().isFinal(time()*1000))
+		learn._actionRequest(self.conn2, LearningDone(self.party2id))
+		self.assertTrue(learn.getState().isFinal(time()*1000))
+
+	def testAddParty(self):
+		l = Learn(Mock(LearnState), Mock(Reporter))
+		self.assertRaises(ValueError, lambda:l.addParticipant(Mock(PartyWithProfile)))
+	
+
+	def testisFinishSentNormally(self):
+		learn = self.createBasicLearn()
+		factory = self.createFactory()
+		learn.start(factory)
+		learn._actionRequest(self.conn1,  LearningDone(self.party1id))
+		learn._actionRequest(self.conn2, LearningDone(self.party2id))
+		
+		self.assertEqual(1, len([call for call in self.conn1.send.call_args_list \
+			if isinstance(call[0][0],Finished)]))
+		self.assertEqual(1, len([call for call in self.conn2.send.call_args_list \
+			if isinstance(call[0][0],Finished)]))
+
+	def testisFinishSentInError(self):
+		learn = self.createBasicLearn()
+		factory = self.createFactory()
+		learn.start(factory)
+		learn._actionRequest(self.conn1, EndNegotiation(self.party1id))
+		self.assertEqual(1, len([call for call in self.conn1.send.call_args_list \
+			if isinstance(call[0][0],Finished)]))
+		self.assertEqual(1, len([call for call in self.conn2.send.call_args_list \
+			if isinstance(call[0][0],Finished)]))
+
+	def createBasicLearn(self)->Learn:
+		team1 = self.createTeam(1)
+		team2 = self.createTeam(2)
+		settings = LearnSettings([team1, team2],self.deadline)
+		return cast(Learn, settings.getProtocol(self.reporter))
+
+	def createTeam( self, nr:int) -> TeamInfo :
+		party1ref = PartyRef(URI("party" + str(nr)))
+		party1 = PartyWithParameters(party1ref, self.params)
+		profile1 = ProfileRef(URI("prof" + str(nr)))
+		partywithp1 = PartyWithProfile(party1, profile1)
+		team1pp = [partywithp1]
+		team = TeamInfo(team1pp)
+		return team
+
+	def createFactory(self )->ProtocolToPartyConnFactory :
+		self.conn1.getParty=Mock(return_value=self.party1id)
+		self.conn2.getParty=Mock(return_value=self.party2id)
+
+		factory = Mock(	ProtocolToPartyConnFactory)
+		# connections = mock(List.class);
+		connections = [self.conn1, self.conn2]
+		factory.connectAll=Mock(return_value=connections)
+		return factory;
+
Index: /geniuswebcore/test/geniusweb/protocol/tournament/TeamTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/protocol/tournament/TeamTest.py	(revision 71)
+++ /geniuswebcore/test/geniusweb/protocol/tournament/TeamTest.py	(revision 71)
@@ -0,0 +1,33 @@
+import unittest
+from unitpy.GeneralTests import GeneralTests
+from geniusweb.protocol.tournament.Team import Team
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from typing import List
+from uri.uri import URI
+
+class TeamTest(unittest.TestCase, GeneralTests[Team]):
+    
+    def setUp(self):
+        self.partyref1 = PartyRef(URI("party1"))
+        self.partyref2 = PartyRef(URI("party2"))
+        self.params1 = Parameters().With("a", 1)
+        self.pwithp1 = PartyWithParameters(self.partyref1, self.params1)
+        self.pwithp2 = PartyWithParameters(self.partyref2, self.params1)
+        self.team1 = Team([self.pwithp1])
+        self.team1a = Team([self.pwithp1])
+        self.team2 = Team([self.pwithp2])
+
+        #@Override
+    def getGeneralTestData(self) -> List[List[Team]]:
+        return [[self.team1, self.team1a],[self.team2]]
+
+    #@Override
+    def getGeneralTestStrings(self) -> List[str]:
+        return [".PartyRef.party1..a=1..",".PartyRef.party2..a=1.."]
+
+    #@Test
+    def testGet(self):
+        self.assertEqual([self.pwithp1], self.team1.getParties())
+        
Index: /geniuswebcore/test/geniusweb/utilsTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/utilsTest.py	(revision 70)
+++ /geniuswebcore/test/geniusweb/utilsTest.py	(revision 71)
@@ -1,4 +1,4 @@
 import unittest
-from geniusweb.utils import val
+from geniusweb.utils import val, HASH, toStr
 
 
@@ -10,2 +10,17 @@
         self.assertEqual(1, val(a))
         
+        
+    def testToStr(self):
+        self.assertEqual("{a=1}", toStr({'a':1}))
+        self.assertEqual("{a=1}", toStr({'a':'1'}))
+        self.assertEqual("[1, 2]", toStr([1,'2']))
+        self.assertEqual("{a=[1, 2]}", toStr({'a':[1,'2']}))
+
+    def testHASH(self):
+        HASH([1,2,3])
+        HASH((1,2,3))
+        HASH({'a':1,'b':2})
+        HASH(12)
+        HASH('12')
+        HASH(0.12)
+        
