File indexing completed on 2024-04-28 07:51:07
0001 #!/usr/bin/env python3 0002 # -*- coding: utf-8 -*- 0003 0004 """ 0005 Copyright (C) 2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0006 0007 SPDX-License-Identifier: GPL-2.0 0008 0009 """ 0010 0011 import signal 0012 import os 0013 import sys 0014 import subprocess 0015 import random 0016 import shutil 0017 import time 0018 import gc 0019 0020 from optparse import OptionParser 0021 from locale import getdefaultlocale 0022 0023 from common import Debug, StrMixin, cacheDir 0024 from util import removeIfExists, gitHead, checkMemory, popenReadlines 0025 from kajcsv import Csv, CsvRow, CsvWriter 0026 0027 signal.signal(signal.SIGINT, signal.SIG_DFL) 0028 0029 0030 OPTIONS = None 0031 0032 KNOWNCOMMITS = set() 0033 0034 class Clone: 0035 0036 """make a temp directory for commitId""" 0037 0038 def __init__(self, commitId): 0039 self.commitId = commitId 0040 if commitId != 'current': 0041 tmpdir = os.path.expanduser(os.path.join(cacheDir(), commitId)) 0042 if not os.path.exists(tmpdir): 0043 subprocess.Popen('git clone --shared --no-checkout -q .. {temp}'.format( 0044 temp=tmpdir).split()).wait() 0045 subprocess.Popen('git checkout -q {commitId}'.format( 0046 commitId=commitId).split(), cwd=tmpdir).wait() 0047 0048 def sourceDirectory(self): 0049 """the source directory for this git commit""" 0050 if self.commitId == 'current': 0051 tmpdir = os.path.abspath('..') 0052 result = os.path.join(tmpdir, 'src') 0053 else: 0054 result = os.path.join(cacheDir(), self.commitId, 'src') 0055 assert os.path.exists(result), '{} does not exist'.format(result) 0056 return result 0057 0058 @classmethod 0059 def removeObsolete(cls): 0060 """remove all clones for obsolete commits""" 0061 for commitDir in os.listdir(cacheDir()): 0062 if not any(x.startswith(commitDir) for x in KNOWNCOMMITS): 0063 removeDir = os.path.join(cacheDir(), commitDir) 0064 shutil.rmtree(removeDir) 0065 0066 class Client: 0067 0068 """a simple container, assigning process to job""" 0069 0070 def __init__(self, process=None, job=None): 0071 self.process = process 0072 self.job = job 0073 0074 0075 class TooManyClients(UserWarning): 0076 0077 """we would surpass options.clients""" 0078 0079 0080 class TooManyServers(UserWarning): 0081 0082 """we would surpass options.servers""" 0083 0084 0085 class Server(StrMixin): 0086 0087 """represents a kajongg server instance. Called when we want to start a job.""" 0088 servers = [] 0089 count = 0 0090 0091 def __new__(cls, job): 0092 """can we reuse an existing server?""" 0093 running = Server.allRunningJobs() 0094 if len(running) >= OPTIONS.clients: 0095 raise TooManyClients 0096 maxClientsPerServer = OPTIONS.clients / OPTIONS.servers 0097 matchingServers = [ 0098 x for x in cls.servers 0099 if x.commitId == job.commitId 0100 and x.pythonVersion == job.pythonVersion 0101 and len(x.jobs) < maxClientsPerServer] 0102 if matchingServers: 0103 result = sorted(matchingServers, key=lambda x: len(x.jobs))[0] 0104 else: 0105 if len(cls.servers) >= OPTIONS.servers: 0106 # maybe we can kill a server without jobs? 0107 for server in cls.servers: 0108 if not server.jobs: 0109 server.stop() 0110 break # we only need to stop one server 0111 else: 0112 # no server without jobs found 0113 raise TooManyServers 0114 result = object.__new__(cls) 0115 cls.servers.append(result) 0116 return result 0117 0118 def __init__(self, job): 0119 if not hasattr(self, 'jobs'): 0120 self.jobs = [] 0121 self.process = None 0122 self.socketName = None 0123 self.portNumber = None 0124 self.commitId = job.commitId 0125 self.pythonVersion = job.pythonVersion 0126 self.clone = Clone(job.commitId) 0127 self.start(job) 0128 else: 0129 self.jobs.append(job) 0130 0131 @classmethod 0132 def allRunningJobs(cls): 0133 """a list of all jobs on all servers""" 0134 return sum((x.jobs for x in cls.servers), []) 0135 0136 def start(self, job): 0137 """start this server""" 0138 job.server = self 0139 assert self.process is None, 'Server.start already has a process' 0140 self.jobs.append(job) 0141 assert self.commitId == job.commitId, 'Server.commitId {} != Job.commitId {}'.format( 0142 self.commitId, job.commitId) 0143 cmd = [os.path.join( 0144 job.srcDir(), 0145 'kajonggserver.py')] 0146 cmd.insert(0, 'python{}'.format(self.pythonVersion)) 0147 if OPTIONS.usePort: 0148 self.portNumber = random.randrange(1025, 65000) 0149 cmd.append('--port={port}'.format(port=self.portNumber)) 0150 else: 0151 Server.count += 1 0152 self.socketName = os.path.expanduser( 0153 os.path.join('~', '.kajongg', 0154 'sock{commit}.py{py}.{ctr}'.format( 0155 commit=self.commitId, py=self.pythonVersion, ctr=Server.count))) 0156 cmd.append('--socket={sock}'.format(sock=self.socketName)) 0157 if OPTIONS.debug: 0158 cmd.append('--debug={dbg}'.format(dbg=','.join(OPTIONS.debug))) 0159 if OPTIONS.log: 0160 self.process = subprocess.Popen( 0161 cmd, cwd=job.srcDir(), 0162 stdout=job.logFile, stderr=job.logFile) 0163 else: 0164 # reuse this server (otherwise it stops by itself) 0165 cmd.append('--continue') 0166 self.process = subprocess.Popen(cmd, cwd=job.srcDir()) 0167 print('{} started'.format(self)) 0168 0169 def stop(self, job=None): 0170 """maybe stop the server""" 0171 if self not in self.servers: 0172 # already stopped 0173 return 0174 if job: 0175 self.jobs.remove(job) 0176 if not self.jobs: 0177 self.servers.remove(self) 0178 if self.process: 0179 try: 0180 self.process.terminate() 0181 self.process.wait() 0182 except OSError: 0183 pass 0184 print('{} killed'.format(self)) 0185 if self.socketName: 0186 removeIfExists(self.socketName) 0187 0188 @classmethod 0189 def stopAll(cls): 0190 """stop all servers even if clients are still there""" 0191 for server in cls.servers: 0192 for job in server.jobs[:]: 0193 server.stop(job) 0194 assert not server.jobs, 'stopAll expects no server jobs but found {}'.format( 0195 server.jobs) 0196 server.stop() 0197 0198 def __str__(self): 0199 return 'Server {} Python{}{} {}'.format( 0200 self.commitId, 0201 self.pythonVersion, 0202 ' pid={}'.format(self.process.pid) if Debug.process else '', 0203 'port={}'.format(self.portNumber) if self.portNumber else 'socket={}'.format(self.socketName)) 0204 0205 0206 class Job(StrMixin): 0207 0208 """a simple container""" 0209 0210 def __init__(self, pythonVersion, ruleset, aiVariant, commitId, game): 0211 self.pythonVersion = pythonVersion 0212 self.ruleset = ruleset 0213 self.aiVariant = aiVariant 0214 self.commitId = commitId 0215 self.game = game 0216 self.__logFile = None 0217 self.logFileName = None 0218 self.process = None 0219 self.server = None 0220 self.started = False 0221 0222 def srcDir(self): 0223 """the path of the directory where the particular test is running""" 0224 assert self.server, 'Job {} has no server'.format(self) 0225 assert self.server.clone, 'Job {} has no server.clone'.format(self) 0226 return self.server.clone.sourceDirectory() 0227 0228 def __startProcess(self, cmd): 0229 """call Popen""" 0230 if OPTIONS.log: 0231 self.process = subprocess.Popen( 0232 cmd, cwd=self.srcDir(), 0233 stdout=self.logFile, stderr=self.logFile) 0234 else: 0235 self.process = subprocess.Popen(cmd, cwd=self.srcDir()) 0236 print(' %s started' % (self)) 0237 0238 def start(self): 0239 """start this job""" 0240 # pylint: disable=too-many-branches 0241 self.server = Server(self) 0242 # never login to the same server twice at the 0243 # same time with the same player name 0244 player = self.server.jobs.index(self) + 1 0245 cmd = [os.path.join(self.srcDir(), 'kajongg.py'), 0246 '--game={game}'.format(game=self.game), 0247 '--player={tester} {player}'.format( 0248 player=player, 0249 tester='Tüster'), 0250 '--ruleset={ap}'.format(ap=self.ruleset)] 0251 if self.server.socketName: 0252 cmd.append('--socket={sock}'.format(sock=self.server.socketName)) 0253 if self.server.portNumber: 0254 cmd.append('--port={port}'.format(port=self.server.portNumber)) 0255 cmd.insert(0, 'python{}'.format(self.pythonVersion)) 0256 if OPTIONS.rounds: 0257 cmd.append('--rounds={rounds}'.format(rounds=OPTIONS.rounds)) 0258 if self.aiVariant != 'DefaultAI': 0259 cmd.append('--ai={ai}'.format(ai=self.aiVariant)) 0260 if OPTIONS.csv: 0261 cmd.append('--csv={csv}'.format(csv=OPTIONS.csv)) 0262 if OPTIONS.gui: 0263 cmd.append('--demo') 0264 else: 0265 cmd.append('--nogui') 0266 if OPTIONS.playopen: 0267 cmd.append('--playopen') 0268 if OPTIONS.debug: 0269 cmd.append('--debug={dbg}'.format(dbg=','.join(OPTIONS.debug))) 0270 self.__startProcess(cmd) 0271 self.started = True 0272 0273 def check(self, silent=False): 0274 """if done, cleanup""" 0275 if not self.started or not self.process: 0276 return 0277 result = self.process.poll() 0278 if result is not None: 0279 self.process = None 0280 if not silent: 0281 print(' {} done{}'.format(self, 'Return code: {}'.format(result) if result else '')) 0282 self.server.jobs.remove(self) 0283 0284 @property 0285 def logFile(self): 0286 """open if needed""" 0287 if self.__logFile is None: 0288 logDir = os.path.expanduser( 0289 os.path.join('~', '.kajongg', 'log', str(self.game), 0290 self.ruleset, self.aiVariant, str(self.pythonVersion))) 0291 if not os.path.exists(logDir): 0292 os.makedirs(logDir) 0293 logFileName = self.commitId 0294 self.logFileName = os.path.join(logDir, logFileName) 0295 self.__logFile = open(self.logFileName, 'wb', buffering=0) 0296 return self.__logFile 0297 0298 def shortRulesetName(self): 0299 """strip leading chars if they are identical for all rulesets""" 0300 names = OPTIONS.knownRulesets 0301 for prefix in range(100): 0302 if sum(x.startswith(self.ruleset[:prefix]) for x in names) == 1: 0303 return self.ruleset[prefix - 1:] 0304 return None 0305 0306 def __str__(self): 0307 pid = 'pid={}'.format( 0308 self.process.pid) if self.process and Debug.process else '' 0309 game = 'game={}'.format(self.game) 0310 ruleset = self.shortRulesetName() 0311 aiName = 'AI={}'.format( 0312 self.aiVariant) if self.aiVariant != 'DefaultAI' else '' 0313 return ' '.join([ 0314 self.commitId, 'Python{}'.format(self.pythonVersion), pid, game, ruleset, aiName]).replace(' ', ' ') 0315 0316 0317 0318 0319 def cleanup_data(csv): 0320 """remove all data referencing obsolete commits""" 0321 logDir = os.path.expanduser(os.path.join('~', '.kajongg', 'log')) 0322 knownCommits = csv.commits() 0323 for dirName, _, fileNames in os.walk(logDir): 0324 for fileName in fileNames: 0325 if fileName not in knownCommits and fileName != 'current': 0326 os.remove(os.path.join(dirName, fileName)) 0327 try: 0328 os.removedirs(dirName) 0329 except OSError: 0330 pass # not yet empty 0331 Clone.removeObsolete() 0332 0333 def pairs(data): 0334 """return all consecutive pairs""" 0335 prev = None 0336 for _ in data: 0337 if prev: 0338 yield prev, _ 0339 prev = _ 0340 0341 0342 class CSV(StrMixin): 0343 """represent kajongg.csv""" 0344 0345 knownCommits = [] 0346 0347 def __init__(self): 0348 self.findKnownCommits() 0349 self.rows = [] 0350 if os.path.exists(OPTIONS.csv): 0351 self.rows = list(sorted({CsvRow(x) for x in Csv.reader(OPTIONS.csv)})) 0352 self.removeInvalidCommits() 0353 0354 def neutralize(self): 0355 """remove things we do not want to compare""" 0356 for row in self.rows: 0357 row.neutralize() 0358 0359 def commits(self): 0360 """return set of all our commit ids""" 0361 # TODO: sorted by date 0362 return {x.commit for x in self.rows} 0363 0364 def games(self): 0365 """return a sorted unique list of all games""" 0366 return sorted({x.game for x in self.rows}) 0367 0368 @classmethod 0369 def findKnownCommits(cls): 0370 """find known commits""" 0371 if not cls.knownCommits: 0372 cls.knownCommits = set() 0373 for branch in subprocess.check_output(b'git branch'.split()).decode().split('\n'): 0374 if 'detached' not in branch and 'no branch' not in branch: 0375 cls.knownCommits |= set(subprocess.check_output( 0376 'git log --max-count=400 --pretty=%H {branch}'.format( 0377 branch=branch[2:]).split()).decode().split('\n')) 0378 0379 @classmethod 0380 def onlyExistingCommits(cls, commits): 0381 """return a set with only existing commits""" 0382 result = set() 0383 for commit in commits: 0384 if any(x.startswith(commit) for x in cls.knownCommits): 0385 result.add(commit) 0386 return result 0387 0388 def removeInvalidCommits(self): 0389 """remove rows with invalid git commit ids""" 0390 csvCommits = {x.commit for x in self.rows} 0391 csvCommits = { 0392 x for x in csvCommits if set( 0393 x) <= set( 0394 '0123456789abcdef') and len( 0395 x) >= 7} 0396 nonExisting = csvCommits - self.onlyExistingCommits(set(x.commit for x in self.rows)) 0397 if nonExisting: 0398 print( 0399 'removing rows from kajongg.csv for commits %s' % 0400 ','.join(nonExisting)) 0401 self.rows = [x for x in self.rows if x.commit not in nonExisting] 0402 self.write() 0403 0404 def write(self): 0405 """write new csv file""" 0406 writer = CsvWriter(OPTIONS.csv) 0407 for row in self.rows: 0408 writer.writerow(row) 0409 del writer 0410 0411 def evaluate(self): 0412 """evaluate the data. Show differences as helpful as possible""" 0413 found_difference = False 0414 for ruleset, aiVariant, game in {(x.ruleset, x.aiVariant, x.game) for x in self.rows}: 0415 rows = list(reversed(sorted( 0416 x for x in self.rows 0417 if ruleset == x.ruleset and aiVariant == x.aiVariant and game == x.game))) 0418 for fixedField in (CsvRow.fields.PY_VERSION, CsvRow.fields.COMMIT): 0419 for fixedValue in set(x[fixedField] for x in rows): 0420 checkRows = [x for x in rows if x[fixedField] == fixedValue] 0421 for warned in self.compareRows(checkRows): 0422 found_difference = True 0423 rows.remove(warned) 0424 found_difference |= len(self.compareRows(rows)) > 0 0425 self.compareRows(rows) 0426 if not found_difference: 0427 print('found no differences in {}'.format(OPTIONS.csv)) 0428 0429 @staticmethod 0430 def compareRows(rows): 0431 """in absence of differences, there should be only one row. 0432 return a list of rows which appeared in warnings""" 0433 if not rows: 0434 return [] 0435 msgHeader = None 0436 result = [] 0437 ruleset = rows[0].ruleset 0438 aiVariant = rows[0].aiVariant 0439 game = rows[0].game 0440 differences = [] 0441 for pair in pairs(rows): 0442 causes = pair[1].differs_for(pair[0]) 0443 if causes: 0444 differences.append(tuple([pair[0], pair[1], causes])) 0445 for difference in sorted(differences, key=lambda x: len(x[2])): 0446 if not set(difference[:2]) & set(result): 0447 if msgHeader is None: 0448 msgHeader = 'looking at game={} ruleset={} AI={}'.format( 0449 game, ruleset, aiVariant) 0450 print(msgHeader) 0451 print(' {} {}'.format(*difference[2])) 0452 result.extend(difference[:2]) 0453 return result 0454 0455 0456 def startingDir(): 0457 """the path of the directory where kajonggtest has been started in""" 0458 return os.path.dirname(sys.argv[0]) 0459 0460 0461 def getJobs(jobs): 0462 """fill the queue""" 0463 try: 0464 while len(jobs) < OPTIONS.clients: 0465 jobs.append(next(OPTIONS.jobs)) 0466 except StopIteration: 0467 pass 0468 return jobs 0469 0470 0471 def doJobs(): 0472 """now execute all jobs""" 0473 # pylint: disable=too-many-branches, too-many-locals, too-many-statements 0474 0475 if not OPTIONS.git and OPTIONS.csv: 0476 if gitHead() in ('current', None): 0477 print( 0478 'Disabling CSV output: %s' % 0479 ('You have uncommitted changes' if gitHead() == 'current' else 'No git')) 0480 print() 0481 OPTIONS.csv = None 0482 0483 try: 0484 jobs = [] 0485 while getJobs(jobs): 0486 for checkJob in Server.allRunningJobs()[:]: 0487 checkJob.check() 0488 try: 0489 jobs[0].start() 0490 jobs = jobs[1:] 0491 except TooManyServers: 0492 time.sleep(3) 0493 except TooManyClients: 0494 time.sleep(3) 0495 except KeyboardInterrupt: 0496 Server.stopAll() 0497 except BaseException as exc: 0498 print(exc) 0499 raise exc 0500 finally: 0501 while True: 0502 running = Server.allRunningJobs() 0503 if not running: 0504 break 0505 for job in running: 0506 if not job.started: 0507 if job.server: 0508 job.server.jobs.remove(job) 0509 else: 0510 job.check() 0511 if job.process: 0512 print('Waiting for %s' % job) 0513 job.process.wait() 0514 time.sleep(1) 0515 0516 0517 def parse_options(): 0518 """parse options""" 0519 parser = OptionParser() 0520 parser.add_option( 0521 '', '--gui', dest='gui', action='store_true', 0522 default=False, help='show graphical user interface') 0523 parser.add_option( 0524 '', '--ruleset', dest='rulesets', default='ALL', 0525 help='play like a robot using RULESET: comma separated list. If missing, test all rulesets', 0526 metavar='RULESET') 0527 parser.add_option( 0528 '', '--rounds', dest='rounds', 0529 help='play only # ROUNDS per game', 0530 metavar='ROUNDS') 0531 parser.add_option( 0532 '', '--ai', dest='aiVariants', 0533 default=None, help='use AI variants: comma separated list', 0534 metavar='AI') 0535 parser.add_option( 0536 '', '--python', dest='pyVersions', 0537 default=None, help='use python versions: comma separated list', 0538 metavar='PY_VERSION') 0539 parser.add_option( 0540 '', '--log', dest='log', action='store_true', 0541 default=False, help='write detailled debug info to ~/.kajongg/log/game/ruleset/commit.' 0542 ' This starts a separate server process per job, it sets --servers to --clients.') 0543 parser.add_option( 0544 '', '--game', dest='game', 0545 help='start first game with GAMEID, increment for following games.' + 0546 ' Without this, random values are used.', 0547 metavar='GAMEID', type=int, default=0) 0548 parser.add_option( 0549 '', '--count', dest='count', 0550 help='play COUNT games. Default is unlimited', 0551 metavar='COUNT', type=int, default=999999999) 0552 parser.add_option( 0553 '', '--playopen', dest='playopen', action='store_true', 0554 help='all robots play with visible concealed tiles', default=False) 0555 parser.add_option( 0556 '', '--clients', dest='clients', 0557 help='start a maximum of CLIENTS kajongg instances. Default is 2', 0558 metavar='CLIENTS', type=int, default=1) 0559 parser.add_option( 0560 '', '--servers', dest='servers', 0561 help='start a maximum of SERVERS kajonggserver instances. Default is 1', 0562 metavar='SERVERS', type=int, default=1) 0563 parser.add_option( 0564 '', '--git', dest='git', 0565 help='check all commits: either a comma separated list or a range from..until') 0566 parser.add_option( 0567 '', '--debug', dest='debug', 0568 help=Debug.help()) 0569 0570 return parser.parse_args() 0571 0572 0573 def improve_options(): 0574 """add sensible defaults""" 0575 # pylint: disable=too-many-branches,too-many-statements 0576 if OPTIONS.servers < 1: 0577 OPTIONS.servers = 1 0578 0579 cmdPath = os.path.join(startingDir(), 'kajongg.py') 0580 cmd = ['python3', cmdPath, '--rulesets'] 0581 OPTIONS.knownRulesets = list(popenReadlines(cmd)) 0582 if OPTIONS.rulesets == 'ALL': 0583 OPTIONS.rulesets = OPTIONS.knownRulesets 0584 else: 0585 wantedRulesets = OPTIONS.rulesets.split(',') 0586 usingRulesets = [] 0587 wrong = False 0588 for ruleset in wantedRulesets: 0589 matches = [x for x in OPTIONS.knownRulesets if ruleset in x] 0590 if not matches: 0591 print('ruleset', ruleset, 'is not known', end=' ') 0592 wrong = True 0593 elif len(matches) > 1: 0594 exactMatch = [x for x in OPTIONS.knownRulesets if ruleset == x] 0595 if len(exactMatch) == 1: 0596 usingRulesets.append(exactMatch[0]) 0597 else: 0598 print('ruleset', ruleset, 'is ambiguous:', matches) 0599 wrong = True 0600 else: 0601 usingRulesets.append(matches[0]) 0602 if wrong: 0603 sys.exit(1) 0604 OPTIONS.rulesets = usingRulesets 0605 if OPTIONS.git is not None: 0606 if '..' in OPTIONS.git: 0607 if '^' not in OPTIONS.git: 0608 OPTIONS.git = OPTIONS.git.replace('..', '^..') 0609 commits = subprocess.check_output( 0610 'git log --grep _SILENT --invert-grep --pretty=%h {range}'.format( 0611 range=OPTIONS.git).split()).decode() 0612 _ = list(x.strip() for x in commits.split('\n') if x.strip()) 0613 OPTIONS.git = list(reversed(_)) 0614 else: 0615 OPTIONS.git = CSV.onlyExistingCommits(OPTIONS.git.split(',')) 0616 if not OPTIONS.git: 0617 sys.exit(1) 0618 if OPTIONS.debug is None: 0619 OPTIONS.debug = [] 0620 else: 0621 OPTIONS.debug = [OPTIONS.debug] 0622 if OPTIONS.log: 0623 OPTIONS.servers = OPTIONS.clients 0624 OPTIONS.debug.extend( 0625 'neutral,dangerousGame,explain,originalCall,robbingKong,robotAI,scores,traffic,hand'.split(',')) 0626 if gitHead() not in ('current', None) and not OPTIONS.log: 0627 OPTIONS.debug.append('git') 0628 if not OPTIONS.aiVariants: 0629 OPTIONS.aiVariants = 'DefaultAI' 0630 if OPTIONS.pyVersions: 0631 OPTIONS.pyVersions = OPTIONS.pyVersions.split(',') 0632 else: 0633 OPTIONS.pyVersions = ['3'] 0634 OPTIONS.allAis = OPTIONS.aiVariants.split(',') 0635 if OPTIONS.count: 0636 print('rulesets:', ', '.join(OPTIONS.rulesets)) 0637 _ = ' '.join(OPTIONS.allAis) 0638 if _ != 'DefaultAI': 0639 print('AIs:', _) 0640 if OPTIONS.git: 0641 print('commits:', ' '.join(OPTIONS.git)) 0642 # since we order jobs by game, commit we want one permanent server per 0643 # commit 0644 OPTIONS.jobs = allJobs() 0645 OPTIONS.games = allGames() 0646 OPTIONS.jobCount = 0 0647 OPTIONS.usePort = os.name == 'nt' 0648 0649 0650 def allGames(): 0651 """a generator returning game ids""" 0652 while True: 0653 if OPTIONS.game: 0654 result = OPTIONS.game 0655 OPTIONS.game += 1 0656 else: 0657 result = int(random.random() * 10 ** 9) 0658 yield result 0659 0660 0661 def allJobs(): 0662 """a generator returning Job instances""" 0663 # pylint: disable=too-many-nested-blocks 0664 for game in OPTIONS.games: 0665 for commitId in OPTIONS.git or ['current']: 0666 for ruleset in OPTIONS.rulesets: 0667 for aiVariant in OPTIONS.allAis: 0668 for pyVersion in OPTIONS.pyVersions: 0669 OPTIONS.jobCount += 1 0670 if OPTIONS.jobCount > OPTIONS.count: 0671 return 0672 yield Job(pyVersion, ruleset, aiVariant, commitId, game) 0673 0674 def main(): 0675 """parse options, play, evaluate results""" 0676 global OPTIONS # pylint: disable=global-statement 0677 0678 locale_encoding = getdefaultlocale()[1] 0679 if locale_encoding != 'UTF-8': 0680 print('we need default encoding UTF-8 but have {}'.format(locale_encoding)) 0681 sys.exit(2) 0682 0683 # we want only english in the logs because i18n and friends 0684 # behave differently in kde and kde 0685 os.environ['LANG'] = 'en_US.UTF-8' 0686 (OPTIONS, args) = parse_options() 0687 OPTIONS.csv = os.path.expanduser( 0688 os.path.join('~', '.kajongg', 'kajongg.csv')) 0689 if not os.path.exists(os.path.dirname(OPTIONS.csv)): 0690 os.makedirs(os.path.dirname(OPTIONS.csv)) 0691 0692 csv = CSV() 0693 0694 improve_options() 0695 0696 csv.evaluate() 0697 0698 errorMessage = Debug.setOptions(','.join(OPTIONS.debug)) 0699 if errorMessage: 0700 print(errorMessage) 0701 sys.exit(2) 0702 0703 if args and ''.join(args): 0704 print('unrecognized arguments:', ' '.join(args)) 0705 sys.exit(2) 0706 0707 print() 0708 0709 if OPTIONS.count: 0710 doJobs() 0711 if OPTIONS.csv: 0712 CSV().evaluate() 0713 0714 def cleanup(sig, unusedFrame): 0715 """at program end""" 0716 Server.stopAll() 0717 sys.exit(sig) 0718 0719 # is one server for two clients. 0720 if __name__ == '__main__': 0721 signal.signal(signal.SIGABRT, cleanup) 0722 signal.signal(signal.SIGTERM, cleanup) 0723 signal.signal(signal.SIGINT, cleanup) 0724 if os.name != 'nt': 0725 signal.signal(signal.SIGHUP, cleanup) 0726 signal.signal(signal.SIGQUIT, cleanup) 0727 0728 main() 0729 gc.collect() 0730 checkMemory()