Index: /cpp/tests/SDK-tests.txt
===================================================================
--- /cpp/tests/SDK-tests.txt	(revision 798)
+++ /cpp/tests/SDK-tests.txt	(revision 798)
@@ -0,0 +1,45 @@
+exe:geno_test
+
+TESTNAME:geno_test valid f1
+arg:XrqX[G:1.23][-1:2.34]
+out:valid
+out:
+RUNTEST
+
+TESTNAME:geno_test invalid f1
+arg:rq[0:0]
+out:[WARN] Geno.isValid: 1 warning(s) while checking validity of ''
+out:[WARN] Geno.isValid: ...[WARN] GenoConv_F1.grow: Illegal neuron position (ignored)
+out:invalid
+out:
+RUNTEST
+
+TESTNAME:geno_test valid f0
+arg:-
+in://0
+in:p:
+in:p:1
+in:j:0,1
+out:valid
+out:
+RUNTEST
+
+TESTNAME:geno_test invalid f0
+arg:-
+in://0
+in:p:
+in:p:
+out:[ERROR] Geno.isValid: 1 error(s) while checking validity of ''
+out:[ERROR] Geno.isValid: ...[ERROR] Model.internalCheck: Not all parts connected (eg. Part #0 and Part #1)
+out:invalid
+out:
+RUNTEST
+
+##################################
+
+exe:full_props
+
+TESTNAME:full_props
+arg:X
+out:*INSERTPLATFORMDEPENDENTFILE*:full_props_x
+RUNTEST
Index: /cpp/tests/full_props_x.goal
===================================================================
--- /cpp/tests/full_props_x.goal	(revision 798)
+++ /cpp/tests/full_props_x.goal	(revision 798)
@@ -0,0 +1,7 @@
+//0
+m:se=1.0, Vstyle=
+p:x=0.0, y=0.0, z=0.0, sh=0, s=1.0, sx=1.0, sy=1.0, sz=1.0, h=0.0, dn=1.0, fr=0.4, ing=0.25, as=0.25, rx=0.0, ry=0.0, rz=0.0, i=, Vstyle=part, vs=0.2, vr=0.5, vg=0.5, vb=0.5
+p:x=1.0, y=0.0, z=0.0, sh=0, s=1.0, sx=1.0, sy=1.0, sz=1.0, h=0.0, dn=1.0, fr=0.4, ing=0.25, as=0.25, rx=0.0, ry=0.0, rz=0.0, i=, Vstyle=part, vs=0.2, vr=0.5, vg=0.5, vb=0.5
+j:p1=0, p2=1, rx=0.0, ry=0.0, rz=0.0, dx=1.0, dy=0.0, dz=0.0, sh=0, stif=1.0, rotstif=1.0, stam=0.25, i=, Vstyle=joint, vr=0.5, vg=0.5, vb=0.5
+
+
Index: /cpp/tests/go.sh
===================================================================
--- /cpp/tests/go.sh	(revision 798)
+++ /cpp/tests/go.sh	(revision 798)
@@ -0,0 +1,1 @@
+python3 ../../tester/tester.py -e '(.*)=../\1' -f SDK-tests.txt
Index: /tester/comparison.py
===================================================================
--- /tester/comparison.py	(revision 798)
+++ /tester/comparison.py	(revision 798)
@@ -0,0 +1,88 @@
+import globals  # our source
+import re
+import sys
+import os, os.path
+
+
+class Comparison:
+	def __init__(self, l1, l2):
+		self.list1 = self.prepare_fill_in_files(l1)
+		self.list2 = self.prepare_fill_in_files(l2)
+		self.p1 = 0  # updated position within list1
+		self.p2 = 0  # updated position within list2
+		self.ok = True  # the final comparison result
+		self.result = ""  # details of comparing individual lines
+
+		while not self.finished():
+			if not self.compare_element():
+				self.ok = False
+
+	@staticmethod
+	def load_platform_specific(file_name):
+		name = file_name + "-" + globals.PLATFORM + ".goal"
+
+		try:
+			fin = open(os.path.join(globals.THISDIR, name))  # first we look for the file with the specialized name (platform-dependent)...
+		except IOError:
+			fin = open(os.path.join(globals.THISDIR, file_name + ".goal"))  # ...if there is no such file, we try to find the default file
+		list_of_lines = []
+		for line in fin:
+			line = globals.stripEOL(line)
+			list_of_lines.append(line)
+		return list_of_lines
+
+	def prepare_fill_in_files(self, list_of_lines):  # when finds a line with SPEC_USEPLATFORMDEPENDENTFILE, replaces it with the contents of the appropriate file
+		new_list = []
+		for l in list_of_lines:
+			if l.startswith(globals.SPEC_INSERTPLATFORMDEPENDENTFILE):
+				file_name = l[len(globals.SPEC_INSERTPLATFORMDEPENDENTFILE):]
+				new_list += self.load_platform_specific(file_name)
+			else:
+				new_list.append(l)
+		return new_list
+
+	def finished(self):
+		return self.p1 >= len(self.list1) and self.p2 >= len(self.list2)
+
+	@staticmethod
+	def compare_strings(s1, s2):
+		if s1 == s2:
+			return True
+		if not globals.WIN_SLASH_SENSITIVE and os.path.sep == '\\':  # give a chance for approximate comparison - does not distinguish /\ but only on the windows platform
+			return s1.replace('\\', '/') == s2  # and matches only the local output, assuming that the goal uses linux-standard /
+		else:
+			return False
+
+	def compare_element(self):
+		e1 = "(missing)" if self.p1 >= len(self.list1) else self.list1[self.p1]
+		e2 = "(missing)" if self.p2 >= len(self.list2) else self.list2[self.p2]
+		e2disp = e2
+		if e2.startswith(globals.SPEC_SKIPTO):
+			e2 = e2[len(globals.SPEC_SKIPTO):]
+			waiting = True
+			if self.p1 >= len(self.list1):  # exception: skipto becomes a standard comparison when the input stream ends (because then we know we will not read anything more than we have now)
+				waiting = False
+		else:
+			waiting = False
+		if e2.startswith(globals.SPEC_REGEXP):
+			ok = re.match(e2[len(globals.SPEC_REGEXP):], e1)
+		else:
+			ok = self.compare_strings(e1, e2)  # ignoring the discrimination of /\ will work only when regexp's are not used
+		self.result += repr(e1) + " "
+		if ok:
+			self.p2 += 1
+		else:
+			if waiting:  # handling skipto:
+				if self.p1 >= len(self.list1):
+					self.p2 += 1
+				else:
+					ok = True
+			else:
+				self.p2 += 1
+		if ok:
+			self.result += globals.ANSI_SETGREEN + "ok" + globals.ANSI_RESET + "\n"  # if both are identical, we display only the left one
+		else:
+			self.result += globals.ANSI_SETRED + "<FAIL>" + globals.ANSI_RESET + " "
+			self.result += repr(e2disp) + "\n"
+		self.p1 += 1
+		return ok
Index: /tester/globals.py
===================================================================
--- /tester/globals.py	(revision 798)
+++ /tester/globals.py	(revision 798)
@@ -0,0 +1,59 @@
+import platform, sys
+
+
+def init_colors(args):
+	global ANSI_SETGREEN, ANSI_SETRED, ANSI_RESET
+	if args.nocolor:
+		ANSI_SETGREEN = ANSI_SETRED = ANSI_RESET = ''
+	else:
+		ANSI_SETGREEN = '\x1b[32m' + '\x1b[1m'  # after '+' there is brightness control (making brighter)
+		ANSI_SETRED = '\x1b[31m' + '\x1b[1m'
+		ANSI_RESET = '\x1b[39m' + '\x1b[0m'  # after '+' the brightness is reset
+		if platform.system() == "Windows":
+			try:
+				from colorama import init, Fore, Style  # Fore and Style are here just to get ansi codes to control color and brightness
+				# print repr(Fore.GREEN)
+				# print repr(Fore.RED)
+				# print repr(Fore.RESET)
+				# print repr(Style.BRIGHT)
+			except ImportError:
+				print("The colorama module not available - there will be no ansi stdout colors")
+				ANSI_SETGREEN = ANSI_SETRED = ANSI_RESET = ''
+			else:
+				init()
+
+
+def stripEOL(s):
+	for char in '\n\r':
+		if s[-1:len(s)] == char:
+			s = s[:-1]
+	return s
+
+
+################################################################ main ################################################################
+
+# no longer assuming any specific directory layout, use -tp/-wp/-fp to configure as needed
+THISDIR = "."  # -tp
+EXEDIR = "."   # -wp
+FILESDIR = "." # -fp
+
+# special ways of comparison, to be used in 'out' lines
+SPEC_REGEXP = "*REGEXP*:"
+SPEC_SKIPTO = "*SKIPTO*:"  # skips any number of lines until a matching line is found
+SPEC_INSERTPLATFORMDEPENDENTFILE = "*INSERTPLATFORMDEPENDENTFILE*:"  # loads into this location a file from disk, appending the platform name to the file name. Convention: we either use a single non-specialized file (i.e. results are identical everywhere) or we don't prepare such a single file, but we prepare all specialized files with separate results for each platform
+
+WIN_SLASH_SENSITIVE = False
+
+EXENETMODE = ["-N"]
+ini = ""
+EXEVALGRINDMODE = ["bash", "vg", "--leak-check=full"]
+
+# data becomes a home dir so that tester can find what it looks for.
+# As a resource dir we set 'emptydata' to make frams use only home, but still use splitfilesys.
+# You could also set -Ddata to activate the mode with no splitfilesys.
+EXENAMES = {"default": ["frams.exe", "-ddata", "-Demptydata", "-Q", "-s"],
+			"teatr": ["theater.exe", "-ddata", "-Demptydata", "-Q", "st"]}
+
+EXERULES = []
+
+PLATFORM = sys.platform
Index: /tester/tester.py
===================================================================
--- /tester/tester.py	(revision 798)
+++ /tester/tester.py	(revision 798)
@@ -0,0 +1,259 @@
+import os, os.path, sys, platform, re, copy
+import errno  # for delete_file_if_present()
+import argparse
+from subprocess import Popen, PIPE
+from time import sleep
+import telnetlib
+
+import comparison  # our source
+import globals  # our source
+
+
+def test(args, test_name, input, output_net, output_msg, exe_prog, exeargs):
+	print(test_name, end=" ")
+	command = prepare_exe_with_name(exe_prog)
+	command += exeargs
+	if len(output_net) > 0:
+		command += globals.EXENETMODE
+	if args.valgrind:
+		command = globals.EXEVALGRINDMODE + command
+
+	p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE)
+
+	if len(output_net) > 0:
+		sleep(10 if args.valgrind else 1)  # time for the server to warm up
+		tn = telnetlib.Telnet("localhost", 9009)
+		tn.write(bytes(input, "UTF-8"))
+		sleep(2)  # time for the server to respond...
+		# if we had a command in the frams server protocol to close the connection gracefully, then we could use read_all() instead of the trick with sleep()+read_very_eager()+close()
+		stdnet = tn.read_very_eager().decode().split("\n")  # the server uses "\n" as the end-of-line character on each platform
+		tn.close()  # after this, the server is supposed to close by itself (the -N option)
+		input = ""
+	# under Windows, p.stderr.read() and p.stdout.read() block while the process works, under linux it may be different
+	# http://stackoverflow.com/questions/3076542/how-can-i-read-all-availably-data-from-subprocess-popen-stdout-non-blocking?rq=1
+	# http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python
+	# p.terminate() #this was required when the server did not have the -N option
+	# stderrdata=p.stderr.read() #fortunately it is possible to reclaim (a part of?) stream contents after the process is killed... under Windows this is the ending of the stream
+
+	(stdoutdata, stderrdata) = p.communicate(bytes(input, "UTF-8"))  # the server process ends...
+	stdoutdata = stdoutdata.decode()  # bytes to str
+	stderrdata = stderrdata.decode()  # bytes to str
+	# p.stdin.write(we) #this is not recommended because buffers can overflow and the process will hang up (and indeed it does under Windows) - so communicate() is recommended
+	# stdout = p.stdout.read()
+	# p.terminate()
+
+	# print repr(input)
+	# print repr(stdoutdata)
+
+	stdout = stdoutdata.split(os.linesep)
+	# print stdout
+	stderr = stderrdata.split(os.linesep)
+	ok = check(stdnet if len(output_net) > 0 else stdout, output_list if len(output_list) > 0 else output_net, output_msg)
+
+	if p.returncode != 0 and p.returncode != None:
+		print("  ", p.returncode, "<- returned code")
+
+	if len(stderrdata) > 0:
+		print("   (stderr has %d lines)" % len(stderr))
+		# valgrind examples:
+		# ==2176== ERROR SUMMARY: 597 errors from 50 contexts (suppressed: 35 from 8)
+		# ==3488== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 35 from 8)
+		if (not args.valgrind) or ("ERROR SUMMARY:" in stderrdata and " 0 errors" not in stderrdata) or (args.always_show_stderr):
+			print(stderrdata)
+
+	if not ok and args.stop:
+		sys.exit("First test failure, stopping early.")
+	return ok
+
+
+def compare(jest, goal, was_compared_to):
+	p = comparison.Comparison(jest, goal)
+	if p.ok:
+		print("\r", globals.ANSI_SETGREEN + " ok" + globals.ANSI_RESET)
+	else:
+		print("\r", globals.ANSI_SETRED + " FAIL\7" + globals.ANSI_RESET)
+		print(p.result)
+		f = open(os.path.join(globals.THISDIR, '_last_failed' + was_compared_to + '.output'), 'w')  # files are easier to compare than stdout
+		print('\n'.join(jest), end="", file=f)
+		f = open(os.path.join(globals.THISDIR, '_last_failed' + was_compared_to + '.goal'), 'w')  # files are easier to compare than stdout
+		print('\n'.join(goal), end="", file=f)
+	return p.ok
+
+
+def remove_prefix(text, prefix):
+	return text[len(prefix):] if text.startswith(prefix) else text
+
+
+def check(stdout, output_net, output_msg):
+	actual_out_msg = []
+	if len(output_net) > 0:  # in case of the server, there is no filtering
+		for line in stdout:
+			actual_out_msg.append(line)
+		return compare(actual_out_msg, output_net, '')
+	else:
+		FROMSCRIPT = "Script.Message: "
+		beginnings = tuple(["[" + v + "] " for v in ("INFO", "WARN", "ERROR", "CRITICAL")])  # there is also "DEBUG"
+		header_begin = 'VMNeuronManager.autoload: Neuro classes added: '  # header section printed when the simulator is created
+		header_end = "UserScripts.autoload: "  # ending of the header section
+		now_in_header = False
+		for line in stdout:
+			if now_in_header:
+				if header_end in line:  # "in" because multithreaded simulators prefix their messages with their numerical id, e.g. #12/...
+					now_in_header = False
+				continue
+			else:
+				if header_begin in line:  # as above
+					now_in_header = True
+					continue
+				line = remove_prefix(line, beginnings[0])  # cut out [INFO], other prefixes we want to leave as they are
+				line = remove_prefix(line, FROMSCRIPT)  # cut out FROMSCRIPT
+				actual_out_msg.append(line)
+		if actual_out_msg[-1] == '':  # empty line at the end which is not present in our "goal" contents
+			actual_out_msg.pop()
+		return compare(actual_out_msg, output_msg, '')
+
+
+def delete_file_if_present(filename):
+	print('"%s" (%s)' % (filename, "the file was present" if os.path.exists(filename) else "this file did not exist"))
+	try:
+		os.remove(filename)
+	except OSError as e:
+		if e.errno != errno.ENOENT:  # errno.ENOENT = no such file or directory
+			raise  # re-raise exception if a different error occurred
+
+
+def reset_values():
+	global input_text, output_net, output_msg, test_name, ini, output_list, exeargs, exe_prog
+	input_text = ""
+	output_list = []
+	ini = ""
+	output_net, output_msg = [], []
+	exeargs = []
+	test_name = "no-name test"
+
+
+def is_test_active():
+	global test_name
+	if name_template == "":
+		return True
+	if re.match(name_template, test_name):
+		return True
+	return False
+
+
+def prepare_exe_with_name(name):
+	if name in globals.EXENAMES:
+		exename = copy.copy(globals.EXENAMES[name])  # without copy, the following modifications would change values in the EXENAMES table
+	else:
+		exename = [name]
+	for rule in globals.EXERULES:
+		exename[0] = re.sub(rule[0], rule[1], exename[0])
+	return exename
+
+
+def main(main_test_filename):
+	global input_text, name_template, test_name, exe_prog, exeargs
+	name_template = ""
+	exeargs = []
+
+	parser = argparse.ArgumentParser()
+	parser.add_argument("-val", "--valgrind", help="Use valgrind", action="store_true")
+	parser.add_argument("-c", "--nocolor", help="Don't use color output", action="store_true")
+	parser.add_argument("-f", "--file", help="File name with tests, default is '" + main_test_filename + "'")
+	parser.add_argument("-tp", "--tests-path", help="tests directory, files containing test definitions, inputs and outputs are relative to this directory, default is '" + globals.THISDIR + "'")
+	parser.add_argument("-fp", "--files-path", help="files directory, files tested by OUTFILECOMPARE are referenced relative to this directory, default is '" + globals.FILESDIR + "'")
+	parser.add_argument("-wp", "--working-path", help="working directory, test executables are launched after chdir to this directory, default is '" + globals.EXEDIR + "'")
+	parser.add_argument("-n", "--name", help="Test name (regexp)")  # e.g. '^((?!python).)*$' = these tests which don't have the word "python" in their name
+	parser.add_argument("-s", "--stop", help="Stops on first difference", action="store_true")
+	parser.add_argument("-ds", "--diffslashes", help="Discriminate between slashes (consider / and \\ different)", action="store_true")
+	parser.add_argument("-err", "--always-show-stderr", help="Always print stderr (by default it is hidden if 0 errors in valgrind)", action="store_true")
+	parser.add_argument("-e", "--exe", help="Regexp 'search=replace' rule(s) transforming executable name(s) into paths (eg. '(.*)=path/to/\\1.exe')", action='append') #in the exaxmple, double backslash is just for printing
+	parser.add_argument("-p", "--platform", help="Override platform identifier (referencing platform specific files " + globals.SPEC_INSERTPLATFORMDEPENDENTFILE + "), default:sys.platform (win32,linux2)")
+	args = parser.parse_args()
+	if args.valgrind:
+		print("Using valgrind...")
+	if args.diffslashes:
+		globals.DIFFSLASHES = args.diffslashes
+	if args.file:
+		main_test_filename = args.file
+	if args.tests_path:
+		globals.THISDIR=args.tests_path
+	if args.files_path:
+		globals.FILESDIR=args.files_path
+	if args.working_path:
+		globals.EXEDIR=args.working_path
+	if args.name:
+		name_template = args.name
+	if args.exe:
+		for e in args.exe:
+			search, replace = e.split('=', 1)
+			globals.EXERULES.append((search, replace))
+	if args.platform:
+		globals.PLATFORM = args.platform
+
+	os.chdir(globals.EXEDIR)
+
+	globals.init_colors(args)
+
+	fin = open(os.path.join(globals.THISDIR, main_test_filename))
+	reset_values()
+	exe_prog = "default" # no longer in reset_values (exe: persists across tests)
+	outfile = []
+	tests_failed = 0
+	tests_total = 0
+	for line in fin:
+		line = globals.stripEOL(line)
+		if len(line) == 0 or line.startswith("#"):
+			continue
+		line = line.split(":", 1)
+		# print line
+		command = line[0]
+		if command == "TESTNAME":
+			reset_values()
+			test_name = line[1]
+		elif command == "arg":
+			exeargs.append(line[1])
+		elif command == "exe":
+			exe_prog = line[1]
+		elif command == "in":
+			input_text += line[1] + "\n"
+		elif command == "out-net":
+			output_net.append(line[1])
+		elif command == "out-file":
+			outfile.append(line[1])
+		elif command == "out-mesg":
+			output_msg.append(line[1])
+		elif command == "out":
+			output_list.append(line[1])
+		elif command == "DELETEFILENOW":
+			if is_test_active():
+				print("\t ", command, end=" ")
+				delete_file_if_present(os.path.join(globals.FILESDIR, line[1]))
+		elif command == "OUTFILECLEAR":
+			outfile = []
+		elif command == "OUTFILECOMPARE":
+			if is_test_active():
+				print("\t", command, '"%s"' % line[1], end=" ")
+				contents = []
+				with open(os.path.join(globals.FILESDIR, line[1]), 'r') as main_test_filename:
+					for line in main_test_filename:
+						contents.append(globals.stripEOL(line))
+				ok = compare(contents, outfile, '_file')
+				tests_failed += int(not ok)
+				tests_total += 1
+		elif command == "RUNTEST":
+			if is_test_active():
+				print("\t", command, end=" ")
+				ok = test(args, test_name, input_text, output_net, output_msg, exe_prog, exeargs)
+				tests_failed += int(not ok)
+				tests_total += 1
+		else:
+			raise Exception("Don't know what to do with this line in test file: ", line)
+
+	return (tests_failed, tests_total)
+
+
+if __name__ == "__main__":
+	tests_failed, tests_total = main("testy.txt")
+	print("%d / %d failed tests" % (tests_failed, tests_total))
+	sys.exit(tests_failed)  # return the number of failed tests as exit code ("error level") to shell
