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 (
18 from mach
.decorators
import (
25 from mozbuild
.base
import (
29 from mozbuild
import nodeutil
37 DEFAULT_REPO
= "https://github.com/andreastt/puppeteer.git"
38 DEFAULT_COMMITISH
= "firefox"
42 # add node and npm from mozbuild to front of system path
43 npm
, _
= nodeutil
.find_npm_executable()
45 exit(EX_CONFIG
, "could not find npm executable")
46 path
= os
.path
.abspath(os
.path
.join(npm
, os
.pardir
))
47 os
.environ
["PATH"] = "{}:{}".format(path
, os
.environ
["PATH"])
51 class RemoteCommands(MachCommandBase
):
52 def __init__(self
, context
):
53 MachCommandBase
.__init
__(self
, context
)
54 self
.remotedir
= os
.path
.join(self
.topsrcdir
, "remote")
56 @Command("remote", category
="misc",
57 description
="Remote protocol related operations.")
59 self
.parser
.print_usage()
62 @SubCommand("remote", "vendor-puppeteer",
63 "Pull in latest changes of the Puppeteer client.")
64 @CommandArgument("--repository",
67 help="The (possibly remote) repository to clone from. "
68 "Defaults to {}.".format(DEFAULT_REPO
))
69 @CommandArgument("--commitish",
71 default
=DEFAULT_COMMITISH
,
72 help="The commit or tag object name to check out. "
73 "Defaults to \"{}\".".format(DEFAULT_COMMITISH
))
74 def vendor_puppeteer(self
, repository
, commitish
):
75 puppeteerdir
= os
.path
.join(self
.remotedir
, "test", "puppeteer")
77 shutil
.rmtree(puppeteerdir
, ignore_errors
=True)
78 os
.makedirs(puppeteerdir
)
79 with
TemporaryDirectory() as tmpdir
:
80 git("clone", "-q", repository
, tmpdir
)
81 git("checkout", commitish
, worktree
=tmpdir
)
82 git("checkout-index", "-a", "-f",
83 "--prefix", "{}/".format(puppeteerdir
),
86 # remove files which may interfere with git checkout of central
88 os
.remove(os
.path
.join(puppeteerdir
, ".gitattributes"))
89 os
.remove(os
.path
.join(puppeteerdir
, ".gitignore"))
97 "product": "Remote Protocol",
102 "description": "Headless Chrome Node API",
104 "license": "Apache-2.0",
105 "release": commitish
,
108 with
open(os
.path
.join(puppeteerdir
, "moz.yaml"), "w") as fh
:
109 yaml
.safe_dump(annotation
, fh
,
110 default_flow_style
=False,
115 def git(*args
, **kwargs
):
117 if kwargs
.get("worktree"):
118 cmd
+= ("-C", kwargs
["worktree"])
121 pipe
= kwargs
.get("pipe")
122 git_p
= subprocess
.Popen(cmd
,
123 env
={"GIT_CONFIG_NOSYSTEM": "1"},
124 stdout
=subprocess
.PIPE
,
125 stderr
=subprocess
.PIPE
)
128 pipe_p
= subprocess
.Popen(pipe
, stdin
=git_p
.stdout
, stderr
=subprocess
.PIPE
)
131 _
, pipe_err
= pipe_p
.communicate()
132 out
, git_err
= git_p
.communicate()
134 # use error from first program that failed
135 if git_p
.returncode
> 0:
136 exit(EX_SOFTWARE
, git_err
)
137 if pipe
and pipe_p
.returncode
> 0:
138 exit(EX_SOFTWARE
, pipe_err
)
143 def npm(*args
, **kwargs
):
145 if kwargs
.get("env"):
146 env
= os
.environ
.copy()
147 env
.update(kwargs
["env"])
149 p
= subprocess
.Popen(("npm",) + args
,
150 cwd
=kwargs
.get("cwd"),
155 exit(p
.returncode
, "npm: exit code {}".format(p
.returncode
))
158 # tempfile.TemporaryDirectory missing from Python 2.7
159 class TemporaryDirectory(object):
161 self
.path
= tempfile
.mkdtemp()
165 return "<{} {!r}>".format(self
.__class
__.__name
__, self
.path
)
170 def __exit__(self
, exc
, value
, tb
):
177 if self
.path
and not self
._closed
:
178 shutil
.rmtree(self
.path
)
182 class PuppeteerRunner(MozbuildObject
):
183 def __init__(self
, *args
, **kwargs
):
184 super(PuppeteerRunner
, self
).__init
__(*args
, **kwargs
)
186 self
.remotedir
= os
.path
.join(self
.topsrcdir
, "remote")
187 self
.puppeteerdir
= os
.path
.join(self
.remotedir
, "test", "puppeteer")
189 def run_test(self
, *tests
, **params
):
191 Runs Puppeteer unit tests with npm.
193 Possible optional test parameters:
196 Path for the browser binary to use. Defaults to the local
199 Number of tests to run in parallel. Defaults to not
200 parallelise, e.g. `-j1`.
202 Boolean to indicate whether to activate Firefox' headless mode.
204 Dictionary of extra preferences to write to the profile,
205 before invoking npm. Overrides default preferences.
209 binary
= params
.get("binary") or self
.get_binary_path()
210 product
= params
.get("product", "firefox")
212 env
= {"DUMPIO": "1"}
214 for k
, v
in params
.get("extra_launcher_options", {}).items():
215 extra_options
[k
] = json
.loads(v
)
217 if product
== "firefox":
218 env
["BINARY"] = binary
219 command
= ["run", "funit", "--verbose"]
220 elif product
== "chrome":
221 command
= ["run", "unit", "--verbose"]
223 if params
.get("jobs"):
224 env
["PPTR_PARALLEL_TESTS"] = str(params
["jobs"])
225 env
["HEADLESS"] = str(params
.get("headless", False))
228 for k
, v
in params
.get("extra_prefs", {}).items():
229 prefs
[k
] = mozprofile
.Preferences
.cast(v
)
232 extra_options
["extraPrefsFirefox"] = prefs
235 env
["EXTRA_LAUNCH_OPTIONS"] = json
.dumps(extra_options
)
237 return npm(*command
, cwd
=self
.puppeteerdir
, env
=env
)
241 class PuppeteerTest(MachCommandBase
):
242 @Command("puppeteer-test", category
="testing",
243 description
="Run Puppeteer unit tests.")
244 @CommandArgument("--product",
247 choices
=["chrome", "firefox"])
248 @CommandArgument("--binary",
250 help="Path to browser binary. Defaults to local Firefox build.")
251 @CommandArgument("--enable-fission",
253 help="Enable Fission (site isolation) in Gecko.")
254 @CommandArgument("-z", "--headless",
256 help="Run browser in headless mode.")
257 @CommandArgument("--setpref",
260 metavar
="<pref>=<value>",
261 help="Defines additional user preferences.")
262 @CommandArgument("--setopt",
264 dest
="extra_options",
265 metavar
="<option>=<value>",
266 help="Defines additional options for `puppeteer.launch`.")
267 @CommandArgument("-j",
271 help="Optionally run tests in parallel.")
272 @CommandArgument("-v",
276 help="Increase remote agent logging verbosity to include "
277 "debug level messages with -v, and trace messages with -vv.")
278 @CommandArgument("tests", nargs
="*")
279 def puppeteer_test(self
, binary
=None, enable_fission
=False, headless
=False,
280 extra_prefs
=None, extra_options
=None, jobs
=1, verbosity
=0,
281 tests
=None, product
="firefox", **kwargs
):
282 # moztest calls this programmatically with test objects or manifests
283 if "test_objects" in kwargs
and tests
is not None:
284 raise ValueError("Expected either 'test_objects' or 'tests'")
286 if product
!= "firefox" and extra_prefs
is not None:
287 raise ValueError("User preferences are not recognized by %s" % product
)
289 if "test_objects" in kwargs
:
291 for test
in kwargs
["test_objects"]:
292 tests
.append(test
["path"])
295 for s
in (extra_prefs
or []):
298 exit(EX_USAGE
, "syntax error in --setpref={}".format(s
))
299 prefs
[kv
[0]] = kv
[1].strip()
302 for s
in (extra_options
or []):
305 exit(EX_USAGE
, "syntax error in --setopt={}".format(s
))
306 options
[kv
[0]] = kv
[1].strip()
309 prefs
.update({"fission.autostart": True,
310 "dom.serviceWorkers.parent_intercept": True,
311 "browser.tabs.documentchannel": True})
314 prefs
["remote.log.level"] = "Debug"
316 prefs
["remote.log.level"] = "Trace"
318 self
.install_puppeteer(product
)
320 params
= {"binary": binary
,
321 "headless": headless
,
322 "extra_prefs": prefs
,
325 "extra_launcher_options": options
}
326 puppeteer
= self
._spawn
(PuppeteerRunner
)
328 return puppeteer
.run_test(*tests
, **params
)
329 except Exception as e
:
332 def install_puppeteer(self
, product
):
335 if product
!= "chrome":
336 env
["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "1"
338 cwd
=os
.path
.join(self
.topsrcdir
, "remote", "test", "puppeteer"),
342 def exit(code
, error
=None):
343 if error
is not None:
344 if isinstance(error
, Exception):
346 traceback
.print_exc()
348 message
= str(error
).split("\n")[0].strip()
349 print("{}: {}".format(sys
.argv
[0], message
), file=sys
.stderr
)