VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/common/utils.py@ 53137

Last change on this file since 53137 was 53137, checked in by vboxsync, 10 years ago

Validation Kit: show both the darwin version and the OSX version

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 44.8 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: utils.py 53137 2014-10-23 21:15:34Z vboxsync $
3# pylint: disable=C0302
4
5"""
6Common Utility Functions.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2014 Oracle Corporation
12
13This file is part of VirtualBox Open Source Edition (OSE), as
14available from http://www.virtualbox.org. This file is free software;
15you can redistribute it and/or modify it under the terms of the GNU
16General Public License (GPL) as published by the Free Software
17Foundation, in version 2 as it comes in the "COPYING" file of the
18VirtualBox OSE distribution. VirtualBox OSE is distributed in the
19hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
20
21The contents of this file may alternatively be used under the terms
22of the Common Development and Distribution License Version 1.0
23(CDDL) only, as it comes in the "COPYING.CDDL" file of the
24VirtualBox OSE distribution, in which case the provisions of the
25CDDL are applicable instead of those of the GPL.
26
27You may elect to license modified versions of this file under the
28terms and conditions of either the GPL or the CDDL or both.
29"""
30__version__ = "$Revision: 53137 $"
31
32
33# Standard Python imports.
34import datetime;
35import os;
36import platform;
37import re;
38import stat;
39import subprocess;
40import sys;
41import tarfile;
42import time;
43import traceback;
44import unittest;
45import zipfile
46
47if sys.platform == 'win32':
48 import win32api; # pylint: disable=F0401
49 import win32con; # pylint: disable=F0401
50 import win32console; # pylint: disable=F0401
51 import win32process; # pylint: disable=F0401
52else:
53 import signal;
54
55# Python 3 hacks:
56if sys.version_info[0] >= 3:
57 long = int; # pylint: disable=W0622,C0103
58
59
60#
61# Host OS and CPU.
62#
63
64def getHostOs():
65 """
66 Gets the host OS name (short).
67
68 See the KBUILD_OSES variable in kBuild/header.kmk for possible return values.
69 """
70 sPlatform = platform.system();
71 if sPlatform in ('Linux', 'Darwin', 'Solaris', 'FreeBSD', 'NetBSD', 'OpenBSD'):
72 sPlatform = sPlatform.lower();
73 elif sPlatform == 'Windows':
74 sPlatform = 'win';
75 elif sPlatform == 'SunOS':
76 sPlatform = 'solaris';
77 else:
78 raise Exception('Unsupported platform "%s"' % (sPlatform,));
79 return sPlatform;
80
81g_sHostArch = None;
82
83def getHostArch():
84 """
85 Gets the host CPU architecture.
86
87 See the KBUILD_ARCHES variable in kBuild/header.kmk for possible return values.
88 """
89 global g_sHostArch;
90 if g_sHostArch is None:
91 sArch = platform.machine();
92 if sArch in ('i386', 'i486', 'i586', 'i686', 'i786', 'i886', 'x86'):
93 sArch = 'x86';
94 elif sArch in ('AMD64', 'amd64', 'x86_64'):
95 sArch = 'amd64';
96 elif sArch == 'i86pc': # SunOS
97 if platform.architecture()[0] == '64bit':
98 sArch = 'amd64';
99 else:
100 try:
101 sArch = processOutputChecked(['/usr/bin/isainfo', '-n',]);
102 except:
103 pass;
104 sArch = sArch.strip();
105 if sArch != 'amd64':
106 sArch = 'x86';
107 else:
108 raise Exception('Unsupported architecture/machine "%s"' % (sArch,));
109 g_sHostArch = sArch;
110 return g_sHostArch;
111
112
113def getHostOsDotArch():
114 """
115 Gets the 'os.arch' for the host.
116 """
117 return '%s.%s' % (getHostOs(), getHostArch());
118
119
120def isValidOs(sOs):
121 """
122 Validates the OS name.
123 """
124 if sOs in ('darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'linux', 'netbsd', 'nt', 'openbsd', \
125 'os2', 'solaris', 'win', 'os-agnostic'):
126 return True;
127 return False;
128
129
130def isValidArch(sArch):
131 """
132 Validates the CPU architecture name.
133 """
134 if sArch in ('x86', 'amd64', 'sparc32', 'sparc64', 's390', 's390x', 'ppc32', 'ppc64', \
135 'mips32', 'mips64', 'ia64', 'hppa32', 'hppa64', 'arm', 'alpha'):
136 return True;
137 return False;
138
139def isValidOsDotArch(sOsDotArch):
140 """
141 Validates the 'os.arch' string.
142 """
143
144 asParts = sOsDotArch.split('.');
145 if asParts.length() != 2:
146 return False;
147 return isValidOs(asParts[0]) \
148 and isValidArch(asParts[1]);
149
150def getHostOsVersion():
151 """
152 Returns the host OS version. This is platform.release with additional
153 distro indicator on linux.
154 """
155 sVersion = platform.release();
156 sOs = getHostOs();
157 if sOs == 'linux':
158 sDist = '';
159 try:
160 # try /etc/lsb-release first to distinguish between Debian and Ubuntu
161 oFile = open('/etc/lsb-release');
162 for sLine in oFile:
163 oMatch = re.search(r'(?:DISTRIB_DESCRIPTION\s*=)\s*"*(.*)"', sLine);
164 if oMatch is not None:
165 sDist = oMatch.group(1).strip();
166 except:
167 pass;
168 if sDist:
169 sVersion += ' / ' + sDist;
170 else:
171 asFiles = \
172 [
173 [ '/etc/debian_version', 'Debian v'],
174 [ '/etc/gentoo-release', '' ],
175 [ '/etc/redhat-release', '' ],
176 [ '/etc/SuSE-release', '' ],
177 ];
178 for sFile, sPrefix in asFiles:
179 if os.path.isfile(sFile):
180 try:
181 oFile = open(sFile);
182 sLine = oFile.readline();
183 oFile.close();
184 except:
185 continue;
186 sLine = sLine.strip()
187 if len(sLine) > 0:
188 sVersion += ' / ' + sPrefix + sLine;
189 break;
190
191 elif sOs == 'solaris':
192 sVersion = platform.version();
193 if os.path.isfile('/etc/release'):
194 try:
195 oFile = open('/etc/release');
196 sLast = oFile.readlines()[-1];
197 oFile.close();
198 sLast = sLast.strip();
199 if len(sLast) > 0:
200 sVersion += ' (' + sLast + ')';
201 except:
202 pass;
203
204 elif sOs == 'darwin':
205 sOsxVersion = platform.mac_ver()[0];
206 codenames = {"4": "Tiger",
207 "5": "Leopard",
208 "6": "Snow Leopard",
209 "7": "Lion",
210 "8": "Mountain Lion",
211 "9": "Mavericks",
212 "10": "Yosemite"}
213 sVersion += ' / OS X ' + sOsxVersion + ' (' + codenames[sOsxVersion.split('.')[1]] + ')'
214
215 return sVersion;
216
217#
218# File system.
219#
220
221def openNoInherit(sFile, sMode = 'r'):
222 """
223 Wrapper around open() that tries it's best to make sure the file isn't
224 inherited by child processes.
225
226 This is a best effort thing at the moment as it doesn't synchronizes with
227 child process spawning in any way. Thus it can be subject to races in
228 multithreaded programs.
229 """
230
231 try:
232 from fcntl import FD_CLOEXEC, F_GETFD, F_SETFD, fcntl; # pylint: disable=F0401
233 except:
234 return open(sFile, sMode);
235
236 oFile = open(sFile, sMode)
237 #try:
238 fcntl(oFile, F_SETFD, fcntl(oFile, F_GETFD) | FD_CLOEXEC);
239 #except:
240 # pass;
241 return oFile;
242
243def noxcptReadLink(sPath, sXcptRet):
244 """
245 No exceptions os.readlink wrapper.
246 """
247 try:
248 sRet = os.readlink(sPath); # pylint: disable=E1101
249 except:
250 sRet = sXcptRet;
251 return sRet;
252
253def readFile(sFile, sMode = 'rb'):
254 """
255 Reads the entire file.
256 """
257 oFile = open(sFile, sMode);
258 sRet = oFile.read();
259 oFile.close();
260 return sRet;
261
262def noxcptReadFile(sFile, sXcptRet, sMode = 'rb'):
263 """
264 No exceptions common.readFile wrapper.
265 """
266 try:
267 sRet = readFile(sFile, sMode);
268 except:
269 sRet = sXcptRet;
270 return sRet;
271
272def noxcptRmDir(sDir, oXcptRet = False):
273 """
274 No exceptions os.rmdir wrapper.
275 """
276 oRet = True;
277 try:
278 os.rmdir(sDir);
279 except:
280 oRet = oXcptRet;
281 return oRet;
282
283def noxcptDeleteFile(sFile, oXcptRet = False):
284 """
285 No exceptions os.remove wrapper.
286 """
287 oRet = True;
288 try:
289 os.remove(sFile);
290 except:
291 oRet = oXcptRet;
292 return oRet;
293
294
295#
296# SubProcess.
297#
298
299def _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs):
300 """
301 If the "executable" is a python script, insert the python interpreter at
302 the head of the argument list so that it will work on systems which doesn't
303 support hash-bang scripts.
304 """
305
306 asArgs = dKeywordArgs.get('args');
307 if asArgs is None:
308 asArgs = aPositionalArgs[0];
309
310 if asArgs[0].endswith('.py'):
311 if sys.executable is not None and len(sys.executable) > 0:
312 asArgs.insert(0, sys.executable);
313 else:
314 asArgs.insert(0, 'python');
315
316 # paranoia...
317 if dKeywordArgs.get('args') is not None:
318 dKeywordArgs['args'] = asArgs;
319 else:
320 aPositionalArgs = (asArgs,) + aPositionalArgs[1:];
321 return None;
322
323def processCall(*aPositionalArgs, **dKeywordArgs):
324 """
325 Wrapper around subprocess.call to deal with its absense in older
326 python versions.
327 Returns process exit code (see subprocess.poll).
328 """
329 assert dKeywordArgs.get('stdout') == None;
330 assert dKeywordArgs.get('stderr') == None;
331 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
332 oProcess = subprocess.Popen(*aPositionalArgs, **dKeywordArgs);
333 return oProcess.wait();
334
335def processOutputChecked(*aPositionalArgs, **dKeywordArgs):
336 """
337 Wrapper around subprocess.check_output to deal with its absense in older
338 python versions.
339 """
340 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
341 oProcess = subprocess.Popen(stdout=subprocess.PIPE, *aPositionalArgs, **dKeywordArgs);
342
343 sOutput, _ = oProcess.communicate();
344 iExitCode = oProcess.poll();
345
346 if iExitCode is not 0:
347 asArgs = dKeywordArgs.get('args');
348 if asArgs is None:
349 asArgs = aPositionalArgs[0];
350 print(sOutput);
351 raise subprocess.CalledProcessError(iExitCode, asArgs);
352
353 return str(sOutput); # str() make pylint happy.
354
355g_fOldSudo = None;
356def _sudoFixArguments(aPositionalArgs, dKeywordArgs, fInitialEnv = True):
357 """
358 Adds 'sudo' (or similar) to the args parameter, whereever it is.
359 """
360
361 # Are we root?
362 fIsRoot = True;
363 try:
364 fIsRoot = os.getuid() == 0; # pylint: disable=E1101
365 except:
366 pass;
367
368 # If not, prepend sudo (non-interactive, simulate initial login).
369 if fIsRoot is not True:
370 asArgs = dKeywordArgs.get('args');
371 if asArgs is None:
372 asArgs = aPositionalArgs[0];
373
374 # Detect old sudo.
375 global g_fOldSudo;
376 if g_fOldSudo is None:
377 try:
378 sVersion = processOutputChecked(['sudo', '-V']);
379 except:
380 sVersion = '1.7.0';
381 sVersion = sVersion.strip().split('\n')[0];
382 sVersion = sVersion.replace('Sudo version', '').strip();
383 g_fOldSudo = len(sVersion) >= 4 \
384 and sVersion[0] == '1' \
385 and sVersion[1] == '.' \
386 and sVersion[2] <= '6' \
387 and sVersion[3] == '.';
388
389 asArgs.insert(0, 'sudo');
390 if not g_fOldSudo:
391 asArgs.insert(1, '-n');
392 if fInitialEnv and not g_fOldSudo:
393 asArgs.insert(1, '-i');
394
395 # paranoia...
396 if dKeywordArgs.get('args') is not None:
397 dKeywordArgs['args'] = asArgs;
398 else:
399 aPositionalArgs = (asArgs,) + aPositionalArgs[1:];
400 return None;
401
402
403def sudoProcessCall(*aPositionalArgs, **dKeywordArgs):
404 """
405 sudo (or similar) + subprocess.call
406 """
407 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
408 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
409 return processCall(*aPositionalArgs, **dKeywordArgs);
410
411def sudoProcessOutputChecked(*aPositionalArgs, **dKeywordArgs):
412 """
413 sudo (or similar) + subprocess.check_output.
414 """
415 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
416 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
417 return processOutputChecked(*aPositionalArgs, **dKeywordArgs);
418
419def sudoProcessOutputCheckedNoI(*aPositionalArgs, **dKeywordArgs):
420 """
421 sudo (or similar) + subprocess.check_output, except '-i' isn't used.
422 """
423 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
424 _sudoFixArguments(aPositionalArgs, dKeywordArgs, False);
425 return processOutputChecked(*aPositionalArgs, **dKeywordArgs);
426
427def sudoProcessPopen(*aPositionalArgs, **dKeywordArgs):
428 """
429 sudo (or similar) + subprocess.Popen.
430 """
431 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
432 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
433 return subprocess.Popen(*aPositionalArgs, **dKeywordArgs);
434
435
436#
437# Generic process stuff.
438#
439
440def processInterrupt(uPid):
441 """
442 Sends a SIGINT or equivalent to interrupt the specified process.
443 Returns True on success, False on failure.
444
445 On Windows hosts this may not work unless the process happens to be a
446 process group leader.
447 """
448 if sys.platform == 'win32':
449 try:
450 win32console.GenerateConsoleCtrlEvent(win32con.CTRL_BREAK_EVENT, uPid); # pylint
451 fRc = True;
452 except:
453 fRc = False;
454 else:
455 try:
456 os.kill(uPid, signal.SIGINT);
457 fRc = True;
458 except:
459 fRc = False;
460 return fRc;
461
462def sendUserSignal1(uPid):
463 """
464 Sends a SIGUSR1 or equivalent to nudge the process into shutting down
465 (VBoxSVC) or something.
466 Returns True on success, False on failure or if not supported (win).
467
468 On Windows hosts this may not work unless the process happens to be a
469 process group leader.
470 """
471 if sys.platform == 'win32':
472 fRc = False;
473 else:
474 try:
475 os.kill(uPid, signal.SIGUSR1); # pylint: disable=E1101
476 fRc = True;
477 except:
478 fRc = False;
479 return fRc;
480
481def processTerminate(uPid):
482 """
483 Terminates the process in a nice manner (SIGTERM or equivalent).
484 Returns True on success, False on failure.
485 """
486 fRc = False;
487 if sys.platform == 'win32':
488 try:
489 hProcess = win32api.OpenProcess(win32con.PROCESS_TERMINATE, False, uPid);
490 except:
491 pass;
492 else:
493 try:
494 win32process.TerminateProcess(hProcess, 0x40010004); # DBG_TERMINATE_PROCESS
495 fRc = True;
496 except:
497 pass;
498 win32api.CloseHandle(hProcess)
499 else:
500 try:
501 os.kill(uPid, signal.SIGTERM);
502 fRc = True;
503 except:
504 pass;
505 return fRc;
506
507def processKill(uPid):
508 """
509 Terminates the process with extreme prejudice (SIGKILL).
510 Returns True on success, False on failure.
511 """
512 if sys.platform == 'win32':
513 fRc = processTerminate(uPid);
514 else:
515 try:
516 os.kill(uPid, signal.SIGKILL); # pylint: disable=E1101
517 fRc = True;
518 except:
519 fRc = False;
520 return fRc;
521
522def processKillWithNameCheck(uPid, sName):
523 """
524 Like processKill(), but checks if the process name matches before killing
525 it. This is intended for killing using potentially stale pid values.
526
527 Returns True on success, False on failure.
528 """
529
530 if processCheckPidAndName(uPid, sName) is not True:
531 return False;
532 return processKill(uPid);
533
534
535def processExists(uPid):
536 """
537 Checks if the specified process exits.
538 This will only work if we can signal/open the process.
539
540 Returns True if it positively exists, False otherwise.
541 """
542 if sys.platform == 'win32':
543 fRc = False;
544 try:
545 hProcess = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, uPid);
546 except:
547 pass;
548 else:
549 win32api.CloseHandle(hProcess)
550 fRc = True;
551 else:
552 try:
553 os.kill(uPid, 0);
554 fRc = True;
555 except:
556 fRc = False;
557 return fRc;
558
559def processCheckPidAndName(uPid, sName):
560 """
561 Checks if a process PID and NAME matches.
562 """
563 fRc = processExists(uPid);
564 if fRc is not True:
565 return False;
566
567 if sys.platform == 'win32':
568 try:
569 from win32com.client import GetObject; # pylint: disable=F0401
570 oWmi = GetObject('winmgmts:');
571 aoProcesses = oWmi.InstancesOf('Win32_Process');
572 for oProcess in aoProcesses:
573 if long(oProcess.Properties_("ProcessId").Value) == uPid:
574 sCurName = oProcess.Properties_("Name").Value;
575 #reporter.log2('uPid=%s sName=%s sCurName=%s' % (uPid, sName, sCurName));
576 sName = sName.lower();
577 sCurName = sCurName.lower();
578 if os.path.basename(sName) == sName:
579 sCurName = os.path.basename(sCurName);
580
581 if sCurName == sName \
582 or sCurName + '.exe' == sName \
583 or sCurName == sName + '.exe':
584 fRc = True;
585 break;
586 except:
587 #reporter.logXcpt('uPid=%s sName=%s' % (uPid, sName));
588 pass;
589 else:
590 if sys.platform in ('linux2', ):
591 asPsCmd = ['/bin/ps', '-p', '%u' % (uPid,), '-o', 'fname='];
592 elif sys.platform in ('sunos5',):
593 asPsCmd = ['/usr/bin/ps', '-p', '%u' % (uPid,), '-o', 'fname='];
594 elif sys.platform in ('darwin',):
595 asPsCmd = ['/bin/ps', '-p', '%u' % (uPid,), '-o', 'ucomm='];
596 else:
597 asPsCmd = None;
598
599 if asPsCmd is not None:
600 try:
601 oPs = subprocess.Popen(asPsCmd, stdout=subprocess.PIPE);
602 sCurName = oPs.communicate()[0];
603 iExitCode = oPs.wait();
604 except:
605 #reporter.logXcpt();
606 return False;
607
608 # ps fails with non-zero exit code if the pid wasn't found.
609 if iExitCode is not 0:
610 return False;
611 if sCurName is None:
612 return False;
613 sCurName = sCurName.strip();
614 if sCurName is '':
615 return False;
616
617 if os.path.basename(sName) == sName:
618 sCurName = os.path.basename(sCurName);
619 elif os.path.basename(sCurName) == sCurName:
620 sName = os.path.basename(sName);
621
622 if sCurName != sName:
623 return False;
624
625 fRc = True;
626 return fRc;
627
628
629class ProcessInfo(object):
630 """Process info."""
631 def __init__(self, iPid):
632 self.iPid = iPid;
633 self.iParentPid = None;
634 self.sImage = None;
635 self.sName = None;
636 self.asArgs = None;
637 self.sCwd = None;
638 self.iGid = None;
639 self.iUid = None;
640 self.iProcGroup = None;
641 self.iSessionId = None;
642
643 def loadAll(self):
644 """Load all the info."""
645 sOs = getHostOs();
646 if sOs == 'linux':
647 sProc = '/proc/%s/' % (self.iPid,);
648 if self.sImage is None: self.sImage = noxcptReadLink(sProc + 'exe', None);
649 if self.sCwd is None: self.sCwd = noxcptReadLink(sProc + 'cwd', None);
650 if self.asArgs is None: self.asArgs = noxcptReadFile(sProc + 'cmdline', '').split('\x00');
651 elif sOs == 'solaris':
652 sProc = '/proc/%s/' % (self.iPid,);
653 if self.sImage is None: self.sImage = noxcptReadLink(sProc + 'path/a.out', None);
654 if self.sCwd is None: self.sCwd = noxcptReadLink(sProc + 'path/cwd', None);
655 else:
656 pass;
657 if self.sName is None and self.sImage is not None:
658 self.sName = self.sImage;
659
660 def windowsGrabProcessInfo(self, oProcess):
661 """Windows specific loadAll."""
662 try: self.sName = oProcess.Properties_("Name").Value;
663 except: pass;
664 try: self.sImage = oProcess.Properties_("ExecutablePath").Value;
665 except: pass;
666 try: self.asArgs = oProcess.Properties_("CommandLine").Value; ## @todo split it.
667 except: pass;
668 try: self.iParentPid = oProcess.Properties_("ParentProcessId").Value;
669 except: pass;
670 try: self.iSessionId = oProcess.Properties_("SessionId").Value;
671 except: pass;
672 if self.sName is None and self.sImage is not None:
673 self.sName = self.sImage;
674
675 def getBaseImageName(self):
676 """
677 Gets the base image name if available, use the process name if not available.
678 Returns image/process base name or None.
679 """
680 sRet = self.sImage if self.sName is None else self.sName;
681 if sRet is None:
682 self.loadAll();
683 sRet = self.sImage if self.sName is None else self.sName;
684 if sRet is None:
685 if self.asArgs is None or len(self.asArgs) == 0:
686 return None;
687 sRet = self.asArgs[0];
688 if len(sRet) == 0:
689 return None;
690 return os.path.basename(sRet);
691
692 def getBaseImageNameNoExeSuff(self):
693 """
694 Same as getBaseImageName, except any '.exe' or similar suffix is stripped.
695 """
696 sRet = self.getBaseImageName();
697 if sRet is not None and len(sRet) > 4 and sRet[-4] == '.':
698 if (sRet[-4:]).lower() in [ '.exe', '.com', '.msc', '.vbs', '.cmd', '.bat' ]:
699 sRet = sRet[:-4];
700 return sRet;
701
702
703def processListAll(): # pylint: disable=R0914
704 """
705 Return a list of ProcessInfo objects for all the processes in the system
706 that the current user can see.
707 """
708 asProcesses = [];
709
710 sOs = getHostOs();
711 if sOs == 'win':
712 from win32com.client import GetObject; # pylint: disable=F0401
713 oWmi = GetObject('winmgmts:');
714 aoProcesses = oWmi.InstancesOf('Win32_Process');
715 for oProcess in aoProcesses:
716 try:
717 iPid = int(oProcess.Properties_("ProcessId").Value);
718 except:
719 continue;
720 oMyInfo = ProcessInfo(iPid);
721 oMyInfo.windowsGrabProcessInfo(oProcess);
722 asProcesses.append(oMyInfo);
723
724 elif sOs in [ 'linux', 'solaris' ]:
725 try:
726 asDirs = os.listdir('/proc');
727 except:
728 asDirs = [];
729 for sDir in asDirs:
730 if sDir.isdigit():
731 asProcesses.append(ProcessInfo(int(sDir),));
732
733 elif sOs == 'darwin':
734 # Try our best to parse ps output. (Not perfect but does the job most of the time.)
735 try:
736 sRaw = processOutputChecked([ '/bin/ps', '-A',
737 '-o', 'pid=',
738 '-o', 'ppid=',
739 '-o', 'pgid=',
740 '-o', 'sess=',
741 '-o', 'uid=',
742 '-o', 'gid=',
743 '-o', 'comm=' ]);
744 except:
745 return asProcesses;
746
747 for sLine in sRaw.split('\n'):
748 sLine = sLine.lstrip();
749 if len(sLine) < 7 or not sLine[0].isdigit():
750 continue;
751
752 iField = 0;
753 off = 0;
754 aoFields = [None, None, None, None, None, None, None];
755 while iField < 7:
756 # Eat whitespace.
757 while off < len(sLine) and (sLine[off] == ' ' or sLine[off] == '\t'):
758 off += 1;
759
760 # Final field / EOL.
761 if iField == 6:
762 aoFields[6] = sLine[off:];
763 break;
764 if off >= len(sLine):
765 break;
766
767 # Generic field parsing.
768 offStart = off;
769 off += 1;
770 while off < len(sLine) and sLine[off] != ' ' and sLine[off] != '\t':
771 off += 1;
772 try:
773 if iField != 3:
774 aoFields[iField] = int(sLine[offStart:off]);
775 else:
776 aoFields[iField] = long(sLine[offStart:off], 16); # sess is a hex address.
777 except:
778 pass;
779 iField += 1;
780
781 if aoFields[0] is not None:
782 oMyInfo = ProcessInfo(aoFields[0]);
783 oMyInfo.iParentPid = aoFields[1];
784 oMyInfo.iProcGroup = aoFields[2];
785 oMyInfo.iSessionId = aoFields[3];
786 oMyInfo.iUid = aoFields[4];
787 oMyInfo.iGid = aoFields[5];
788 oMyInfo.sName = aoFields[6];
789 asProcesses.append(oMyInfo);
790
791 return asProcesses;
792
793
794def processCollectCrashInfo(uPid, fnLog, fnCrashFile):
795 """
796 Looks for information regarding the demise of the given process.
797 """
798 sOs = getHostOs();
799 if sOs == 'darwin':
800 #
801 # On darwin we look for crash and diagnostic reports.
802 #
803 asLogDirs = [
804 u'/Library/Logs/DiagnosticReports/',
805 u'/Library/Logs/CrashReporter/',
806 u'~/Library/Logs/DiagnosticReports/',
807 u'~/Library/Logs/CrashReporter/',
808 ];
809 for sDir in asLogDirs:
810 sDir = os.path.expanduser(sDir);
811 if not os.path.isdir(sDir):
812 continue;
813 try:
814 asDirEntries = os.listdir(sDir);
815 except:
816 continue;
817 for sEntry in asDirEntries:
818 # Only interested in .crash files.
819 _, sSuff = os.path.splitext(sEntry);
820 if sSuff != '.crash':
821 continue;
822
823 # The pid can be found at the end of the first line.
824 sFull = os.path.join(sDir, sEntry);
825 try:
826 oFile = open(sFull, 'r');
827 sFirstLine = oFile.readline();
828 oFile.close();
829 except:
830 continue;
831 if len(sFirstLine) <= 4 or sFirstLine[-2] != ']':
832 continue;
833 offPid = len(sFirstLine) - 3;
834 while offPid > 1 and sFirstLine[offPid - 1].isdigit():
835 offPid -= 1;
836 try: uReportPid = int(sFirstLine[offPid:-2]);
837 except: continue;
838
839 # Does the pid we found match?
840 if uReportPid == uPid:
841 fnLog('Found crash report for %u: %s' % (uPid, sFull,));
842 fnCrashFile(sFull, False);
843 elif sOs == 'win':
844 #
845 # Getting WER reports would be great, however we have trouble match the
846 # PID to those as they seems not to mention it in the brief reports.
847 # Instead we'll just look for crash dumps in C:\CrashDumps (our custom
848 # location - see the windows readme for the testbox script) and what
849 # the MSDN article lists for now.
850 #
851 # It's been observed on Windows server 2012 that the dump files takes
852 # the form: <processimage>.<decimal-pid>.dmp
853 #
854 asDmpDirs = [
855 u'%SystemDrive%/CrashDumps/', # Testboxes.
856 u'%LOCALAPPDATA%/CrashDumps/', # MSDN example.
857 u'%WINDIR%/ServiceProfiles/LocalServices/', # Local and network service.
858 u'%WINDIR%/ServiceProfiles/NetworkSerices/',
859 u'%WINDIR%/ServiceProfiles/',
860 u'%WINDIR%/System32/Config/SystemProfile/', # System services.
861 ];
862 sMatchSuffix = '.%u.dmp' % (uPid,);
863
864 for sDir in asDmpDirs:
865 sDir = os.path.expandvars(sDir);
866 if not os.path.isdir(sDir):
867 continue;
868 try:
869 asDirEntries = os.listdir(sDir);
870 except:
871 continue;
872 for sEntry in asDirEntries:
873 if sEntry.endswith(sMatchSuffix):
874 sFull = os.path.join(sDir, sEntry);
875 fnLog('Found crash dump for %u: %s' % (uPid, sFull,));
876 fnCrashFile(sFull, True);
877
878 else:
879 pass; ## TODO
880 return None;
881
882
883#
884# Time.
885#
886
887def timestampNano():
888 """
889 Gets a nanosecond timestamp.
890 """
891 if sys.platform == 'win32':
892 return long(time.clock() * 1000000000);
893 return long(time.time() * 1000000000);
894
895def timestampMilli():
896 """
897 Gets a millisecond timestamp.
898 """
899 if sys.platform == 'win32':
900 return long(time.clock() * 1000);
901 return long(time.time() * 1000);
902
903def timestampSecond():
904 """
905 Gets a second timestamp.
906 """
907 if sys.platform == 'win32':
908 return long(time.clock());
909 return long(time.time());
910
911def getTimePrefix():
912 """
913 Returns a timestamp prefix, typically used for logging. UTC.
914 """
915 try:
916 oNow = datetime.datetime.utcnow();
917 sTs = '%02u:%02u:%02u.%06u' % (oNow.hour, oNow.minute, oNow.second, oNow.microsecond);
918 except:
919 sTs = 'getTimePrefix-exception';
920 return sTs;
921
922def getTimePrefixAndIsoTimestamp():
923 """
924 Returns current UTC as log prefix and iso timestamp.
925 """
926 try:
927 oNow = datetime.datetime.utcnow();
928 sTsPrf = '%02u:%02u:%02u.%06u' % (oNow.hour, oNow.minute, oNow.second, oNow.microsecond);
929 sTsIso = formatIsoTimestamp(oNow);
930 except:
931 sTsPrf = sTsIso = 'getTimePrefix-exception';
932 return (sTsPrf, sTsIso);
933
934def formatIsoTimestamp(oNow):
935 """Formats the datetime object as an ISO timestamp."""
936 assert oNow.tzinfo is None;
937 sTs = '%s.%09uZ' % (oNow.strftime('%Y-%m-%dT%H:%M:%S'), oNow.microsecond * 1000);
938 return sTs;
939
940def getIsoTimestamp():
941 """Returns the current UTC timestamp as a string."""
942 return formatIsoTimestamp(datetime.datetime.utcnow());
943
944
945def getLocalHourOfWeek():
946 """ Local hour of week (0 based). """
947 oNow = datetime.datetime.now();
948 return (oNow.isoweekday() - 1) * 24 + oNow.hour;
949
950
951def formatIntervalSeconds(cSeconds):
952 """ Format a seconds interval into a nice 01h 00m 22s string """
953 # Two simple special cases.
954 if cSeconds < 60:
955 return '%ss' % (cSeconds,);
956 if cSeconds < 3600:
957 cMins = cSeconds / 60;
958 cSecs = cSeconds % 60;
959 if cSecs == 0:
960 return '%sm' % (cMins,);
961 return '%sm %ss' % (cMins, cSecs,);
962
963 # Generic and a bit slower.
964 cDays = cSeconds / 86400;
965 cSeconds %= 86400;
966 cHours = cSeconds / 3600;
967 cSeconds %= 3600;
968 cMins = cSeconds / 60;
969 cSecs = cSeconds % 60;
970 sRet = '';
971 if cDays > 0:
972 sRet = '%sd ' % (cDays,);
973 if cHours > 0:
974 sRet += '%sh ' % (cHours,);
975 if cMins > 0:
976 sRet += '%sm ' % (cMins,);
977 if cSecs > 0:
978 sRet += '%ss ' % (cSecs,);
979 assert len(sRet) > 0; assert sRet[-1] == ' ';
980 return sRet[:-1];
981
982def formatIntervalSeconds2(oSeconds):
983 """
984 Flexible input version of formatIntervalSeconds for use in WUI forms where
985 data is usually already string form.
986 """
987 if isinstance(oSeconds, int) or isinstance(oSeconds, long):
988 return formatIntervalSeconds(oSeconds);
989 if not isString(oSeconds):
990 try:
991 lSeconds = long(oSeconds);
992 except:
993 pass;
994 else:
995 if lSeconds >= 0:
996 return formatIntervalSeconds2(lSeconds);
997 return oSeconds;
998
999def parseIntervalSeconds(sString):
1000 """
1001 Reverse of formatIntervalSeconds.
1002
1003 Returns (cSeconds, sError), where sError is None on success.
1004 """
1005
1006 # We might given non-strings, just return them without any fuss.
1007 if not isString(sString):
1008 if isinstance(sString, int) or isinstance(sString, long) or sString is None:
1009 return (sString, None);
1010 ## @todo time/date objects?
1011 return (int(sString), None);
1012
1013 # Strip it and make sure it's not empty.
1014 sString = sString.strip();
1015 if len(sString) == 0:
1016 return (0, 'Empty interval string.');
1017
1018 #
1019 # Split up the input into a list of 'valueN, unitN, ...'.
1020 #
1021 # Don't want to spend too much time trying to make re.split do exactly what
1022 # I need here, so please forgive the extra pass I'm making here.
1023 #
1024 asRawParts = re.split(r'\s*([0-9]+)\s*([^0-9,;]*)[\s,;]*', sString);
1025 asParts = [];
1026 for sPart in asRawParts:
1027 sPart = sPart.strip();
1028 if len(sPart) > 0:
1029 asParts.append(sPart);
1030 if len(asParts) == 0:
1031 return (0, 'Empty interval string or something?');
1032
1033 #
1034 # Process them one or two at the time.
1035 #
1036 cSeconds = 0;
1037 asErrors = [];
1038 i = 0;
1039 while i < len(asParts):
1040 sNumber = asParts[i];
1041 i += 1;
1042 if sNumber.isdigit():
1043 iNumber = int(sNumber);
1044
1045 sUnit = 's';
1046 if i < len(asParts) and not asParts[i].isdigit():
1047 sUnit = asParts[i];
1048 i += 1;
1049
1050 sUnitLower = sUnit.lower();
1051 if sUnitLower in [ 's', 'se', 'sec', 'second', 'seconds' ]:
1052 pass;
1053 elif sUnitLower in [ 'm', 'mi', 'min', 'minute', 'minutes' ]:
1054 iNumber *= 60;
1055 elif sUnitLower in [ 'h', 'ho', 'hou', 'hour', 'hours' ]:
1056 iNumber *= 3600;
1057 elif sUnitLower in [ 'd', 'da', 'day', 'days' ]:
1058 iNumber *= 86400;
1059 elif sUnitLower in [ 'w', 'week', 'weeks' ]:
1060 iNumber *= 7 * 86400;
1061 else:
1062 asErrors.append('Unknown unit "%s".' % (sUnit,));
1063 cSeconds += iNumber;
1064 else:
1065 asErrors.append('Bad number "%s".' % (sNumber,));
1066 return (cSeconds, None if len(asErrors) == 0 else ' '.join(asErrors));
1067
1068def formatIntervalHours(cHours):
1069 """ Format a hours interval into a nice 1w 2d 1h string. """
1070 # Simple special cases.
1071 if cHours < 24:
1072 return '%sh' % (cHours,);
1073
1074 # Generic and a bit slower.
1075 cWeeks = cHours / (7 * 24);
1076 cHours %= 7 * 24;
1077 cDays = cHours / 24;
1078 cHours %= 24;
1079 sRet = '';
1080 if cWeeks > 0:
1081 sRet = '%sw ' % (cWeeks,);
1082 if cDays > 0:
1083 sRet = '%sd ' % (cDays,);
1084 if cHours > 0:
1085 sRet += '%sh ' % (cHours,);
1086 assert len(sRet) > 0; assert sRet[-1] == ' ';
1087 return sRet[:-1];
1088
1089def parseIntervalHours(sString):
1090 """
1091 Reverse of formatIntervalHours.
1092
1093 Returns (cHours, sError), where sError is None on success.
1094 """
1095
1096 # We might given non-strings, just return them without any fuss.
1097 if not isString(sString):
1098 if isinstance(sString, int) or isinstance(sString, long) or sString is None:
1099 return (sString, None);
1100 ## @todo time/date objects?
1101 return (int(sString), None);
1102
1103 # Strip it and make sure it's not empty.
1104 sString = sString.strip();
1105 if len(sString) == 0:
1106 return (0, 'Empty interval string.');
1107
1108 #
1109 # Split up the input into a list of 'valueN, unitN, ...'.
1110 #
1111 # Don't want to spend too much time trying to make re.split do exactly what
1112 # I need here, so please forgive the extra pass I'm making here.
1113 #
1114 asRawParts = re.split(r'\s*([0-9]+)\s*([^0-9,;]*)[\s,;]*', sString);
1115 asParts = [];
1116 for sPart in asRawParts:
1117 sPart = sPart.strip();
1118 if len(sPart) > 0:
1119 asParts.append(sPart);
1120 if len(asParts) == 0:
1121 return (0, 'Empty interval string or something?');
1122
1123 #
1124 # Process them one or two at the time.
1125 #
1126 cHours = 0;
1127 asErrors = [];
1128 i = 0;
1129 while i < len(asParts):
1130 sNumber = asParts[i];
1131 i += 1;
1132 if sNumber.isdigit():
1133 iNumber = int(sNumber);
1134
1135 sUnit = 'h';
1136 if i < len(asParts) and not asParts[i].isdigit():
1137 sUnit = asParts[i];
1138 i += 1;
1139
1140 sUnitLower = sUnit.lower();
1141 if sUnitLower in [ 'h', 'ho', 'hou', 'hour', 'hours' ]:
1142 pass;
1143 elif sUnitLower in [ 'd', 'da', 'day', 'days' ]:
1144 iNumber *= 24;
1145 elif sUnitLower in [ 'w', 'week', 'weeks' ]:
1146 iNumber *= 7 * 24;
1147 else:
1148 asErrors.append('Unknown unit "%s".' % (sUnit,));
1149 cHours += iNumber;
1150 else:
1151 asErrors.append('Bad number "%s".' % (sNumber,));
1152 return (cHours, None if len(asErrors) == 0 else ' '.join(asErrors));
1153
1154
1155#
1156# Introspection.
1157#
1158
1159def getCallerName(oFrame=None, iFrame=2):
1160 """
1161 Returns the name of the caller's caller.
1162 """
1163 if oFrame is None:
1164 try:
1165 raise Exception();
1166 except:
1167 oFrame = sys.exc_info()[2].tb_frame.f_back;
1168 while iFrame > 1:
1169 if oFrame is not None:
1170 oFrame = oFrame.f_back;
1171 iFrame = iFrame - 1;
1172 if oFrame is not None:
1173 sName = '%s:%u' % (oFrame.f_code.co_name, oFrame.f_lineno);
1174 return sName;
1175 return "unknown";
1176
1177
1178def getXcptInfo(cFrames = 1):
1179 """
1180 Gets text detailing the exception. (Good for logging.)
1181 Returns list of info strings.
1182 """
1183
1184 #
1185 # Try get exception info.
1186 #
1187 try:
1188 oType, oValue, oTraceback = sys.exc_info();
1189 except:
1190 oType = oValue = oTraceback = None;
1191 if oType is not None:
1192
1193 #
1194 # Try format the info
1195 #
1196 asRet = [];
1197 try:
1198 try:
1199 asRet = asRet + traceback.format_exception_only(oType, oValue);
1200 asTraceBack = traceback.format_tb(oTraceback);
1201 if cFrames is not None and cFrames <= 1:
1202 asRet.append(asTraceBack[-1]);
1203 else:
1204 asRet.append('Traceback:')
1205 for iFrame in range(min(cFrames, len(asTraceBack))):
1206 asRet.append(asTraceBack[-iFrame - 1]);
1207 asRet.append('Stack:')
1208 asRet = asRet + traceback.format_stack(oTraceback.tb_frame.f_back, cFrames);
1209 except:
1210 asRet.append('internal-error: Hit exception #2! %s' % (traceback.format_exc(),));
1211
1212 if len(asRet) == 0:
1213 asRet.append('No exception info...');
1214 except:
1215 asRet.append('internal-error: Hit exception! %s' % (traceback.format_exc(),));
1216 else:
1217 asRet = ['Couldn\'t find exception traceback.'];
1218 return asRet;
1219
1220
1221#
1222# TestSuite stuff.
1223#
1224
1225def isRunningFromCheckout(cScriptDepth = 1):
1226 """
1227 Checks if we're running from the SVN checkout or not.
1228 """
1229
1230 try:
1231 sFile = __file__;
1232 cScriptDepth = 1;
1233 except:
1234 sFile = sys.argv[0];
1235
1236 sDir = os.path.abspath(sFile);
1237 while cScriptDepth >= 0:
1238 sDir = os.path.dirname(sDir);
1239 if os.path.exists(os.path.join(sDir, 'Makefile.kmk')) \
1240 or os.path.exists(os.path.join(sDir, 'Makefile.kup')):
1241 return True;
1242 cScriptDepth -= 1;
1243
1244 return False;
1245
1246
1247#
1248# Bourne shell argument fun.
1249#
1250
1251
1252def argsSplit(sCmdLine):
1253 """
1254 Given a bourne shell command line invocation, split it up into arguments
1255 assuming IFS is space.
1256 Returns None on syntax error.
1257 """
1258 ## @todo bourne shell argument parsing!
1259 return sCmdLine.split(' ');
1260
1261def argsGetFirst(sCmdLine):
1262 """
1263 Given a bourne shell command line invocation, get return the first argument
1264 assuming IFS is space.
1265 Returns None on invalid syntax, otherwise the parsed and unescaped argv[0] string.
1266 """
1267 asArgs = argsSplit(sCmdLine);
1268 if asArgs is None or len(asArgs) == 0:
1269 return None;
1270
1271 return asArgs[0];
1272
1273#
1274# String helpers.
1275#
1276
1277def stricmp(sFirst, sSecond):
1278 """
1279 Compares to strings in an case insensitive fashion.
1280
1281 Python doesn't seem to have any way of doing the correctly, so this is just
1282 an approximation using lower.
1283 """
1284 if sFirst == sSecond:
1285 return 0;
1286 sLower1 = sFirst.lower();
1287 sLower2 = sSecond.lower();
1288 if sLower1 == sLower2:
1289 return 0;
1290 if sLower1 < sLower2:
1291 return -1;
1292 return 1;
1293
1294
1295#
1296# Misc.
1297#
1298
1299def versionCompare(sVer1, sVer2):
1300 """
1301 Compares to version strings in a fashion similar to RTStrVersionCompare.
1302 """
1303
1304 ## @todo implement me!!
1305
1306 if sVer1 == sVer2:
1307 return 0;
1308 if sVer1 < sVer2:
1309 return -1;
1310 return 1;
1311
1312
1313def formatNumber(lNum, sThousandSep = ' '):
1314 """
1315 Formats a decimal number with pretty separators.
1316 """
1317 sNum = str(lNum);
1318 sRet = sNum[-3:];
1319 off = len(sNum) - 3;
1320 while off > 0:
1321 off -= 3;
1322 sRet = sNum[(off if off >= 0 else 0):(off + 3)] + sThousandSep + sRet;
1323 return sRet;
1324
1325
1326def formatNumberNbsp(lNum):
1327 """
1328 Formats a decimal number with pretty separators.
1329 """
1330 sRet = formatNumber(lNum);
1331 return unicode(sRet).replace(' ', u'\u00a0');
1332
1333
1334def isString(oString):
1335 """
1336 Checks if the object is a string object, hiding difference between python 2 and 3.
1337
1338 Returns True if it's a string of some kind.
1339 Returns False if not.
1340 """
1341 if sys.version_info[0] >= 3:
1342 return isinstance(oString, str);
1343 return isinstance(oString, basestring);
1344
1345
1346def hasNonAsciiCharacters(sText):
1347 """
1348 Returns True is specified string has non-ASCII characters.
1349 """
1350 sTmp = unicode(sText, errors='ignore') if isinstance(sText, str) else sText
1351 return not all(ord(cChar) < 128 for cChar in sTmp)
1352
1353
1354def chmodPlusX(sFile):
1355 """
1356 Makes the specified file or directory executable.
1357 Returns success indicator, no exceptions.
1358
1359 Note! Symbolic links are followed and the target will be changed.
1360 """
1361 try:
1362 oStat = os.stat(sFile);
1363 except:
1364 return False;
1365 try:
1366 os.chmod(sFile, oStat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH);
1367 except:
1368 return False;
1369 return True;
1370
1371
1372def unpackFile(sArchive, sDstDir, fnLog, fnError = None):
1373 """
1374 Unpacks the given file if it has a know archive extension, otherwise do
1375 nothing.
1376
1377 Returns list of the extracted files (full path) on success.
1378 Returns empty list if not a supported archive format.
1379 Returns None on failure. Raises no exceptions.
1380 """
1381 if fnError is None:
1382 fnError = fnLog;
1383
1384 asMembers = [];
1385
1386 sBaseNameLower = os.path.basename(sArchive).lower();
1387 if sBaseNameLower.endswith('.zip'):
1388 fnLog('Unzipping "%s" to "%s"...' % (sArchive, sDstDir));
1389 try:
1390 oZipFile = zipfile.ZipFile(sArchive, 'r')
1391 asMembers = oZipFile.namelist();
1392 for sMember in asMembers:
1393 if sMember.endswith('/'):
1394 os.makedirs(os.path.join(sDstDir, sMember.replace('/', os.path.sep)), 0775);
1395 else:
1396 oZipFile.extract(sMember, sDstDir);
1397 oZipFile.close();
1398 except Exception, oXcpt:
1399 fnError('Error unpacking "%s" into "%s": %s' % (sArchive, sDstDir, oXcpt));
1400 return None;
1401
1402 elif sBaseNameLower.endswith('.tar') \
1403 or sBaseNameLower.endswith('.tar.gz') \
1404 or sBaseNameLower.endswith('.tgz') \
1405 or sBaseNameLower.endswith('.tar.bz2'):
1406 fnLog('Untarring "%s" to "%s"...' % (sArchive, sDstDir));
1407 try:
1408 oTarFile = tarfile.open(sArchive, 'r:*');
1409 asMembers = [oTarInfo.name for oTarInfo in oTarFile.getmembers()];
1410 oTarFile.extractall(sDstDir);
1411 oTarFile.close();
1412 except Exception, oXcpt:
1413 fnError('Error unpacking "%s" into "%s": %s' % (sArchive, sDstDir, oXcpt));
1414 return None;
1415
1416 else:
1417 fnLog('Not unpacking "%s".' % (sArchive,));
1418 return [];
1419
1420 #
1421 # Change asMembers to local slashes and prefix with path.
1422 #
1423 asMembersRet = [];
1424 for sMember in asMembers:
1425 asMembersRet.append(os.path.join(sDstDir, sMember.replace('/', os.path.sep)));
1426
1427 return asMembersRet;
1428
1429
1430def getDiskUsage(sPath):
1431 """
1432 Get free space of a partition that corresponds to specified sPath in MB.
1433
1434 Returns partition free space value in MB.
1435 """
1436 if platform.system() == 'Windows':
1437 import ctypes
1438 oCTypeFreeSpace = ctypes.c_ulonglong(0);
1439 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(sPath), None, None,
1440 ctypes.pointer(oCTypeFreeSpace));
1441 cbFreeSpace = oCTypeFreeSpace.value;
1442 else:
1443 oStats = os.statvfs(sPath); # pylint: disable=E1101
1444 cbFreeSpace = long(oStats.f_frsize) * oStats.f_bfree;
1445
1446 # Convert to MB
1447 cMbFreeSpace = long(cbFreeSpace) / (1024 * 1024);
1448
1449 return cMbFreeSpace;
1450
1451
1452#
1453# Unit testing.
1454#
1455
1456# pylint: disable=C0111
1457class BuildCategoryDataTestCase(unittest.TestCase):
1458 def testIntervalSeconds(self):
1459 self.assertEqual(parseIntervalSeconds(formatIntervalSeconds(3600)), (3600, None));
1460 self.assertEqual(parseIntervalSeconds(formatIntervalSeconds(1209438593)), (1209438593, None));
1461 self.assertEqual(parseIntervalSeconds('123'), (123, None));
1462 self.assertEqual(parseIntervalSeconds(123), (123, None));
1463 self.assertEqual(parseIntervalSeconds(99999999999), (99999999999, None));
1464 self.assertEqual(parseIntervalSeconds(''), (0, 'Empty interval string.'));
1465 self.assertEqual(parseIntervalSeconds('1X2'), (3, 'Unknown unit "X".'));
1466 self.assertEqual(parseIntervalSeconds('1 Y3'), (4, 'Unknown unit "Y".'));
1467 self.assertEqual(parseIntervalSeconds('1 Z 4'), (5, 'Unknown unit "Z".'));
1468 self.assertEqual(parseIntervalSeconds('1 hour 2m 5second'), (3725, None));
1469 self.assertEqual(parseIntervalSeconds('1 hour,2m ; 5second'), (3725, None));
1470
1471if __name__ == '__main__':
1472 unittest.main();
1473 # not reached.
1474
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette