Coverage for quibble/__init__.py: 90%
57 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-08-07 07:21 +0000
« 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.
16from contextlib import contextmanager
17from collections import namedtuple
18import logging
19import os
20import time
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 )
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)
62def use_headless():
63 log = logging.getLogger('quibble.use_headless')
64 log.info("Display: %s", os.environ.get('DISPLAY', '<None>'))
66 return not bool(os.environ.get('DISPLAY'))
69def chromium_flags():
70 args = []
72 flags_from_env = os.environ.get('CHROMIUM_FLAGS', None)
73 if flags_from_env:
74 args.append(flags_from_env)
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')
81 # Chrome throttles calls to history.pushState() which causes the history
82 # update to be ignored. T198171
83 args.append('--disable-pushstate-throttle')
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 )
96 log = logging.getLogger('quibble.chromium_flags')
97 log.debug("Flags: %s", args)
98 return ' '.join(args)
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')
107def get_npm_command():
108 # Allow for overriding npm with e.g. pnpm.
109 return os.getenv('NPM_COMMAND') or 'npm'
112# Keep track of Chronometer usage
113DURATIONS = []
115# fmt: off
116CommandTiming = namedtuple('Timing', [
117 'seconds',
118 'command',
119])
120# fmt: on
123@contextmanager
124def Chronometer(name, logger):
125 """Context wrapper to log duration
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.
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.
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))
153 DURATIONS.append(CommandTiming(command=name, seconds=duration))