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()