VirtualBox

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

Last change on this file since 61225 was 61172, checked in by vboxsync, 9 years ago

common/utils.py: Updated getHostOsVersion for OS X 10.11 and 10.12.

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