豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 053cd9a

Browse files
committed
feat(bazel): introduce rule for running integration tests with Bazel
Introduces a rule for running integration tests with Bazel. This is reworked version conceptually matching mostly with the existing rule in the framework repository. The rule has been reworked to be TypeScript-based and to be more maintainable/future-proof by following best-practices as done in all other tools under `dev-infra/`. The tool also has an additional feature, allowing tools to be wired up/aliased within Starlark. This makes it a lot easier to write commands for the integration tests as the rather inconvenient Bazel make location expansion would not be needed. The location expansion also is brittle enough to break later in the test execution where test scripts might call to e.g. `node` or `yarn`.. but these would not be available due to Bazel's environment variable encapsulation/filter. Also the tool mappings can be provided as defaults in a macro, so that integration test authors would not need to bother about Bazel Rules_NodeJS internals.
1 parent 696fe0c commit 053cd9a

File tree

16 files changed

+740
-0
lines changed

16 files changed

+740
-0
lines changed

bazel/integration/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
# Make source files available for distribution via pkg_npm
4+
filegroup(
5+
name = "files",
6+
srcs = glob(["*"]) + [
7+
"//bazel/integration/test_runner:files",
8+
],
9+
)

bazel/integration/index.bzl

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_test")
2+
3+
def _serialize_file(file):
4+
"""Serializes a file into a struct that matches the `BazelFileInfo` type in the
5+
packager implementation. Useful for transmission of such information."""
6+
7+
return struct(path = file.path, shortPath = file.short_path)
8+
9+
def _split_and_expand_command(ctx, command):
10+
"""Splits a command into the binary and its arguments. Also Bazel locations are expanded."""
11+
expanded_command = ctx.expand_location(command, targets = ctx.attr.data)
12+
return expanded_command.split(" ", 1)
13+
14+
def _unwrap_label_keyed_mappings(dict, description):
15+
"""Unwraps a label-keyed dictionary used for expressing mappings into a JSON-serializable
16+
dictionary that will match the `Record<string, BazelFileInfo>` type as in the test
17+
runner. Additionally, the list of referenced mapping files is returned so that these
18+
can be added to the runfiles of the tool relying on the serialized mappings.
19+
20+
This helper is used for serializing the `npm_packages` and `tool_mappings`
21+
dictionaries into JSON that can be passed to the test runner."""
22+
23+
serialized_mappings = {}
24+
referenced_files = []
25+
26+
for target in dict:
27+
name = dict[target]
28+
29+
if not DefaultInfo in target:
30+
fail("Expected %s mapping for %s to have the `DefaultInfo` provider." % (description, target))
31+
32+
files = target[DefaultInfo].files.to_list()
33+
34+
if len(files) != 1:
35+
fail("Expected %s target %s to only have a single file in `DefaultInfo`" % (description, target))
36+
37+
serialized_mappings[name] = _serialize_file(files[0])
38+
referenced_files.append(files[0])
39+
40+
return serialized_mappings, referenced_files
41+
42+
def _integration_test_config_impl(ctx):
43+
"""Implementation of the `_integration_test_config` rule."""
44+
45+
npmPackageMappings, npmPackageFiles = \
46+
_unwrap_label_keyed_mappings(ctx.attr.npm_packages, "NPM package")
47+
toolMappings, toolFiles = _unwrap_label_keyed_mappings(ctx.attr.tool_mappings, "Tool")
48+
49+
config_file = ctx.actions.declare_file("%s.json" % ctx.attr.name)
50+
config = struct(
51+
testPackage = ctx.label.package,
52+
testFiles = [_serialize_file(f) for f in ctx.files.srcs],
53+
commands = [_split_and_expand_command(ctx, c) for c in ctx.attr.commands],
54+
npmPackageMappings = npmPackageMappings,
55+
toolMappings = toolMappings,
56+
)
57+
58+
ctx.actions.write(
59+
output = config_file,
60+
content = json.encode(config),
61+
)
62+
63+
runfiles = [config_file] + ctx.files.data + ctx.files.srcs + npmPackageFiles + toolFiles
64+
65+
return [
66+
DefaultInfo(
67+
files = depset([config_file]),
68+
runfiles = ctx.runfiles(files = runfiles),
69+
),
70+
]
71+
72+
_integration_test_config = rule(
73+
implementation = _integration_test_config_impl,
74+
doc = """Rule which controls the integration test runner by writing a configuration file.""",
75+
attrs = {
76+
"srcs": attr.label_list(
77+
allow_files = True,
78+
mandatory = True,
79+
doc = "Files which need to be available when the integration test commands are invoked.",
80+
),
81+
"data": attr.label_list(
82+
allow_files = True,
83+
doc = """
84+
Files which will be available for runfile resolution. Useful when location
85+
expansion is used in a command.""",
86+
),
87+
"commands": attr.string_list(
88+
mandatory = True,
89+
doc = """
90+
List of commands to run as part of the integration test. The commands can rely on
91+
the global tools made available through the tool mappings.
92+
93+
Commands can also use Bazel make location expansion.""",
94+
),
95+
"npm_packages": attr.label_keyed_string_dict(
96+
allow_files = True,
97+
doc = """
98+
Dictionary of targets which map to NPM packages. This allows for NPM packages
99+
to be mapped to first-party built NPM artifacts.""",
100+
),
101+
"tool_mappings": attr.label_keyed_string_dict(
102+
allow_files = True,
103+
doc = """
104+
Dictionary of targets which map to global tools needed by the integration test.
105+
This allows for binaries like `node` to be made available to the integration test
106+
using the `PATH` environment variable.""",
107+
),
108+
},
109+
)
110+
111+
def integration_test(
112+
name,
113+
srcs,
114+
commands,
115+
npm_packages = {},
116+
tool_mappings = {},
117+
data = [],
118+
tags = [],
119+
**kwargs):
120+
"""Rule that allows for arbitrary commands to be executed within a temporary
121+
directory which will hold the specified test source files."""
122+
123+
config_target = "%s_config" % name
124+
125+
_integration_test_config(
126+
name = config_target,
127+
srcs = srcs,
128+
data = data,
129+
commands = commands,
130+
npm_packages = npm_packages,
131+
tool_mappings = tool_mappings,
132+
tags = tags,
133+
)
134+
135+
nodejs_test(
136+
name = name,
137+
data = ["//bazel/integration/test_runner", ":" + config_target],
138+
templated_args = ["--bazel_patch_module_resolver", "$(rootpath :%s)" % config_target],
139+
entry_point = "//bazel/integration/test_runner:main.ts",
140+
tags = tags,
141+
**kwargs
142+
)

bazel/integration/test/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//bazel/integration:index.bzl", "integration_test")
2+
3+
integration_test(
4+
name = "test",
5+
srcs = ["package.json", "yarn.lock"],
6+
commands = [
7+
"yarn test"
8+
],
9+
tool_mappings = {
10+
"@nodejs//:yarn_bin": "yarn",
11+
"@nodejs//:node_bin": "node",
12+
}
13+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "test",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"scripts": {
6+
"test": "node ./some-test.js"
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('Running test!');

bazel/integration/test/yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@npm//@bazel/typescript:index.bzl", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "test_runner",
7+
srcs = glob(["*.ts"]),
8+
module_name = "@angular/dev-infra-private/bazel/integration/test_runner",
9+
# A tsconfig needs to be specified as otherwise `ts_library` will look for the config
10+
# in `//:package.json` and this breaks when the BUILD file is copied to `@npm//`.
11+
tsconfig = "//:tsconfig.json",
12+
deps = [
13+
"@npm//@bazel/runfiles",
14+
"@npm//@types/node",
15+
"@npm//@types/tmp",
16+
"@npm//tmp",
17+
],
18+
)
19+
20+
# Make source files available for distribution via pkg_npm
21+
filegroup(
22+
name = "files",
23+
srcs = glob(["*"]),
24+
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {debug} from './debug';
10+
import {runfiles} from '@bazel/runfiles';
11+
12+
// Exposing the runfiles to keep the Bazel-specific code local to this file.
13+
export {runfiles};
14+
15+
/**
16+
* Interface describing a file captured in the Bazel action.
17+
* https://docs.bazel.build/versions/main/skylark/lib/File.html.
18+
*/
19+
export interface BazelFileInfo {
20+
/** Execroot-relative path pointing to the file. */
21+
path: string;
22+
/** The path of this file relative to its root. e.g. omitting `bazel-out/<..>/bin`. */
23+
shortPath: string;
24+
}
25+
26+
/** Resolves the specified Bazel file to an absolute disk path. */
27+
export function resolveBazelFile(file: BazelFileInfo): string {
28+
return runfiles.resolveWorkspaceRelative(file.shortPath);
29+
}
30+
31+
/**
32+
* Resolves a binary with respect to the runfiles part of this test. An integration
33+
* test could use a Bazel location substitution within a command. This function ensures
34+
* that the substituted manifest path is then resolved to an absolute path.
35+
*
36+
* e.g. consider a case where a Bazel-built tool, like `$(rootpath @nodejs//:node)` is
37+
* used as binary for the integration test command. This results in a root-relative
38+
* path that we try to resolve here (using the runfiles).
39+
*/
40+
export async function resolveBinaryWithRunfiles(binary: string): Promise<string> {
41+
try {
42+
const resolved = runfiles.resolveWorkspaceRelative(binary);
43+
debug(`Resolved ${binary} to ${resolved} using runfile resolution.`);
44+
return resolved;
45+
} catch {
46+
debug(`Unable to resolve ${binary} with respect to runfiles.`);
47+
return binary;
48+
}
49+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as util from 'util';
10+
11+
/** Function for printing debug messages when working with the test runner. */
12+
export const debug = util.debuglog('ng-integration-test');
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as fs from 'fs';
10+
11+
/** Gets whether the file is executable or not. */
12+
export async function isExecutable(filePath: string): Promise<boolean> {
13+
try {
14+
await fs.promises.access(filePath, fs.constants.X_OK);
15+
return true;
16+
} catch {
17+
return false;
18+
}
19+
}
20+
21+
/** Adds the `write` permission to the given file using `chmod`. */
22+
export async function addWritePermissionFlag(filePath: string) {
23+
if (await isExecutable(filePath)) {
24+
await fs.promises.chmod(filePath, 0o755);
25+
} else {
26+
await fs.promises.chmod(filePath, 0o644);
27+
}
28+
}
29+
30+
/** Writes an executable file to the specified location. */
31+
export async function writeExecutableFile(outputFilePath: string, content: string): Promise<void> {
32+
await fs.promises.writeFile(outputFilePath, content, {mode: 0o755});
33+
}

0 commit comments

Comments
 (0)