Install chrome for browser testing
The unzip util is not part of the recipe standard library (called
"recipe_engine"), so I copied it from chromium's util folder (called
"recipe_modules") to our util folder.
The env stuff is done a bit differently than the original code from
gerrit_plugins.py because we are not running karma. Possibly this will
take some adjusting, but according to
https://github.com/GoogleChrome/chrome-launcher#launch-options it should
detect chrome on the PATH without needing to set CHROME_PATH ourselves.
Change-Id: I9a881b0189aab5982f92606971457c48a353219b
diff --git a/recipes/README.recipes.md b/recipes/README.recipes.md
index 762c20f..46529da 100644
--- a/recipes/README.recipes.md
+++ b/recipes/README.recipes.md
@@ -2,9 +2,97 @@
# Repo documentation for [gerrit](https://gerrit.googlesource.com/luci-config.git)
## Table of Contents
+**[Recipe Modules](#Recipe-Modules)**
+ * [zip](#recipe_modules-zip) (Python3 ✅)
+
**[Recipes](#Recipes)**
* [hello_world](#recipes-hello_world)
* [luci-test](#recipes-luci-test) (Python3 ✅)
+ * [zip:examples/full](#recipes-zip_examples_full) (Python3 ✅)
+## Recipe Modules
+
+### *recipe_modules* / [zip](/recipes/recipe_modules/zip)
+
+[DEPS](/recipes/recipe_modules/zip/__init__.py#7): [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+PYTHON_VERSION_COMPATIBILITY: PY2+3
+
+#### **class [ZipApi](/recipes/recipe_modules/zip/api.py#8)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
+
+Provides steps to zip and unzip files.
+
+— **def [directory](/recipes/recipe_modules/zip/api.py#50)(self, step_name, directory, output, comment=None):**
+
+Step to compress a single directory.
+
+Args:
+ step_name: display name of the step.
+ directory: path to a directory to compress, it would become the root of
+ an archive, i.e. |directory|/file.txt would be named 'file.txt' in
+ the archive.
+ output: path to a zip file to create.
+ comment: the archive comment to set on the created ZIP file.
+
+— **def [get\_comment](/recipes/recipe_modules/zip/api.py#93)(self, step_name, zip_file):**
+
+Returns the archive comment from |zip_file|.
+
+Args:
+ step_name: display name of a step.
+ zip_file: path to a zip file to read, should exist.
+
+— **def [make\_package](/recipes/recipe_modules/zip/api.py#11)(self, root, output):**
+
+Returns ZipPackage object that can be used to compress a set of files.
+
+Usage:
+ pkg = api.zip.make_package(root, output)
+ pkg.add_file(root.join('file'))
+ pkg.add_directory(root.join('directory'))
+ yield pkg.zip('zipping step')
+
+Args:
+ root: a directory that would become root of a package, all files added to
+ an archive will have archive paths relative to this directory.
+ output: path to a zip file to create.
+
+Returns:
+ ZipPackage object.
+
+— **def [unzip](/recipes/recipe_modules/zip/api.py#67)(self, step_name, zip_file, output, quiet=False):**
+
+Step to uncompress |zip_file| into |output| directory.
+
+Zip package will be unpacked to |output| so that root of an archive is in
+|output|, i.e. archive.zip/file.txt will become |output|/file.txt.
+
+Step will FAIL if |output| already exists.
+
+Args:
+ step_name: display name of a step.
+ zip_file: path to a zip file to uncompress, should exist.
+ output: path to a directory to unpack to, it should NOT exist.
+ quiet (bool): If True, print terse output instead of the name
+ of each unzipped file.
+
+— **def [update\_package](/recipes/recipe_modules/zip/api.py#30)(self, root, output):**
+
+Returns ZipPackage object that can be used to update an existing package.
+
+Usage:
+ pkg = api.zip.update_package(root, output)
+ pkg.add_file(root.join('file'))
+ pkg.add_directory(root.join('directory'))
+ yield pkg.zip('updating zip step')
+
+Args:
+ root: the root directory for adding new files/dirs to the package; all
+ files/dirs added to an archive will have archive paths relative to
+ this directory.
+ output: path to a zip file to update.
+
+Returns:
+ ZipPackage object.
## Recipes
### *recipes* / [hello\_world](/recipes/recipes/hello_world.py)
@@ -16,16 +104,29 @@
— **def [RunSteps](/recipes/recipes/hello_world.py#11)(api):**
### *recipes* / [luci-test](/recipes/recipes/luci-test.py)
-[DEPS](/recipes/recipes/luci-test.py#7): [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/nodejs][recipe_engine/recipe_modules/nodejs], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+[DEPS](/recipes/recipes/luci-test.py#7): [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/gsutil][depot_tools/recipe_modules/gsutil], [zip](#recipe_modules-zip), [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/nodejs][recipe_engine/recipe_modules/nodejs], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/step][recipe_engine/recipe_modules/step]
PYTHON_VERSION_COMPATIBILITY: PY3
-— **def [RunSteps](/recipes/recipes/luci-test.py#19)(api):**
+— **def [RunSteps](/recipes/recipes/luci-test.py#23)(api):**
+### *recipes* / [zip:examples/full](/recipes/recipe_modules/zip/examples/full.py)
+
+[DEPS](/recipes/recipe_modules/zip/examples/full.py#7): [zip](#recipe_modules-zip), [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+PYTHON_VERSION_COMPATIBILITY: PY2+3
+
+— **def [RunSteps](/recipes/recipe_modules/zip/examples/full.py#17)(api):**
[depot_tools/recipe_modules/bot_update]: https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/e1c8efebe0a3cce42ca46d6057b6d4bd909ad203/recipes/README.recipes.md#recipe_modules-bot_update
[depot_tools/recipe_modules/gclient]: https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/e1c8efebe0a3cce42ca46d6057b6d4bd909ad203/recipes/README.recipes.md#recipe_modules-gclient
+[depot_tools/recipe_modules/gsutil]: https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/e1c8efebe0a3cce42ca46d6057b6d4bd909ad203/recipes/README.recipes.md#recipe_modules-gsutil
[recipe_engine/recipe_modules/buildbucket]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-buildbucket
[recipe_engine/recipe_modules/context]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-context
+[recipe_engine/recipe_modules/file]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-file
+[recipe_engine/recipe_modules/json]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-json
[recipe_engine/recipe_modules/nodejs]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-nodejs
[recipe_engine/recipe_modules/path]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-path
+[recipe_engine/recipe_modules/platform]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-platform
+[recipe_engine/recipe_modules/raw_io]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-raw_io
[recipe_engine/recipe_modules/step]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/README.recipes.md#recipe_modules-step
+[recipe_engine/wkt/RecipeApi]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/a4e5d51c4351ab0674e264a8a360572286b04a6f/recipe_engine/recipe_api.py#886
diff --git a/recipes/recipe_modules/zip/README.md b/recipes/recipe_modules/zip/README.md
new file mode 100644
index 0000000..7af526d
--- /dev/null
+++ b/recipes/recipe_modules/zip/README.md
@@ -0,0 +1 @@
+This module is copied from https://chromium.googlesource.com/infra/infra/+/refs/heads/main/recipes/recipe_modules/zip/
\ No newline at end of file
diff --git a/recipes/recipe_modules/zip/__init__.py b/recipes/recipe_modules/zip/__init__.py
new file mode 100644
index 0000000..26a3c00
--- /dev/null
+++ b/recipes/recipe_modules/zip/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+PYTHON_VERSION_COMPATIBILITY = "PY2+3"
+
+DEPS = [
+ 'recipe_engine/path',
+ 'recipe_engine/platform',
+ 'recipe_engine/json',
+ 'recipe_engine/step',
+ 'recipe_engine/raw_io',
+]
diff --git a/recipes/recipe_modules/zip/api.py b/recipes/recipe_modules/zip/api.py
new file mode 100644
index 0000000..2f6db5a
--- /dev/null
+++ b/recipes/recipe_modules/zip/api.py
@@ -0,0 +1,173 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine import recipe_api
+
+
+class ZipApi(recipe_api.RecipeApi):
+ """Provides steps to zip and unzip files."""
+
+ def make_package(self, root, output):
+ """Returns ZipPackage object that can be used to compress a set of files.
+
+ Usage:
+ pkg = api.zip.make_package(root, output)
+ pkg.add_file(root.join('file'))
+ pkg.add_directory(root.join('directory'))
+ yield pkg.zip('zipping step')
+
+ Args:
+ root: a directory that would become root of a package, all files added to
+ an archive will have archive paths relative to this directory.
+ output: path to a zip file to create.
+
+ Returns:
+ ZipPackage object.
+ """
+ return ZipPackage(self, root, output)
+
+ def update_package(self, root, output):
+ """Returns ZipPackage object that can be used to update an existing package.
+
+ Usage:
+ pkg = api.zip.update_package(root, output)
+ pkg.add_file(root.join('file'))
+ pkg.add_directory(root.join('directory'))
+ yield pkg.zip('updating zip step')
+
+ Args:
+ root: the root directory for adding new files/dirs to the package; all
+ files/dirs added to an archive will have archive paths relative to
+ this directory.
+ output: path to a zip file to update.
+
+ Returns:
+ ZipPackage object.
+ """
+ return ZipPackage(self, root, output, mode='a')
+
+ def directory(self, step_name, directory, output, comment=None):
+ """Step to compress a single directory.
+
+ Args:
+ step_name: display name of the step.
+ directory: path to a directory to compress, it would become the root of
+ an archive, i.e. |directory|/file.txt would be named 'file.txt' in
+ the archive.
+ output: path to a zip file to create.
+ comment: the archive comment to set on the created ZIP file.
+ """
+ pkg = self.make_package(directory, output)
+ pkg.add_directory(directory)
+ if comment:
+ pkg.set_comment(comment)
+ pkg.zip(step_name)
+
+ def unzip(self, step_name, zip_file, output, quiet=False):
+ """Step to uncompress |zip_file| into |output| directory.
+
+ Zip package will be unpacked to |output| so that root of an archive is in
+ |output|, i.e. archive.zip/file.txt will become |output|/file.txt.
+
+ Step will FAIL if |output| already exists.
+
+ Args:
+ step_name: display name of a step.
+ zip_file: path to a zip file to uncompress, should exist.
+ output: path to a directory to unpack to, it should NOT exist.
+ quiet (bool): If True, print terse output instead of the name
+ of each unzipped file.
+ """
+ # TODO(vadimsh): Use 7zip on Windows if available?
+ script_input = {
+ 'output': str(output),
+ 'zip_file': str(zip_file),
+ 'quiet': quiet,
+ }
+ self.m.step(
+ name=step_name,
+ cmd=['python3', self.resource('unzip.py')],
+ stdin=self.m.json.input(script_input))
+
+ def get_comment(self, step_name, zip_file):
+ """Returns the archive comment from |zip_file|.
+
+ Args:
+ step_name: display name of a step.
+ zip_file: path to a zip file to read, should exist.
+ """
+ script_input = {
+ 'zip_file': str(zip_file),
+ }
+ step_result = self.m.step(
+ step_name, ['python3', self.resource('zipcomment.py')],
+ stdout=self.m.raw_io.output_text(),
+ stdin=self.m.json.input(script_input))
+ return step_result.stdout
+
+
+class ZipPackage(object):
+ """Used to gather a list of files to zip."""
+
+ def __init__(self, module, root, output, mode='w'):
+ self._module = module
+ self._root = root
+ self._output = output
+ self._mode = mode
+ self._entries = []
+ self._comment = ''
+
+ @property
+ def root(self):
+ return self._root
+
+ @property
+ def output(self):
+ return self._output
+
+ def set_comment(self, comment):
+ self._comment = comment
+
+ def add_file(self, path, archive_name=None):
+ """Stages single file to be added to the package.
+
+ Args:
+ path: absolute path to a file, should be in |root| subdirectory.
+ archive_name: name of the file in the archive, if non-None
+ """
+ assert self._root.is_parent_of(path), path
+ self._entries.append({
+ 'type': 'file',
+ 'path': str(path),
+ 'archive_name': archive_name
+ })
+
+ def add_directory(self, path):
+ """Stages a directory with all its content to be added to the package.
+
+ Args:
+ path: absolute path to a directory, should be in |root| subdirectory.
+ """
+ # TODO(vadimsh): Implement 'exclude' filter.
+ assert self._root.is_parent_of(path) or path == self._root, path
+ self._entries.append({
+ 'type': 'dir',
+ 'path': str(path),
+ })
+
+ def zip(self, step_name):
+ """Step to zip all staged files."""
+ script_input = {
+ 'entries': self._entries,
+ 'comment': self._comment,
+ 'output': str(self._output),
+ 'root': str(self._root),
+ 'mode': str(self._mode),
+ }
+ step_result = self._module.m.step(
+ name=step_name,
+ cmd=['python3', self._module.resource('zip.py')],
+ stdin=self._module.m.json.input(script_input))
+ self._module.m.path.mock_add_paths(self._output)
+ return step_result
diff --git a/recipes/recipe_modules/zip/examples/full.expected/linux.json b/recipes/recipe_modules/zip/examples/full.expected/linux.json
new file mode 100644
index 0000000..33bca49
--- /dev/null
+++ b/recipes/recipe_modules/zip/examples/full.expected/linux.json
@@ -0,0 +1,115 @@
+[
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/a"
+ ],
+ "name": "touch a"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/b"
+ ],
+ "name": "touch b"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "ensure-directory",
+ "--mode",
+ "0777",
+ "[CLEANUP]/zip-example_tmp_1/sub/dir"
+ ],
+ "infra_step": true,
+ "name": "mkdirs"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/sub/dir/c"
+ ],
+ "name": "touch c"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping",
+ "stdin": "{\"comment\": \"hello\", \"entries\": [{\"path\": \"[CLEANUP]/zip-example_tmp_1\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]/zip-example_tmp_1/output.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping more",
+ "stdin": "{\"comment\": \"\", \"entries\": [{\"archive_name\": null, \"path\": \"[CLEANUP]/zip-example_tmp_1/a\", \"type\": \"file\"}, {\"archive_name\": null, \"path\": \"[CLEANUP]/zip-example_tmp_1/b\", \"type\": \"file\"}, {\"path\": \"[CLEANUP]/zip-example_tmp_1/sub\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]/zip-example_tmp_1/more.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping more updates",
+ "stdin": "{\"comment\": \"hello again\", \"entries\": [{\"archive_name\": \"renamed_a\", \"path\": \"[CLEANUP]/zip-example_tmp_1/update_a\", \"type\": \"file\"}, {\"archive_name\": \"renamed_b\", \"path\": \"[CLEANUP]/zip-example_tmp_1/update_b\", \"type\": \"file\"}], \"mode\": \"a\", \"output\": \"[CLEANUP]/zip-example_tmp_1/more.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ "[CLEANUP]/zip-example_tmp_1/more.zip"
+ ],
+ "name": "report"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/unzip.py"
+ ],
+ "name": "unzipping",
+ "stdin": "{\"output\": \"[CLEANUP]/zip-example_tmp_1/output\", \"quiet\": true, \"zip_file\": \"[CLEANUP]/zip-example_tmp_1/output.zip\"}"
+ },
+ {
+ "cmd": [
+ "find"
+ ],
+ "cwd": "[CLEANUP]/zip-example_tmp_1/output",
+ "name": "listing"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "rmtree",
+ "[CLEANUP]/zip-example_tmp_1"
+ ],
+ "infra_step": true,
+ "name": "cleanup"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zipcomment.py"
+ ],
+ "name": "get comment",
+ "stdin": "{\"zip_file\": \"[CLEANUP]/zip-example_tmp_1/output.zip\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ ""
+ ],
+ "name": "report comment"
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/recipe_modules/zip/examples/full.expected/mac.json b/recipes/recipe_modules/zip/examples/full.expected/mac.json
new file mode 100644
index 0000000..33bca49
--- /dev/null
+++ b/recipes/recipe_modules/zip/examples/full.expected/mac.json
@@ -0,0 +1,115 @@
+[
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/a"
+ ],
+ "name": "touch a"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/b"
+ ],
+ "name": "touch b"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "ensure-directory",
+ "--mode",
+ "0777",
+ "[CLEANUP]/zip-example_tmp_1/sub/dir"
+ ],
+ "infra_step": true,
+ "name": "mkdirs"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]/zip-example_tmp_1/sub/dir/c"
+ ],
+ "name": "touch c"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping",
+ "stdin": "{\"comment\": \"hello\", \"entries\": [{\"path\": \"[CLEANUP]/zip-example_tmp_1\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]/zip-example_tmp_1/output.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping more",
+ "stdin": "{\"comment\": \"\", \"entries\": [{\"archive_name\": null, \"path\": \"[CLEANUP]/zip-example_tmp_1/a\", \"type\": \"file\"}, {\"archive_name\": null, \"path\": \"[CLEANUP]/zip-example_tmp_1/b\", \"type\": \"file\"}, {\"path\": \"[CLEANUP]/zip-example_tmp_1/sub\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]/zip-example_tmp_1/more.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zip.py"
+ ],
+ "name": "zipping more updates",
+ "stdin": "{\"comment\": \"hello again\", \"entries\": [{\"archive_name\": \"renamed_a\", \"path\": \"[CLEANUP]/zip-example_tmp_1/update_a\", \"type\": \"file\"}, {\"archive_name\": \"renamed_b\", \"path\": \"[CLEANUP]/zip-example_tmp_1/update_b\", \"type\": \"file\"}], \"mode\": \"a\", \"output\": \"[CLEANUP]/zip-example_tmp_1/more.zip\", \"root\": \"[CLEANUP]/zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ "[CLEANUP]/zip-example_tmp_1/more.zip"
+ ],
+ "name": "report"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/unzip.py"
+ ],
+ "name": "unzipping",
+ "stdin": "{\"output\": \"[CLEANUP]/zip-example_tmp_1/output\", \"quiet\": true, \"zip_file\": \"[CLEANUP]/zip-example_tmp_1/output.zip\"}"
+ },
+ {
+ "cmd": [
+ "find"
+ ],
+ "cwd": "[CLEANUP]/zip-example_tmp_1/output",
+ "name": "listing"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "rmtree",
+ "[CLEANUP]/zip-example_tmp_1"
+ ],
+ "infra_step": true,
+ "name": "cleanup"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/zipcomment.py"
+ ],
+ "name": "get comment",
+ "stdin": "{\"zip_file\": \"[CLEANUP]/zip-example_tmp_1/output.zip\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ ""
+ ],
+ "name": "report comment"
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/recipe_modules/zip/examples/full.expected/win.json b/recipes/recipe_modules/zip/examples/full.expected/win.json
new file mode 100644
index 0000000..2b10e40
--- /dev/null
+++ b/recipes/recipe_modules/zip/examples/full.expected/win.json
@@ -0,0 +1,115 @@
+[
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]\\zip-example_tmp_1\\a"
+ ],
+ "name": "touch a"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]\\zip-example_tmp_1\\b"
+ ],
+ "name": "touch b"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "ensure-directory",
+ "--mode",
+ "0777",
+ "[CLEANUP]\\zip-example_tmp_1\\sub\\dir"
+ ],
+ "infra_step": true,
+ "name": "mkdirs"
+ },
+ {
+ "cmd": [
+ "touch",
+ "[CLEANUP]\\zip-example_tmp_1\\sub\\dir\\c"
+ ],
+ "name": "touch c"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]\\resources\\zip.py"
+ ],
+ "name": "zipping",
+ "stdin": "{\"comment\": \"hello\", \"entries\": [{\"path\": \"[CLEANUP]\\\\zip-example_tmp_1\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\output.zip\", \"root\": \"[CLEANUP]\\\\zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]\\resources\\zip.py"
+ ],
+ "name": "zipping more",
+ "stdin": "{\"comment\": \"\", \"entries\": [{\"archive_name\": null, \"path\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\a\", \"type\": \"file\"}, {\"archive_name\": null, \"path\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\b\", \"type\": \"file\"}, {\"path\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\sub\", \"type\": \"dir\"}], \"mode\": \"w\", \"output\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\more.zip\", \"root\": \"[CLEANUP]\\\\zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]\\resources\\zip.py"
+ ],
+ "name": "zipping more updates",
+ "stdin": "{\"comment\": \"hello again\", \"entries\": [{\"archive_name\": \"renamed_a\", \"path\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\update_a\", \"type\": \"file\"}, {\"archive_name\": \"renamed_b\", \"path\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\update_b\", \"type\": \"file\"}], \"mode\": \"a\", \"output\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\more.zip\", \"root\": \"[CLEANUP]\\\\zip-example_tmp_1\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ "[CLEANUP]\\zip-example_tmp_1\\more.zip"
+ ],
+ "name": "report"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]\\resources\\unzip.py"
+ ],
+ "name": "unzipping",
+ "stdin": "{\"output\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\output\", \"quiet\": true, \"zip_file\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\output.zip\"}"
+ },
+ {
+ "cmd": [
+ "find"
+ ],
+ "cwd": "[CLEANUP]\\zip-example_tmp_1\\output",
+ "name": "listing"
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "rmtree",
+ "[CLEANUP]\\zip-example_tmp_1"
+ ],
+ "infra_step": true,
+ "name": "cleanup"
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]\\resources\\zipcomment.py"
+ ],
+ "name": "get comment",
+ "stdin": "{\"zip_file\": \"[CLEANUP]\\\\zip-example_tmp_1\\\\output.zip\"}"
+ },
+ {
+ "cmd": [
+ "echo",
+ ""
+ ],
+ "name": "report comment"
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/recipe_modules/zip/examples/full.py b/recipes/recipe_modules/zip/examples/full.py
new file mode 100644
index 0000000..a6b180b
--- /dev/null
+++ b/recipes/recipe_modules/zip/examples/full.py
@@ -0,0 +1,64 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+PYTHON_VERSION_COMPATIBILITY = "PY2+3"
+
+DEPS = [
+ 'recipe_engine/context',
+ 'recipe_engine/file',
+ 'recipe_engine/path',
+ 'recipe_engine/platform',
+ 'recipe_engine/step',
+ 'zip',
+]
+
+
+def RunSteps(api):
+ # Prepare files.
+ temp = api.path.mkdtemp('zip-example')
+ api.step('touch a', ['touch', temp.join('a')])
+ api.step('touch b', ['touch', temp.join('b')])
+ api.file.ensure_directory('mkdirs', temp.join('sub', 'dir'))
+ api.step('touch c', ['touch', temp.join('sub', 'dir', 'c')])
+
+ # Build zip using 'zip.directory'.
+ api.zip.directory('zipping', temp, temp.join('output.zip'), comment='hello')
+
+ # Build a zip using ZipPackage api.
+ package = api.zip.make_package(temp, temp.join('more.zip'))
+ package.add_file(package.root.join('a'))
+ package.add_file(package.root.join('b'))
+ package.add_directory(package.root.join('sub'))
+ package.zip('zipping more')
+
+ # Update a zip using ZipPackage api.
+ package = api.zip.update_package(temp, temp.join('more.zip'))
+ package.add_file(temp.join('update_a'), 'renamed_a')
+ package.add_file(temp.join('update_b'), 'renamed_b')
+ package.set_comment('hello again')
+ package.zip('zipping more updates')
+
+ # Coverage for 'output' property.
+ api.step('report', ['echo', package.output])
+
+ # Unzip the package.
+ api.zip.unzip(
+ 'unzipping', temp.join('output.zip'), temp.join('output'), quiet=True)
+ # List unzipped content.
+ with api.context(cwd=temp.join('output')):
+ api.step('listing', ['find'])
+ # Clean up.
+ api.file.rmtree('cleanup', temp)
+
+ # Retrieve archive comment.
+ comment = api.zip.get_comment('get comment', temp.join('output.zip'))
+ api.step('report comment', ['echo', comment])
+
+
+def GenTests(api):
+ for platform in ('linux', 'win', 'mac'):
+ yield api.test(
+ platform,
+ api.platform.name(platform),
+ )
diff --git a/recipes/recipe_modules/zip/resources/unzip.py b/recipes/recipe_modules/zip/resources/unzip.py
new file mode 100644
index 0000000..5b14082
--- /dev/null
+++ b/recipes/recipe_modules/zip/resources/unzip.py
@@ -0,0 +1,93 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Standalone python script to unzip an archive. Intended to be used by 'zip'
+recipe module internally. Should not be used elsewhere.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import shutil
+import subprocess
+import sys
+import zipfile
+
+
+def unzip_with_subprocess(zip_file, output, quiet):
+ """Unzips an archive using 'zip' utility.
+
+ Works only on Linux and Mac, uses system 'zip' program.
+
+ Args:
+ zip_file: absolute path to an archive to unzip.
+ output: existing directory to unzip to.
+ quiet (bool): If True, instruct the subprocess to unzip with
+ minimal output.
+
+ Returns:
+ Exit code (0 on success).
+ """
+ args = ['unzip']
+ if quiet:
+ args += ['-q']
+ args += [zip_file]
+
+ return subprocess.call(args=args, cwd=output)
+
+
+def unzip_with_python(zip_file, output):
+ """Unzips an archive using 'zipfile' python module.
+
+ Works everywhere where python works (Windows and Posix).
+
+ Args:
+ zip_file: absolute path to an archive to unzip.
+ output: existing directory to unzip to.
+
+ Returns:
+ Exit code (0 on success).
+ """
+ with zipfile.ZipFile(zip_file) as zip_file_obj:
+ for name in zip_file_obj.namelist():
+ print('Extracting %s' % name)
+ zip_file_obj.extract(name, output)
+ return 0
+
+
+def main():
+ # See zip/api.py, def unzip(...) for format of |data|.
+ data = json.load(sys.stdin)
+ output = data['output']
+ zip_file = data['zip_file']
+ quiet = data['quiet']
+
+ # Archive path should exist and be an absolute path to a file.
+ assert os.path.exists(zip_file), zip_file
+ assert os.path.isfile(zip_file), zip_file
+
+ # Output path should be an absolute path, and should NOT exist.
+ assert os.path.isabs(output), output
+ assert not os.path.exists(output), output
+
+ print('Unzipping %s...' % zip_file)
+ exit_code = -1
+ try:
+ os.makedirs(output)
+ if sys.platform == 'win32':
+ # Used on Windows, since there's no builtin 'unzip' utility there.
+ exit_code = unzip_with_python(zip_file, output)
+ else:
+ # On mac and linux 'unzip' utility handles symlink and file modes.
+ exit_code = unzip_with_subprocess(zip_file, output, quiet)
+ finally:
+ # On non-zero exit code or on unexpected exception, clean up.
+ if exit_code:
+ shutil.rmtree(output, ignore_errors=True)
+ return exit_code
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/recipes/recipe_modules/zip/resources/zip.py b/recipes/recipe_modules/zip/resources/zip.py
new file mode 100644
index 0000000..c70db4e
--- /dev/null
+++ b/recipes/recipe_modules/zip/resources/zip.py
@@ -0,0 +1,169 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Standalone python script to zip a set of files. Intended to be used by 'zip'
+recipe module internally. Should not be used elsewhere.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import subprocess
+import sys
+import zipfile
+
+
+def zip_with_subprocess(root, output, entries, comment, mode):
+ """Zips set of files and directories using 'zip' utility.
+
+ Works only on Linux and Mac, uses system 'zip' program.
+
+ Args:
+ root: absolute path to a directory that will become a root of the archive.
+ output: absolute path to a destination archive.
+ entries: list of dicts, describing what to zip, see zip/api.py.
+ comment: archive comment to store in the archive.
+ mode: 'w' to create/overwrite output file, or 'a' to append to output file.
+ Note, if output file doesn't exist, this always creates a new file.
+
+ Returns:
+ Exit code (0 on success).
+ """
+ # Collect paths relative to |root| of all items we'd like to zip.
+ items_to_zip = []
+ for entry in entries:
+ tp = entry['type']
+ path = entry['path']
+ if tp == 'file':
+ # File must exist and be inside |root|.
+ assert os.path.isfile(path), path
+ assert path.startswith(root), path
+ items_to_zip.append(path[len(root):])
+ elif entry['type'] == 'dir':
+ # Append trailing '/'.
+ path = path.rstrip(os.path.sep) + os.path.sep
+ # Directory must exist and be inside |root| or be |root| itself.
+ assert os.path.isdir(path), path
+ assert path.startswith(root), path
+ items_to_zip.append(path[len(root):] or '.')
+ else:
+ raise AssertionError('Invalid entry type: %s' % (tp,))
+
+ # zip defaults to adding/updating files, so explicitly remove any existing
+ # file in 'write' mode.
+ if mode == 'w' and os.path.exists(output):
+ os.unlink(output)
+ # Invoke 'zip' in |root| directory, passing all relative paths via stdin.
+ proc = subprocess.Popen(
+ args=['zip', '-1', '--recurse-paths', '--symlinks', '-@', output],
+ stdin=subprocess.PIPE,
+ universal_newlines=True,
+ cwd=root)
+ proc.communicate('\n'.join(items_to_zip))
+ if proc.returncode == 0 and comment:
+ proc = subprocess.Popen(
+ args=['zip', '--archive-comment', output],
+ stdin=subprocess.PIPE,
+ universal_newlines=True,
+ cwd=root)
+ proc.communicate(comment)
+ return proc.returncode
+
+
+def zip_with_python(root, output, entries, comment, mode):
+ """Zips set of files and directories using 'zipfile' python module.
+
+ Works everywhere where python works (Windows and Posix).
+
+ Args:
+ root: absolute path to a directory that will become a root of the archive.
+ output: absolute path to a destination archive.
+ entries: list of dicts, describing what to zip, see zip/api.py.
+ comment: archive comment to store in the archive.
+ mode: 'w' to create/overwrite output file, or 'a' to append to output file.
+ Note, if output file doesn't exist, this always creates a new file.
+
+ Returns:
+ Exit code (0 on success).
+ """
+ with zipfile.ZipFile(
+ output, mode, zipfile.ZIP_DEFLATED, allowZip64=True) as zip_file:
+
+ def add(path, archive_name):
+ assert path.startswith(root), path
+ # Do not add itself to archive.
+ if path == output:
+ return
+ if archive_name is None:
+ archive_name = path[len(root):]
+ print('Adding %s' % archive_name)
+ zip_file.write(path, archive_name)
+
+ for entry in entries:
+ tp = entry['type']
+ path = entry['path']
+ if tp == 'file':
+ add(path, entry.get('archive_name'))
+ elif tp == 'dir':
+ for cur, _, files in os.walk(path):
+ for name in files:
+ add(os.path.join(cur, name), None)
+ else:
+ raise AssertionError('Invalid entry type: %s' % (tp,))
+ if comment:
+ zip_file.comment = comment
+ return 0
+
+
+def use_python_zip(entries):
+ if sys.platform == 'win32':
+ return True
+ for entry in entries:
+ if entry.get('archive_name') is not None:
+ return True
+ return False
+
+
+def main():
+ # See zip/api.py, def zip(...) for format of |data|.
+ data = json.load(sys.stdin)
+ entries = data['entries']
+ output = data['output']
+ root = data['root'].rstrip(os.path.sep) + os.path.sep
+ mode = data['mode']
+ comment = data['comment']
+
+ # Archive root directory should exist and be an absolute path.
+ assert os.path.exists(root), root
+ assert os.path.isabs(root), root
+
+ # Output zip path should be an absolute path.
+ assert os.path.isabs(output), output
+
+ print('Zipping %s...' % output)
+ exit_code = -1
+ try:
+ if use_python_zip(entries):
+ # Used on Windows, since there's no builtin 'zip' utility there, and when
+ # an explicit archive_name is set, since there's no way to do that with
+ # the native zip utility without filesystem shenanigans
+ exit_code = zip_with_python(root, output, entries, comment, mode)
+ else:
+ # On mac and linux 'zip' utility handles symlink and file modes.
+ exit_code = zip_with_subprocess(root, output, entries, comment, mode)
+ finally:
+ # On non-zero exit code or on unexpected exception, clean up.
+ if exit_code:
+ try:
+ os.remove(output)
+ except: # pylint: disable=bare-except
+ pass
+ if not exit_code:
+ print('Archive size: %.1f KB' % (os.stat(output).st_size / 1024.0,))
+ return exit_code
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/recipes/recipe_modules/zip/resources/zipcomment.py b/recipes/recipe_modules/zip/resources/zipcomment.py
new file mode 100644
index 0000000..b2fc344
--- /dev/null
+++ b/recipes/recipe_modules/zip/resources/zipcomment.py
@@ -0,0 +1,31 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Standalone python script to return the comment from a ZIP archive. Intended
+to be used by 'zip' recipe module internally. Should not be used elsewhere.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import sys
+import zipfile
+
+
+def main():
+ # See zip/api.py, def unzip(...) for format of |data|.
+ data = json.load(sys.stdin)
+ zip_file = data['zip_file']
+
+ # Archive path should exist and be an absolute path to a file.
+ assert os.path.exists(zip_file), zip_file
+ assert os.path.isfile(zip_file), zip_file
+
+ with zipfile.ZipFile(zip_file) as zip_file_obj:
+ sys.stdout.buffer.write(zip_file_obj.comment)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/recipes/recipes/luci-test.expected/basic.json b/recipes/recipes/luci-test.expected/basic.json
index 58c2766..b32453c 100644
--- a/recipes/recipes/luci-test.expected/basic.json
+++ b/recipes/recipes/luci-test.expected/basic.json
@@ -145,6 +145,123 @@
]
},
{
+ "cmd": [],
+ "name": "get chrome"
+ },
+ {
+ "cmd": [
+ "python3",
+ "-u",
+ "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+ "--",
+ "RECIPE_REPO[depot_tools]/gsutil.py",
+ "----",
+ "cp",
+ "gs://chromium-browser-snapshots/Linux_x64/LAST_CHANGE",
+ "[CLEANUP]/chrome_tmp_1"
+ ],
+ "infra_step": true,
+ "luci_context": {
+ "realm": {
+ "name": "gerrit:try"
+ },
+ "resultdb": {
+ "current_invocation": {
+ "name": "invocations/build:8945511751514863184",
+ "update_token": "token"
+ },
+ "hostname": "rdbhost"
+ }
+ },
+ "name": "get chrome.gsutil download",
+ "~followup_annotations": [
+ "@@@STEP_NEST_LEVEL@1@@@"
+ ]
+ },
+ {
+ "cmd": [
+ "vpython3",
+ "-u",
+ "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+ "--json-output",
+ "/path/to/tmp/json",
+ "copy",
+ "[CLEANUP]/chrome_tmp_1/LAST_CHANGE",
+ "/path/to/tmp/"
+ ],
+ "infra_step": true,
+ "luci_context": {
+ "realm": {
+ "name": "gerrit:try"
+ },
+ "resultdb": {
+ "current_invocation": {
+ "name": "invocations/build:8945511751514863184",
+ "update_token": "token"
+ },
+ "hostname": "rdbhost"
+ }
+ },
+ "name": "get chrome.read latest chrome version",
+ "~followup_annotations": [
+ "@@@STEP_NEST_LEVEL@1@@@",
+ "@@@STEP_LOG_END@LAST_CHANGE@@@"
+ ]
+ },
+ {
+ "cmd": [
+ "python3",
+ "-u",
+ "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+ "--",
+ "RECIPE_REPO[depot_tools]/gsutil.py",
+ "----",
+ "cp",
+ "gs://chromium-browser-snapshots/Linux_x64//chrome-linux.zip",
+ "[CLEANUP]/chrome_tmp_1"
+ ],
+ "infra_step": true,
+ "luci_context": {
+ "realm": {
+ "name": "gerrit:try"
+ },
+ "resultdb": {
+ "current_invocation": {
+ "name": "invocations/build:8945511751514863184",
+ "update_token": "token"
+ },
+ "hostname": "rdbhost"
+ }
+ },
+ "name": "get chrome.gsutil download (2)",
+ "~followup_annotations": [
+ "@@@STEP_NEST_LEVEL@1@@@"
+ ]
+ },
+ {
+ "cmd": [
+ "python3",
+ "RECIPE_MODULE[gerrit::zip]/resources/unzip.py"
+ ],
+ "luci_context": {
+ "realm": {
+ "name": "gerrit:try"
+ },
+ "resultdb": {
+ "current_invocation": {
+ "name": "invocations/build:8945511751514863184",
+ "update_token": "token"
+ },
+ "hostname": "rdbhost"
+ }
+ },
+ "name": "get chrome.unzip chrome",
+ "stdin": "{\"output\": \"[CLEANUP]/chrome_tmp_1/zip\", \"quiet\": false, \"zip_file\": \"[CLEANUP]/chrome_tmp_1/chrome-linux.zip\"}",
+ "~followup_annotations": [
+ "@@@STEP_NEST_LEVEL@1@@@"
+ ]
+ },
+ {
"cmd": [
"cipd",
"ensure",
@@ -158,6 +275,9 @@
"/path/to/tmp/json"
],
"cwd": "[CACHE]/builder/src",
+ "env": {
+ "PATH": "[CLEANUP]/chrome_tmp_1/zip/chrome-linux:<PATH>"
+ },
"infra_step": true,
"luci_context": {
"realm": {
@@ -193,6 +313,7 @@
],
"cwd": "[CACHE]/builder/src",
"env": {
+ "PATH": "[CLEANUP]/chrome_tmp_1/zip/chrome-linux:<PATH>",
"npm_config_cache": "[CACHE]/npmcache/npm",
"npm_config_prefix": "[CACHE]/npmcache/pfx"
},
@@ -224,6 +345,7 @@
],
"cwd": "[CACHE]/builder/src",
"env": {
+ "PATH": "[CLEANUP]/chrome_tmp_1/zip/chrome-linux:<PATH>",
"npm_config_cache": "[CACHE]/npmcache/npm",
"npm_config_prefix": "[CACHE]/npmcache/pfx"
},
diff --git a/recipes/recipes/luci-test.py b/recipes/recipes/luci-test.py
index b298cae..cc17a0f 100644
--- a/recipes/recipes/luci-test.py
+++ b/recipes/recipes/luci-test.py
@@ -7,11 +7,15 @@
DEPS = [
'depot_tools/bot_update',
'depot_tools/gclient',
+ 'depot_tools/gsutil',
'recipe_engine/buildbucket',
'recipe_engine/context',
+ 'recipe_engine/file',
+ 'recipe_engine/nodejs',
'recipe_engine/path',
'recipe_engine/step',
- 'recipe_engine/nodejs',
+ 'recipe_engine/platform',
+ 'zip',
]
# Check out the change and run the tests to verify the change as part of Change
@@ -40,8 +44,15 @@
gclient_config=gclient_config)
api.step.raise_on_failure(update_result)
+ # Download Chrome and add to the PATH so that the test runner can launch it
+ # See https://github.com/GoogleChrome/chrome-launcher#launch-options
+ chrome_path = _getChrome(api)
+ env = {
+ 'PATH': api.path.pathsep.join([str(chrome_path), '%(PATH)s']),
+ }
+
# Now in the checked out code directory run our verification.
- with api.context(cwd=api.path['cache'].join('builder').join(s.name)):
+ with api.context(env=env, cwd=api.path['cache'].join('builder').join(s.name)):
# Current LTS for Node.js
with api.nodejs(version='18.11.0'):
# Named steps to test the change
@@ -59,3 +70,20 @@
git_repo="https://gerrit.googlesource.com/luci-test",
),
)
+
+# Download and unzip Chrome from a cloud storage bucket.
+# Copied from https://chromium.googlesource.com/infra/infra/+/refs/heads/main/recipes/recipes/gerrit_plugins.py
+def _getChrome(api):
+ with api.step.nest('get chrome'):
+ chrome = api.path.mkdtemp(prefix='chrome')
+ gs_bucket = 'chromium-browser-snapshots'
+ gs_path = 'Linux_x64'
+ version_file = 'LAST_CHANGE'
+ chrome_zip = 'chrome-linux.zip'
+ api.gsutil.download(gs_bucket, '%s/%s' % (gs_path, version_file), chrome)
+ version = api.file.read_text('read latest chrome version',
+ chrome.join(version_file))
+ api.gsutil.download(gs_bucket, '%s/%s/%s' % (gs_path, version, chrome_zip),
+ chrome)
+ api.zip.unzip('unzip chrome', chrome.join(chrome_zip), chrome.join('zip'))
+ return chrome.join('zip', 'chrome-linux')