VirtualBox

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

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

ValidationKit: distinguish between RHEL and OL

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