1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, # You can obtain one at http://mozilla.org/MPL/2.0/.
5 from __future__
import (
17 from mach
.decorators
import (
24 from mozbuild
.base
import (
28 from mozbuild
import nodeutil
36 DEFAULT_REPO
= "https://github.com/andreastt/puppeteer.git"
37 DEFAULT_COMMITISH
= "firefox"
41 # add node and npm from mozbuild to front of system path
42 npm
, _
= nodeutil
.find_npm_executable()
44 exit(EX_CONFIG
, "could not find npm executable")
45 path
= os
.path
.abspath(os
.path
.join(npm
, os
.pardir
))
46 os
.environ
["PATH"] = "{}:{}".format(path
, os
.environ
["PATH"])
50 class RemoteCommands(MachCommandBase
):
51 def __init__(self
, context
):
52 MachCommandBase
.__init
__(self
, context
)
53 self
.remotedir
= os
.path
.join(self
.topsrcdir
, "remote")
55 @Command("remote", category
="misc",
56 description
="Remote protocol related operations.")
58 self
.parser
.print_usage()
61 @SubCommand("remote", "vendor-puppeteer",
62 "Pull in latest changes of the Puppeteer client.")
63 @CommandArgument("--repository",
66 help="The (possibly remote) repository to clone from. "
67 "Defaults to {}.".format(DEFAULT_REPO
))
68 @CommandArgument("--commitish",
70 default
=DEFAULT_COMMITISH
,
71 help="The commit or tag object name to check out. "
72 "Defaults to \"{}\".".format(DEFAULT_COMMITISH
))
73 def vendor_puppeteer(self
, repository
, commitish
):
74 puppeteerdir
= os
.path
.join(self
.remotedir
, "test", "puppeteer")
76 shutil
.rmtree(puppeteerdir
, ignore_errors
=True)
77 os
.makedirs(puppeteerdir
)
78 with
TemporaryDirectory() as tmpdir
:
79 git("clone", "-q", repository
, tmpdir
)
80 git("checkout", commitish
, worktree
=tmpdir
)
81 git("checkout-index", "-a", "-f",
82 "--prefix", "{}/".format(puppeteerdir
),
85 # remove files which may interfere with git checkout of central
87 os
.remove(os
.path
.join(puppeteerdir
, ".gitattributes"))
88 os
.remove(os
.path
.join(puppeteerdir
, ".gitignore"))
96 "product": "Remote Protocol",
101 "description": "Headless Chrome Node API",
103 "license": "Apache-2.0",
104 "release": commitish
,
107 with
open(os
.path
.join(puppeteerdir
, "moz.yaml"), "w") as fh
:
108 yaml
.safe_dump(annotation
, fh
,
109 default_flow_style
=False,
114 def git(*args
, **kwargs
):
116 if kwargs
.get("worktree"):
117 cmd
+= ("-C", kwargs
["worktree"])
120 pipe
= kwargs
.get("pipe")
121 git_p
= subprocess
.Popen(cmd
,
122 env
={"GIT_CONFIG_NOSYSTEM": "1"},
123 stdout
=subprocess
.PIPE
,
124 stderr
=subprocess
.PIPE
)
127 pipe_p
= subprocess
.Popen(pipe
, stdin
=git_p
.stdout
, stderr
=subprocess
.PIPE
)
130 _
, pipe_err
= pipe_p
.communicate()
131 out
, git_err
= git_p
.communicate()
133 # use error from first program that failed
134 if git_p
.returncode
> 0:
135 exit(EX_SOFTWARE
, git_err
)
136 if pipe
and pipe_p
.returncode
> 0:
137 exit(EX_SOFTWARE
, pipe_err
)
142 def npm(*args
, **kwargs
):
144 if kwargs
.get("env"):
145 env
= os
.environ
.copy()
146 env
.update(kwargs
["env"])
148 p
= subprocess
.Popen(("npm",) + args
,
149 cwd
=kwargs
.get("cwd"),
154 exit(p
.returncode
, "npm: exit code {}".format(p
.returncode
))
157 # tempfile.TemporaryDirectory missing from Python 2.7
158 class TemporaryDirectory(object):
160 self
.path
= tempfile
.mkdtemp()
164 return "<{} {!r}>".format(self
.__class
__.__name
__, self
.path
)
169 def __exit__(self
, exc
, value
, tb
):
176 if self
.path
and not self
._closed
:
177 shutil
.rmtree(self
.path
)
181 class PuppeteerRunner(MozbuildObject
):
182 def __init__(self
, *args
, **kwargs
):
183 super(PuppeteerRunner
, self
).__init
__(*args
, **kwargs
)
185 self
.profile
= mozprofile
.Profile()
187 self
.remotedir
= os
.path
.join(self
.topsrcdir
, "remote")
188 self
.puppeteerdir
= os
.path
.join(self
.remotedir
, "test", "puppeteer")
190 def run_test(self
, *tests
, **params
):
192 Runs Puppeteer unit tests with npm.
194 Possible optional test parameters:
197 Path for the browser binary to use. Defaults to the local
200 Number of tests to run in parallel. Defaults to not
201 parallelise, e.g. `-j1`.
203 Boolean to indicate whether to activate Firefox' headless mode.
205 Dictionary of extra preferences to write to the profile,
206 before invoking npm. Overrides default preferences.
210 binary
= params
.get("binary") or self
.get_binary_path()
212 # currently runs against puppeteer-chrome
213 # but future intention is to run against puppeteer-firefox
214 # when it targets the Mozilla remote agent instead of Juggler
215 env
= {"CHROME": binary
,
218 if params
.get("jobs"):
219 env
["PPTR_PARALLEL_TESTS"] = str(params
["jobs"])
221 if params
.get("headless"):
222 env
["MOZ_HEADLESS"] = "1"
224 prefs
= params
.get("extra_prefs", {})
225 for k
, v
in params
.get("extra_prefs", {}).items():
226 prefs
[k
] = mozprofile
.Preferences
.cast(v
)
229 # https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
230 "remote.enabled": True,
231 # https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
232 "browser.dom.window.dump.enabled": True,
235 self
.profile
.set_preferences(prefs
)
237 # PROFILE is a Puppeteer workaround (see ab302d6)
238 # for passing the --profile flag to Firefox
239 env
["PROFILE"] = self
.profile
.profile
241 return npm("run", "unit", "--verbose", *tests
,
242 cwd
=self
.puppeteerdir
,
247 class PuppeteerTest(MachCommandBase
):
248 @Command("puppeteer-test", category
="testing",
249 description
="Run Puppeteer unit tests.")
250 @CommandArgument("--binary",
252 help="Path to Firefox binary. Defaults to local build.")
253 @CommandArgument("-z", "--headless",
255 help="Run browser in headless mode (default).")
256 @CommandArgument("--setpref",
259 metavar
="<pref>=<value>",
260 help="Defines additional user preferences.")
261 @CommandArgument("-j",
265 help="Optionally run tests in parallel.")
266 @CommandArgument("tests", nargs
="*")
267 def puppeteer_test(self
, binary
=None, headless
=True, extra_prefs
=None,
268 jobs
=1, tests
=None, **kwargs
):
269 # moztest calls this programmatically with test objects or manifests
270 if "test_objects" in kwargs
and tests
is not None:
271 raise ValueError("Expected either 'test_objects' or 'tests'")
273 if "test_objects" in kwargs
:
275 for test
in kwargs
["test_objects"]:
276 tests
.append(test
["path"])
279 for s
in (extra_prefs
or []):
282 exit(EX_USAGE
, "syntax error in --setpref={}".format(s
))
283 prefs
[kv
[0]] = kv
[1].strip()
285 self
.install_puppeteer()
287 params
= {"binary": binary
,
288 "headless": headless
,
289 "extra_prefs": prefs
,
291 puppeteer
= self
._spawn
(PuppeteerRunner
)
293 return puppeteer
.run_test(*tests
, **params
)
294 except Exception as e
:
297 def install_puppeteer(self
):
300 cwd
=os
.path
.join(self
.topsrcdir
, "remote", "test", "puppeteer"),
301 env
={"PUPPETEER_SKIP_CHROMIUM_DOWNLOAD": "1"})
304 def exit(code
, error
=None):
305 if error
is not None:
306 if isinstance(error
, Exception):
308 traceback
.print_exc()
310 message
= str(error
).split("\n")[0].strip()
311 print("{}: {}".format(sys
.argv
[0], message
), file=sys
.stderr
)