Skip to content

Commit c5af7be

Browse files
committed
Exclude the package under test from imports when building the test binary
1 parent 8c16dae commit c5af7be

File tree

4 files changed

+127
-50
lines changed

4 files changed

+127
-50
lines changed

build_defs/go.build_defs

Lines changed: 99 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -216,29 +216,35 @@ def go_stdlib(name:str, tags:list=CONFIG.GO.BUILD_TAGS, labels:list=[], visibili
216216
visibility = visibility,
217217
)
218218

219-
220-
def _pkg_sources(name: str, deps: list, test_only:bool):
219+
def _pkg_local_deps(deps: list):
221220
"""
222-
This rule generates a list of go srcs for any rule in the deps list that is local to this package.
223-
224-
This is how we avoid transitively depending on all sources for a target in order to compile non-external tests.
225-
Without this, we'd have to transitively pull in all third party sources for the rule which is very expensive,
226-
especially for remote execution.
221+
Used by go_test to find libraries that are under test. Returns any build targets from
222+
the deps list that are in the current package.
227223
"""
228224
package_prefix = canonicalise(":all").removesuffix("all")
225+
return [dep for dep in deps if canonicalise(dep).startswith(package_prefix)]
226+
227+
228+
def _go_srcs_for_deps(name: str, deps: list, test_only:bool):
229+
"""
230+
Used by go_test to extract the sources of libraries under test so we can compile them
231+
into the test package. This just uses the require/provide mechanism to export the go_src
232+
rule for each go_library target.
233+
"""
234+
229235
return filegroup(
230-
name = name,
231-
tag = "pkg_srcs",
232-
exported_deps = [dep for dep in deps if canonicalise(dep).startswith(package_prefix)],
233-
requires = ["go_src"],
234-
test_only = test_only,
235-
)
236+
name = tag(name, "test_srcs"),
237+
tag = "pkg_srcs",
238+
exported_deps = deps,
239+
requires = ["go_src"],
240+
test_only = test_only,
241+
)
236242

237243
def go_library(name:str, srcs:list, resources:list=[], asm_srcs:list=None, hdrs:list=None, deps:list=[],
238244
visibility:list=None, test_only:bool&testonly=False, complete:bool=True,
239245
_needs_transitive_deps=False, _all_srcs=False, cover:bool=True,
240246
filter_srcs:bool=True, _link_private:bool=False, _link_extra:bool=True, _abi:str=None,
241-
_generate_import_config:bool=True, _generate_pkg_info:bool=CONFIG.GO.PKG_INFO,
247+
_generate_import_config:bool=True, _generate_pkg_info:bool=CONFIG.GO.PKG_INFO, _excluded_imports:list=None,
242248
import_path:str='', labels:list=[], package:str=None, pgo_file:str=None, _module:str='', _subrepo:str=''):
243249
"""Generates a Go library which can be reused by other rules.
244250

@@ -304,10 +310,6 @@ def go_library(name:str, srcs:list, resources:list=[], asm_srcs:list=None, hdrs:
304310
)
305311
deps += [modinfo]
306312

307-
# We could just depend on go_src for all our deps but that would mean bringing in srcs for targets outside this
308-
# package like third party sources which is especially slow on systems with slow syscall performance (macOS)
309-
if _all_srcs:
310-
deps += [_pkg_sources(name, deps, test_only)]
311313
if CONFIG.GO.STDLIB:
312314
deps += _stdlib()
313315

@@ -483,7 +485,18 @@ def go_library(name:str, srcs:list, resources:list=[], asm_srcs:list=None, hdrs:
483485
if pgo_file:
484486
srcs['pgo'] = [pgo_file]
485487

486-
cmds, tools = _go_library_cmds(name, import_path=package_path, complete=complete, all_srcs=_all_srcs, cover=cover, filter_srcs=filter_srcs, abi=_abi, embedcfg=embedcfg, pgo_file=pgo_file)
488+
cmds, tools = _go_library_cmds(
489+
name=name,
490+
import_path=package_path,
491+
complete=complete,
492+
all_srcs=_all_srcs,
493+
cover=cover,
494+
filter_srcs=filter_srcs,
495+
abi=_abi,
496+
embedcfg=embedcfg,
497+
pgo_file=pgo_file,
498+
excluded_imports=_excluded_imports,
499+
)
487500

488501
return build_rule(
489502
name = name,
@@ -533,8 +546,15 @@ def _generate_pkg_import_cfg_cmd(name, out, pkg_location, handle_basename_eq_dir
533546

534547
return f'{target_comment} && {find_archives} | {remove_pkg_location} | {remove_ext} | {format_packagefile_directive} | sort -u >> {out}'
535548

536-
def _aggregate_import_cfg_cmd():
537-
return 'find . -name "*.importconfig" | xargs -I{} cat {} | sort -u >> importconfig'
549+
def _aggregate_import_cfg_cmd(excluded_libs:list):
550+
exclude = ""
551+
# Exclude any libs from the import config. This is used to exclude the package under
552+
# test in go_test, as this has been replaced with a test version of this package that
553+
# contains the test sources as well.
554+
if excluded_libs:
555+
exclude_pattern = "|".join([f"$(location {lib})" for lib in excluded_libs])
556+
exclude = f" | grep -v \"{exclude_pattern}\""
557+
return 'find . -name "*.importconfig" | xargs -I{} cat {}' + exclude + ' | sort -u >> importconfig'
538558

539559
def _trim_import_path(pkg):
540560
parts = pkg.split("/")
@@ -729,14 +749,19 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
729749
used to contruct the list of definitions passed to the linker.
730750
env (dict): Additional environment variables to set for the test
731751
"""
732-
733-
test_package = name + '_lib'
752+
# For non-external tests, we exclude any local go_libraries from deps. These packages will be recompiled
753+
# as _{name}#lib with the test sources. This new package replaces the non-test version.
754+
excluded_packages = None
755+
if not external:
756+
excluded_packages = _pkg_local_deps(deps)
757+
deps += [_go_srcs_for_deps(tag(name, "local_srcs"), excluded_packages, True)]
758+
759+
test_package = name + '_lib' if external or not package_name() else None
734760
# Unfortunately we have to recompile this to build the test together with its library.
735761
lib_rule = go_library(
736762
name = f'_{name}#lib',
737763
srcs = srcs,
738764
resources = resources,
739-
package = test_package,
740765
deps = deps,
741766
_all_srcs=not external,
742767
labels = labels,
@@ -747,10 +772,11 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
747772
_generate_import_config=False,
748773
_generate_pkg_info = False,
749774
import_path = test_package,
775+
_excluded_imports=excluded_packages,
750776
)
751777

752778
if cgo:
753-
archive_name = test_package + "_cgo"
779+
archive_name = f"_{name}#lib_cgo"
754780
lib_rule = merge_cgo_obj(
755781
name = name,
756782
tag='cgo',
@@ -762,12 +788,12 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
762788
package = archive_name,
763789
)
764790
else:
765-
archive_name = test_package
791+
archive_name = f"_{name}#lib"
766792

767793
import_config = build_rule(
768794
name=f'_{name}#lib_import_config',
769795
cmd = _write_import_config_cmd(archive_name, test_package),
770-
outs = [f'{test_package}.importconfig'],
796+
outs = [f'{name}_test_lib.importconfig'],
771797
visibility = visibility,
772798
test_only = True,
773799
labels = labels,
@@ -779,8 +805,8 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
779805
deps = deps,
780806
test_only = True,
781807
labels = labels,
782-
test_package = test_package,
783808
external = external,
809+
test_package = test_package,
784810
)
785811
modinfo = _go_modinfo(
786812
name = name,
@@ -799,9 +825,16 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
799825
labels = labels,
800826
import_path = "main",
801827
_generate_pkg_info = False,
828+
_excluded_imports=excluded_packages,
829+
)
830+
cmds, tools = _go_binary_cmds(
831+
name=name,
832+
static=static,
833+
definitions=definitions,
834+
gcov=cgo,
835+
test=True,
836+
excluded_imports=excluded_packages,
802837
)
803-
cmds, tools = _go_binary_cmds(name, static=static, definitions=definitions, gcov=cgo, test=True)
804-
805838

806839
test_cmd = f'$TEST {flags} 2>&1 | tee $TMP_DIR/test.results'
807840
worker_cmd = f'$(worker {worker})' if worker else ""
@@ -855,7 +888,7 @@ def go_test(name:str, srcs:list, resources:list=None, data:list|dict=None, deps:
855888
building_description = "Compiling...",
856889
needs_transitive_deps = True,
857890
output_is_complete = True,
858-
pre_build = _collect_linker_flags(static, definitions),
891+
pre_build = _collect_linker_flags(static, definitions, excluded_packages),
859892
env = env,
860893
)
861894

@@ -893,13 +926,19 @@ def go_benchmark(name:str, srcs:list, resources:list=None, data:list|dict=None,
893926
used to contruct the list of definitions passed to the linker.
894927
test_only (bool): If True, is only visible to test rules.
895928
"""
896-
test_package = name + '_lib'
929+
# For non-external tests, we exclude any local go_libraries from deps. These packages will be recompiled
930+
# as _{name}#lib with the test sources. This new package replaces the non-test version.
931+
excluded_packages = None
932+
if not external:
933+
excluded_packages = _pkg_local_deps(deps)
934+
deps += [_go_srcs_for_deps(tag(name, "local_srcs"), excluded_packages, True)]
935+
936+
test_package = name + '_lib' if external or not package_name() else None
897937
# Unfortunately we have to recompile this to build the test together with its library.
898938
lib_rule = go_library(
899939
name = '_%s#lib' % name,
900940
srcs = srcs,
901941
resources = resources,
902-
package = test_package,
903942
deps = deps,
904943
_all_srcs=not external,
905944
labels = labels,
@@ -909,14 +948,7 @@ def go_benchmark(name:str, srcs:list, resources:list=None, data:list|dict=None,
909948
complete = False,
910949
_generate_import_config=False,
911950
import_path=test_package,
912-
)
913-
import_config = build_rule(
914-
name=f'_{name}#lib_import_config',
915-
cmd = _write_import_config_cmd(test_package, test_package),
916-
outs = [f'{test_package}.importconfig'],
917-
visibility=visibility,
918-
test_only=True,
919-
labels=labels,
951+
_excluded_imports=excluded_packages,
920952
)
921953
if cgo:
922954
lib_rule = merge_cgo_obj(
@@ -928,6 +960,14 @@ def go_benchmark(name:str, srcs:list, resources:list=None, data:list|dict=None,
928960
deps = deps,
929961
test_only = test_only,
930962
)
963+
import_config = build_rule(
964+
name=f'_{name}#lib_import_config',
965+
cmd = _write_import_config_cmd(test_package if test_package else lib_rule.removeprefix(":"), test_package),
966+
outs = [f'{test_package}.importconfig'],
967+
visibility=visibility,
968+
test_only=True,
969+
labels=labels,
970+
)
931971
main_rule = go_test_main(
932972
name = name,
933973
_tag = 'main',
@@ -952,8 +992,9 @@ def go_benchmark(name:str, srcs:list, resources:list=None, data:list|dict=None,
952992
labels = labels,
953993
test_only = test_only,
954994
import_path="main",
995+
_excluded_imports=excluded_packages,
955996
)
956-
cmds, tools = _go_binary_cmds(name, static=static, definitions=definitions, gcov=cgo, test=True)
997+
cmds, tools = _go_binary_cmds(name, static=static, definitions=definitions, gcov=cgo, test=True, excluded_imports=excluded_packages)
957998

958999
return build_rule(
9591000
name=name,
@@ -973,7 +1014,7 @@ def go_benchmark(name:str, srcs:list, resources:list=None, data:list|dict=None,
9731014
needs_transitive_deps=True,
9741015
output_is_complete=True,
9751016
test_only=test_only,
976-
pre_build = _collect_linker_flags(static, definitions),
1017+
pre_build = _collect_linker_flags(static, definitions, excluded_packages),
9771018
)
9781019

9791020
def go_test_main(name:str, srcs:list, test_package:str="", test_only:bool=False, external=False,
@@ -1112,7 +1153,7 @@ def _go_install_module(name:str, module:str, install:list, src:str, outs:list, d
11121153
else:
11131154
cmd += [_generate_pkg_import_cfg_cmd(name, "goroot.importconfig", '"$GOROOT"')]
11141155
cmd += [
1115-
_aggregate_import_cfg_cmd(),
1156+
_aggregate_import_cfg_cmd(None),
11161157
f"$TOOLS_PLEASE_GO install {build_tags} --trim_path $TMP_DIR --src_root=$(location {src}) --module_name={module} --importcfg=importconfig --go_tool=$TOOLS_GO {cc_tool_flag} --out=pkg/{CONFIG.OS}_{CONFIG.ARCH} " + " ".join(install),
11171158
"cat LD_FLAGS",
11181159
]
@@ -1610,7 +1651,7 @@ def _set_go_env():
16101651
return f"export CGO_ENABLED=1 && {cmd}" if CONFIG.GO.CGO_ENABLED else cmd
16111652

16121653

1613-
def _go_library_cmds(name, import_path:str="", complete=True, all_srcs=False, cover=True, filter_srcs=True, abi=False, embedcfg=None, pgo_file=None):
1654+
def _go_library_cmds(name, import_path:str="", complete=True, all_srcs=False, cover=True, filter_srcs=True, abi=False, embedcfg=None, pgo_file=None, excluded_imports=None):
16141655
"""Returns the commands to run for building a Go library."""
16151656
complete_flag = '-complete ' if complete else ''
16161657
embed_flag = ' -embedcfg $SRCS_EMBED' if embedcfg else ''
@@ -1640,7 +1681,7 @@ def _go_library_cmds(name, import_path:str="", complete=True, all_srcs=False, co
16401681
gen_import_cfg = _set_go_env()
16411682
if not CONFIG.GO.STDLIB:
16421683
gen_import_cfg += ' && ' + _generate_pkg_import_cfg_cmd(name, "goroot.importconfig", '"$GOROOT"')
1643-
gen_import_cfg += ' && ' + _aggregate_import_cfg_cmd()
1684+
gen_import_cfg += ' && ' + _aggregate_import_cfg_cmd(excluded_imports)
16441685
prefix = ('export SRCS_GO="$PKG_DIR/*.go"; ' + gen_import_cfg) if all_srcs else gen_import_cfg
16451686

16461687
cmds = {
@@ -1657,7 +1698,7 @@ def _go_library_cmds(name, import_path:str="", complete=True, all_srcs=False, co
16571698
return cmds, tools
16581699

16591700

1660-
def _go_binary_cmds(name, static=False, ldflags='', pkg_config='', definitions=None, gcov=False, split_debug=False, strip=None, test=False):
1701+
def _go_binary_cmds(name, excluded_imports=None, static=False, ldflags='', pkg_config='', definitions=None, gcov=False, split_debug=False, strip=None, test=False):
16611702
"""Returns the commands to run for linking a Go binary."""
16621703

16631704
tool = '"$TOOLS_GO"' if CONFIG.GO.GO_LINK_TOOL else '"$TOOLS_GO" tool link'
@@ -1666,7 +1707,7 @@ def _go_binary_cmds(name, static=False, ldflags='', pkg_config='', definitions=N
16661707
gen_import_cfg = _set_go_env()
16671708
if not CONFIG.GO.STDLIB:
16681709
gen_import_cfg += ' && ' + _generate_pkg_import_cfg_cmd(name, "goroot.importconfig", '"$GOROOT"')
1669-
gen_import_cfg += ' && ' + _aggregate_import_cfg_cmd()
1710+
gen_import_cfg += ' && ' + _aggregate_import_cfg_cmd(excluded_imports)
16701711

16711712
linkerdefs = []
16721713
if definitions is None:
@@ -1736,12 +1777,20 @@ def _go_import_path_cmd(import_path):
17361777
return ' && ln -s "$TMP_DIR" ' + import_path
17371778

17381779

1739-
def _collect_linker_flags(static, definitions, strip=None):
1780+
def _collect_linker_flags(static, definitions, excluded_imports=None, strip=None):
17401781
"""Returns a pre-build function to apply transitive linker flags to a go_binary rule."""
17411782
def collect_linker_flags(name):
17421783
ldflags, pkg_config = _get_ldflags_and_pkgconfig(name)
17431784
if ldflags or pkg_config:
1744-
cmds, _ = _go_binary_cmds(name=name, static=static, ldflags=ldflags, pkg_config=pkg_config, definitions=definitions, strip=strip)
1785+
cmds, _ = _go_binary_cmds(
1786+
name=name,
1787+
static=static,
1788+
ldflags=ldflags,
1789+
pkg_config=pkg_config,
1790+
definitions=definitions,
1791+
strip=strip,
1792+
excluded_imports=excluded_imports,
1793+
)
17451794
for k, v in cmds.items():
17461795
set_command(name, k, v)
17471796
return collect_linker_flags

test/normal_test/BUILD

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
subinclude("///go//build_defs:go")
2+
3+
go_library(
4+
name = "foo",
5+
srcs = ["foo.go"],
6+
)
7+
8+
go_test(
9+
name = "foo_test",
10+
srcs = ["foo_test.go"],
11+
deps = [":foo"],
12+
)

test/normal_test/foo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package foo
2+
3+
func foo() string {
4+
return "foo"
5+
}

test/normal_test/foo_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package foo
2+
3+
import "testing"
4+
5+
func TestFoo(t *testing.T) {
6+
res := foo()
7+
8+
if res != "foo" {
9+
t.Fail()
10+
}
11+
}

0 commit comments

Comments
 (0)