blob: 1dfd155561904b0dd1101b229e8fd58943949454 [file] [log] [blame]
# Copyright 2014 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load(
"//go/private:context.bzl",
"go_context",
)
load(
"//go/private:common.bzl",
"as_list",
"asm_exts",
"cgo_exts",
"go_exts",
"pkg_dir",
"split_srcs",
)
load(
"//go/private:go_toolchain.bzl",
"GO_TOOLCHAIN",
)
load(
"//go/private/rules:binary.bzl",
"gc_linkopts",
)
load(
"//go/private:providers.bzl",
"GoArchive",
"GoLibrary",
"GoSource",
"INFERRED_PATH",
"get_archive",
)
load(
"//go/private/rules:transition.bzl",
"go_transition",
)
load(
"//go/private:mode.bzl",
"LINKMODE_NORMAL",
)
load(
"@bazel_skylib//lib:structs.bzl",
"structs",
)
def _go_test_impl(ctx):
"""go_test_impl implements go testing.
It emits an action to run the test generator, and then compiles the
test into a binary."""
go = go_context(ctx)
# Compile the library to test with internal white box tests
internal_library = go.new_library(go, testfilter = "exclude")
internal_source = go.library_to_source(go, ctx.attr, internal_library, ctx.coverage_instrumented())
internal_archive = go.archive(go, internal_source)
go_srcs = split_srcs(internal_source.srcs).go
# Compile the library with the external black box tests
external_library = go.new_library(
go,
name = internal_library.name + "_test",
importpath = internal_library.importpath + "_test",
testfilter = "only",
)
external_source = go.library_to_source(go, struct(
srcs = [struct(files = go_srcs)],
embedsrcs = [struct(files = internal_source.embedsrcs)],
deps = internal_archive.direct + [internal_archive],
x_defs = ctx.attr.x_defs,
), external_library, ctx.coverage_instrumented())
external_source, internal_archive = _recompile_external_deps(go, external_source, internal_archive, [t.label for t in ctx.attr.embed])
external_archive = go.archive(go, external_source)
external_srcs = split_srcs(external_source.srcs).go
# now generate the main function
if ctx.attr.rundir:
if ctx.attr.rundir.startswith("/"):
run_dir = ctx.attr.rundir
else:
run_dir = pkg_dir(ctx.label.workspace_root, ctx.attr.rundir)
else:
run_dir = pkg_dir(ctx.label.workspace_root, ctx.label.package)
main_go = go.declare_file(go, path = "testmain.go")
arguments = go.builder_args(go, "gentestmain")
arguments.add("-output", main_go)
if go.coverage_enabled:
if go.mode.race:
arguments.add("-cover_mode", "atomic")
else:
arguments.add("-cover_mode", "set")
arguments.add("-cover_format", go.cover_format)
arguments.add(
# the l is the alias for the package under test, the l_test must be the
# same with the test suffix
"-import",
"l=" + internal_source.library.importpath,
)
arguments.add(
"-import",
"l_test=" + external_source.library.importpath,
)
arguments.add("-pkgname", internal_source.library.importpath)
arguments.add_all(go_srcs, before_each = "-src", format_each = "l=%s")
ctx.actions.run(
inputs = go_srcs,
outputs = [main_go],
mnemonic = "GoTestGenTest",
executable = go.toolchain._builder,
arguments = [arguments],
)
test_gc_linkopts = gc_linkopts(ctx)
if not go.mode.debug:
# Disable symbol table and DWARF generation for test binaries.
test_gc_linkopts.extend(["-s", "-w"])
# Link in the run_dir global for bzltestutil
test_gc_linkopts.extend(["-X", "github.com/bazelbuild/rules_go/go/tools/bzltestutil.RunDir=" + run_dir])
# Now compile the test binary itself
test_library = GoLibrary(
name = go.label.name + "~testmain",
label = go.label,
importpath = "testmain",
importmap = "testmain",
importpath_aliases = (),
pathtype = INFERRED_PATH,
is_main = True,
resolve = None,
)
test_deps = external_archive.direct + [external_archive] + ctx.attr._testmain_additional_deps
if ctx.configuration.coverage_enabled:
test_deps.append(go.coverdata)
test_source = go.library_to_source(go, struct(
srcs = [struct(files = [main_go])],
deps = test_deps,
), test_library, False)
test_archive, executable, runfiles = go.binary(
go,
name = ctx.label.name,
source = test_source,
test_archives = [internal_archive.data],
gc_linkopts = test_gc_linkopts,
version_file = ctx.version_file,
info_file = ctx.info_file,
)
env = {}
for k, v in ctx.attr.env.items():
env[k] = ctx.expand_location(v, ctx.attr.data)
# Bazel only looks for coverage data if the test target has an
# InstrumentedFilesProvider. If the provider is found and at least one
# source file is present, Bazel will set the COVERAGE_OUTPUT_FILE
# environment variable during tests and will save that file to the build
# events + test outputs.
return [
test_archive,
DefaultInfo(
files = depset([executable]),
runfiles = runfiles,
executable = executable,
),
OutputGroupInfo(
compilation_outputs = [internal_archive.data.file],
),
coverage_common.instrumented_files_info(
ctx,
source_attributes = ["srcs"],
dependency_attributes = ["deps", "embed"],
extensions = ["go"],
),
testing.TestEnvironment(env),
]
_go_test_kwargs = {
"implementation": _go_test_impl,
"attrs": {
"data": attr.label_list(
allow_files = True,
doc = """List of files needed by this rule at run-time. This may include data files
needed or other programs that may be executed. The [bazel] package may be
used to locate run files; they may appear in different places depending on the
operating system and environment. See [data dependencies] for more
information on data files.
""",
),
"srcs": attr.label_list(
allow_files = go_exts + asm_exts + cgo_exts,
doc = """The list of Go source files that are compiled to create the package.
Only `.go` and `.s` files are permitted, unless the `cgo`
attribute is set, in which case,
`.c .cc .cpp .cxx .h .hh .hpp .hxx .inc .m .mm`
files are also permitted. Files may be filtered at build time
using Go [build constraints].
""",
),
"deps": attr.label_list(
providers = [GoLibrary],
doc = """List of Go libraries this test imports directly.
These may be go_library rules or compatible rules with the [GoLibrary] provider.
""",
cfg = go_transition,
),
"embed": attr.label_list(
providers = [GoLibrary],
doc = """List of Go libraries whose sources should be compiled together with this
package's sources. Labels listed here must name `go_library`,
`go_proto_library`, or other compatible targets with the [GoLibrary] and
[GoSource] providers. Embedded libraries must have the same `importpath` as
the embedding library. At most one embedded library may have `cgo = True`,
and the embedding library may not also have `cgo = True`. See [Embedding]
for more information.
""",
cfg = go_transition,
),
"embedsrcs": attr.label_list(
allow_files = True,
doc = """The list of files that may be embedded into the compiled package using
`//go:embed` directives. All files must be in the same logical directory
or a subdirectory as source files. All source files containing `//go:embed`
directives must be in the same logical directory. It's okay to mix static and
generated source files and static and generated embeddable files.
""",
),
"env": attr.string_dict(
doc = """Environment variables to set for the test execution.
The values (but not keys) are subject to
[location expansion](https://docs.bazel.build/versions/main/skylark/macros.html) but not full
[make variable expansion](https://docs.bazel.build/versions/main/be/make-variables.html).
""",
),
"importpath": attr.string(
doc = """The import path of this test. Tests can't actually be imported, but this
may be used by [go_path] and other tools to report the location of source
files. This may be inferred from embedded libraries.
""",
),
"gc_goopts": attr.string_list(
doc = """List of flags to add to the Go compilation command when using the gc compiler.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
""",
),
"gc_linkopts": attr.string_list(
doc = """List of flags to add to the Go link command when using the gc compiler.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
""",
),
"rundir": attr.string(
doc = """ A directory to cd to before the test is run.
This should be a path relative to the execution dir of the test.
The default behaviour is to change to the workspace relative path, this replicates the normal
behaviour of `go test` so it is easy to write compatible tests.
Setting it to `.` makes the test behave the normal way for a bazel test.
***Note:*** This defaults to the package path.
""",
),
"x_defs": attr.string_dict(
doc = """Map of defines to add to the go link command.
See [Defines and stamping] for examples of how to use these.
""",
),
"linkmode": attr.string(
default = LINKMODE_NORMAL,
doc = """Determines how the binary should be built and linked. This accepts some of
the same values as `go build -buildmode` and works the same way.
<br><br>
<ul>
<li>`normal`: Builds a normal executable with position-dependent code.</li>
<li>`pie`: Builds a position-independent executable.</li>
<li>`plugin`: Builds a shared library that can be loaded as a Go plugin. Only supported on platforms that support plugins.</li>
<li>`c-shared`: Builds a shared library that can be linked into a C program.</li>
<li>`c-archive`: Builds an archive that can be linked into a C program.</li>
</ul>
""",
),
"cgo": attr.bool(
doc = """
If `True`, the package may contain [cgo] code, and `srcs` may contain
C, C++, Objective-C, and Objective-C++ files and non-Go assembly files.
When cgo is enabled, these files will be compiled with the C/C++ toolchain
and included in the package. Note that this attribute does not force cgo
to be enabled. Cgo is enabled for non-cross-compiling builds when a C/C++
toolchain is configured.
""",
),
"cdeps": attr.label_list(
doc = """The list of other libraries that the c code depends on.
This can be anything that would be allowed in [cc_library deps]
Only valid if `cgo` = `True`.
""",
),
"cppopts": attr.string_list(
doc = """List of flags to add to the C/C++ preprocessor command.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
Only valid if `cgo` = `True`.
""",
),
"copts": attr.string_list(
doc = """List of flags to add to the C compilation command.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
Only valid if `cgo` = `True`.
""",
),
"cxxopts": attr.string_list(
doc = """List of flags to add to the C++ compilation command.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
Only valid if `cgo` = `True`.
""",
),
"clinkopts": attr.string_list(
doc = """List of flags to add to the C link command.
Subject to ["Make variable"] substitution and [Bourne shell tokenization].
Only valid if `cgo` = `True`.
""",
),
"pure": attr.string(
default = "auto",
doc = """Controls whether cgo source code and dependencies are compiled and linked,
similar to setting `CGO_ENABLED`. May be one of `on`, `off`,
or `auto`. If `auto`, pure mode is enabled when no C/C++
toolchain is configured or when cross-compiling. It's usually better to
control this on the command line with
`--@io_bazel_rules_go//go/config:pure`. See [mode attributes], specifically
[pure].
""",
),
"static": attr.string(
default = "auto",
doc = """Controls whether a binary is statically linked. May be one of `on`,
`off`, or `auto`. Not available on all platforms or in all
modes. It's usually better to control this on the command line with
`--@io_bazel_rules_go//go/config:static`. See [mode attributes],
specifically [static].
""",
),
"race": attr.string(
default = "auto",
doc = """Controls whether code is instrumented for race detection. May be one of
`on`, `off`, or `auto`. Not available when cgo is
disabled. In most cases, it's better to control this on the command line with
`--@io_bazel_rules_go//go/config:race`. See [mode attributes], specifically
[race].
""",
),
"msan": attr.string(
default = "auto",
doc = """Controls whether code is instrumented for memory sanitization. May be one of
`on`, `off`, or `auto`. Not available when cgo is
disabled. In most cases, it's better to control this on the command line with
`--@io_bazel_rules_go//go/config:msan`. See [mode attributes], specifically
[msan].
""",
),
"gotags": attr.string_list(
doc = """Enables a list of build tags when evaluating [build constraints]. Useful for
conditional compilation.
""",
),
"goos": attr.string(
default = "auto",
doc = """Forces a binary to be cross-compiled for a specific operating system. It's
usually better to control this on the command line with `--platforms`.
This disables cgo by default, since a cross-compiling C/C++ toolchain is
rarely available. To force cgo, set `pure` = `off`.
See [Cross compilation] for more information.
""",
),
"goarch": attr.string(
default = "auto",
doc = """Forces a binary to be cross-compiled for a specific architecture. It's usually
better to control this on the command line with `--platforms`.
This disables cgo by default, since a cross-compiling C/C++ toolchain is
rarely available. To force cgo, set `pure` = `off`.
See [Cross compilation] for more information.
""",
),
"_go_context_data": attr.label(default = "//:go_context_data", cfg = go_transition),
"_testmain_additional_deps": attr.label_list(
providers = [GoLibrary],
default = ["//go/tools/bzltestutil"],
cfg = go_transition,
),
# Workaround for bazelbuild/bazel#6293. See comment in lcov_merger.sh.
"_lcov_merger": attr.label(
executable = True,
default = "//go/tools/builders:lcov_merger",
cfg = "target",
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
"executable": True,
"test": True,
"toolchains": [GO_TOOLCHAIN],
"doc": """This builds a set of tests that can be run with `bazel test`.<br><br>
To run all tests in the workspace, and print output on failure (the
equivalent of `go test ./...`), run<br>
```
bazel test --test_output=errors //...
```<br><br>
To run a Go benchmark test, run<br>
```
bazel run //path/to:test -- -test.bench=.
```<br><br>
You can run specific tests by passing the `--test_filter=pattern
<test_filter_>` argument to Bazel. You can pass arguments to tests by passing
`--test_arg=arg <test_arg_>` arguments to Bazel, and you can set environment
variables in the test environment by passing
`--test_env=VAR=value <test_env_>`. You can terminate test execution after the first
failure by passing the `--test_runner_fast_fast <test_runner_fail_fast_>` argument
to Bazel. This is equivalent to passing `--test_arg=-failfast <test_arg_>`.<br><br>
To write structured testlog information to Bazel's `XML_OUTPUT_FILE`, tests
ran with `bazel test` execute using a wrapper. This functionality can be
disabled by setting `GO_TEST_WRAP=0` in the test environment. Additionally,
the testbinary can be invoked with `-test.v` by setting
`GO_TEST_WRAP_TESTV=1` in the test environment; this will result in the
`XML_OUTPUT_FILE` containing more granular data.<br><br>
***Note:*** To interoperate cleanly with old targets generated by [Gazelle], `name`
should be `go_default_test` for internal tests and
`go_default_xtest` for external tests. Gazelle now generates
the name based on the last component of the path. For example, a test
in `//foo/bar` is named `bar_test`, and uses internal and external
sources.
""",
}
go_test = rule(**_go_test_kwargs)
def _recompile_external_deps(go, external_source, internal_archive, library_labels):
"""Recompiles some archives in order to split internal and external tests.
go_test, like 'go test', splits tests into two separate archives: an
internal archive ('package foo') and an external archive
('package foo_test'). The library under test is embedded into the internal
archive. The external archive may import it and may depend on symbols
defined in the internal test files.
To avoid conflicts, the library under test must not be linked into the test
binary, since the internal test archive embeds the same sources.
Libraries imported by the external test that transitively import the
library under test must be recompiled too, or the linker will complain that
export data they were compiled with doesn't match the export data they
are linked with.
This function identifies which archives may need to be recompiled, then
declares new output files and actions to recompile them. This is an
unfortunately an expensive process requiring O(V+E) time and space in the
size of the test's dependency graph for each test.
Args:
go: go object returned by go_context.
external_source: GoSource for the external archive.
internal_archive: GoArchive for the internal archive.
library_labels: labels for embedded libraries under test.
Returns:
external_soruce: recompiled GoSource for the external archive. If no
recompilation is needed, the original GoSource is returned.
internal_archive: recompiled GoArchive for the internal archive. If no
recompilation is needed, the original GoSource is returned.
"""
# If no libraries are embedded in the internal archive, then nothing needs
# to be recompiled.
if not library_labels:
return external_source, internal_archive
# Build a map from labels to GoArchiveData.
# If none of the librares embedded in the internal archive are in the
# dependency graph, then nothing needs to be recompiled.
arc_data_list = depset(transitive = [get_archive(dep).transitive for dep in external_source.deps]).to_list()
label_to_arc_data = {a.label: a for a in arc_data_list}
if all([l not in label_to_arc_data for l in library_labels]):
return external_source, internal_archive
# Build a depth-first post-order list of dependencies starting with the
# external archive. Each archive appears after its dependencies and before
# its dependents.
#
# This is tricky because Starlark doesn't support recursion or while loops.
# We simulate a while loop by iterating over a list of 2N elements where
# N is the number of archives. Each archive is pushed onto the stack
# twice: once before its dependencies are pushed, and once after.
# dep_list is the post-order list of dependencies we're building.
dep_list = []
# stack is a stack of targets to process. We're done when it's empty.
stack = [get_archive(dep).data.label for dep in external_source.deps]
# deps_pushed tracks the status of each target.
# DEPS_UNPROCESSED means the target is on the stack, but its dependencies
# are not.
# ON_DEP_LIST means the target and its dependencies have been added to
# dep_list.
# Non-negative integers are the number of dependencies on the stack that
# still need to be processed.
# A target is on the stack if its status is DEPS_UNPROCESSED or 0.
DEPS_UNPROCESSED = -1
ON_DEP_LIST = -2
deps_pushed = {l: DEPS_UNPROCESSED for l in stack}
# dependents maps labels to lists of known dependents. When a target is
# processed, its dependents' deps_pushed count is deprecated.
dependents = {l: [] for l in stack}
# step is a list to iterate over to simulate a while loop. i tracks
# iterations.
step = [None] * (2 * len(arc_data_list))
i = 0
for _ in step:
if len(stack) == 0:
break
i += 1
label = stack.pop()
if deps_pushed[label] == 0:
# All deps have been added to dep_list. Append this target to the
# list. If a dependent is not waiting for anything else, push
# it back onto the stack.
dep_list.append(label)
for p in dependents.get(label, []):
deps_pushed[p] -= 1
if deps_pushed[p] == 0:
stack.append(p)
continue
# deps_pushed[label] == None, indicating we don't know whether this
# targets dependencies have been processed. Other targets processed
# earlier may depend on them.
deps_pushed[label] = 0
arc_data = label_to_arc_data[label]
for c in arc_data._dep_labels:
if c not in deps_pushed:
# Dependency not seen yet; push it.
stack.append(c)
deps_pushed[c] = None
deps_pushed[label] += 1
dependents[c] = [label]
elif deps_pushed[c] != 0:
# Dependency pushed, not processed; wait for it.
deps_pushed[label] += 1
dependents[c].append(label)
if deps_pushed[label] == 0:
# No dependencies to wait for; push self.
stack.append(label)
if i != len(step):
fail("assertion failed: iterated %d times instead of %d" % (i, len(step)))
# Determine which dependencies need to be recompiled because they depend
# on embedded libraries.
need_recompile = {}
for label in dep_list:
arc_data = label_to_arc_data[label]
need_recompile[label] = any([
dep in library_labels or need_recompile[dep]
for dep in arc_data._dep_labels
])
# Recompile the internal archive without dependencies that need
# recompilation. This breaks a cycle which occurs because the deps list
# is shared between the internal and external archive. The internal archive
# can't import anything that imports itself.
internal_source = internal_archive.source
internal_deps = [dep for dep in internal_source.deps if not need_recompile[get_archive(dep).data.label]]
x_defs = dict(internal_source.x_defs)
x_defs.update(internal_archive.x_defs)
attrs = structs.to_dict(internal_source)
attrs["deps"] = internal_deps
attrs["x_defs"] = x_defs
internal_source = GoSource(**attrs)
internal_archive = go.archive(go, internal_source, _recompile_suffix = ".recompileinternal")
# Build a map from labels to possibly recompiled GoArchives.
label_to_archive = {}
i = 0
for label in dep_list:
i += 1
recompile_suffix = ".recompile%d" % i
# If this library is the internal archive, use the recompiled version.
if label == internal_archive.data.label:
label_to_archive[label] = internal_archive
continue
# If this is a library embedded into the internal test archive,
# use the internal test archive instead.
if label in library_labels:
label_to_archive[label] = internal_archive
continue
# Create a stub GoLibrary and GoSource from the archive data.
arc_data = label_to_arc_data[label]
library = GoLibrary(
name = arc_data.name,
label = arc_data.label,
importpath = arc_data.importpath,
importmap = arc_data.importmap,
importpath_aliases = arc_data.importpath_aliases,
pathtype = arc_data.pathtype,
resolve = None,
testfilter = None,
is_main = False,
)
deps = [label_to_archive[d] for d in arc_data._dep_labels]
source = GoSource(
library = library,
mode = go.mode,
srcs = as_list(arc_data.srcs),
orig_srcs = as_list(arc_data.orig_srcs),
orig_src_map = dict(zip(arc_data.srcs, arc_data._orig_src_map)),
cover = arc_data._cover,
embedsrcs = as_list(arc_data._embedsrcs),
x_defs = dict(arc_data._x_defs),
deps = deps,
gc_goopts = as_list(arc_data._gc_goopts),
runfiles = go._ctx.runfiles(files = arc_data.data_files),
cgo = arc_data._cgo,
cdeps = as_list(arc_data._cdeps),
cppopts = as_list(arc_data._cppopts),
copts = as_list(arc_data._copts),
cxxopts = as_list(arc_data._cxxopts),
clinkopts = as_list(arc_data._clinkopts),
cgo_exports = as_list(arc_data._cgo_exports),
)
# If this archive needs to be recompiled, use go.archive.
# Otherwise, create a stub GoArchive, using the original file.
if need_recompile[label]:
recompile_suffix = ".recompile%d" % i
archive = go.archive(go, source, _recompile_suffix = recompile_suffix)
else:
archive = GoArchive(
source = source,
data = arc_data,
direct = deps,
libs = depset(direct = [arc_data.file], transitive = [a.libs for a in deps]),
transitive = depset(direct = [arc_data], transitive = [a.transitive for a in deps]),
x_defs = source.x_defs,
cgo_deps = depset(direct = arc_data._cgo_deps, transitive = [a.cgo_deps for a in deps]),
cgo_exports = depset(direct = list(source.cgo_exports), transitive = [a.cgo_exports for a in deps]),
runfiles = source.runfiles,
mode = go.mode,
)
label_to_archive[label] = archive
# Finally, we need to replace external_source.deps with the recompiled
# archives.
attrs = structs.to_dict(external_source)
attrs["deps"] = [label_to_archive[get_archive(dep).data.label] for dep in external_source.deps]
return GoSource(**attrs), internal_archive