Coverage for quibble/__init__.py: 90%

57 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-08-07 07:21 +0000

1# Copyright 2014, 2018 Antoine "hashar" Musso 

2# Copyright 2014, 2018 Wikimedia Foundation Inc. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); 

5# you may not use this file except in compliance with the License. 

6# You may obtain a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15 

16from contextlib import contextmanager 

17from collections import namedtuple 

18import logging 

19import os 

20import time 

21 

22 

23def colored_logging(): 

24 # Color codes http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html 

25 logging.addLevelName( # cyan 

26 logging.DEBUG, 

27 "\033[36m%s\033[0m" % logging.getLevelName(logging.DEBUG), 

28 ) 

29 logging.addLevelName( # green 

30 logging.INFO, "\033[32m%s\033[0m" % logging.getLevelName(logging.INFO) 

31 ) 

32 logging.addLevelName( # yellow 

33 logging.WARNING, 

34 "\033[33m%s\033[0m" % logging.getLevelName(logging.WARNING), 

35 ) 

36 logging.addLevelName( # red 

37 logging.ERROR, 

38 "\033[31m%s\033[0m" % logging.getLevelName(logging.ERROR), 

39 ) 

40 logging.addLevelName( # red background 

41 logging.CRITICAL, 

42 "\033[41m%s\033[0m" % logging.getLevelName(logging.CRITICAL), 

43 ) 

44 

45 

46# Can be used to temporarily alter a logging level. 

47# 

48# with logginglevel('root', logging.ERROR): 

49# do something silently 

50# 

51@contextmanager 

52def logginglevel(name, new_level): 

53 logger = logging.getLogger(name) 

54 prev_level = logger.getEffectiveLevel() 

55 logger.setLevel(new_level) 

56 try: 

57 yield 

58 finally: 

59 logger.setLevel(prev_level) 

60 

61 

62def use_headless(): 

63 log = logging.getLogger('quibble.use_headless') 

64 log.info("Display: %s", os.environ.get('DISPLAY', '<None>')) 

65 

66 return not bool(os.environ.get('DISPLAY')) 

67 

68 

69def chromium_flags(): 

70 args = [] 

71 

72 flags_from_env = os.environ.get('CHROMIUM_FLAGS', None) 

73 if flags_from_env: 

74 args.append(flags_from_env) 

75 

76 # play() would fail if the user didn't interact with the document 

77 # first. The autoplay policy got changed with v66 

78 # https://goo.gl/xX8pDD and T197687 

79 args.append('--autoplay-policy=no-user-gesture-required') 

80 

81 # Chrome throttles calls to history.pushState() which causes the history 

82 # update to be ignored. T198171 

83 args.append('--disable-pushstate-throttle') 

84 

85 if is_in_docker(): 

86 args.append('--no-sandbox') 

87 if use_headless(): 

88 args.extend( 

89 [ 

90 '--headless', 

91 '--disable-gpu', 

92 '--remote-debugging-port=9222', 

93 ] 

94 ) 

95 

96 log = logging.getLogger('quibble.chromium_flags') 

97 log.debug("Flags: %s", args) 

98 return ' '.join(args) 

99 

100 

101def is_in_docker(): 

102 # Note: Also check for the "container" environment variable if running 

103 # under podman. It injects the environment variable since v0.3.4 

104 return os.path.exists('/.dockerenv') or os.getenv('container') 

105 

106 

107def get_npm_command(): 

108 # Allow for overriding npm with e.g. pnpm. 

109 return os.getenv('NPM_COMMAND') or 'npm' 

110 

111 

112# Keep track of Chronometer usage 

113DURATIONS = [] 

114 

115# fmt: off 

116CommandTiming = namedtuple('Timing', [ 

117 'seconds', 

118 'command', 

119]) 

120# fmt: on 

121 

122 

123@contextmanager 

124def Chronometer(name, logger): 

125 """Context wrapper to log duration 

126 

127 Arguments: 

128 - name -- string identifying the command 

129 - logger - logging function to report beginning and completion of the 

130 command. The total duration is reported in seconds. 

131 

132 Durations are globally tracked in the global list quibble.DURATIONS. Each 

133 entry is a `CommandTiming` tuple made of the elapsed time in second and the 

134 command description. 

135 

136 On success the command reports `<<< Finish: ...`. When the wrapped block 

137 raises, it reports `<<< Failed: ...` instead, so the outcome of each 

138 command is machine readable and not only its duration. 

139 """ 

140 start = time.time() 

141 logger('>>> Start: %s' % name) 

142 failed = False 

143 try: 

144 yield 

145 except BaseException: 

146 failed = True 

147 raise 

148 finally: 

149 duration = time.time() - start 

150 outcome = 'Failed' if failed else 'Finish' 

151 logger('<<< %s: %s, in %.03f s' % (outcome, name, duration)) 

152 

153 DURATIONS.append(CommandTiming(command=name, seconds=duration))