forked from bazelbuild/bazel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Test if JavaInfo identical to one returned by Java rules can be const…
…ructed. I implemented the tests for the most basic parameters of java_library, that is srcs, deps, runtime_deps, exports. More complex examples with annotation processing are to follow. Comparing JavaInfo provider in Java was challenging, because most of the Bazel classes (providers, NestedSets) implement equals based on reference. So in order to compare it you need to destruct the object down to NestedSets and then compare each Artifact based on path. Artifacts equals method actually depends on the producing action (so they would be different), while compareTo==0 only compares path. The whole JavaInfo comparison in Java would result in a lot of boilerplate code. This is why I chose to use Starlark to convert a JavaInfo object into Dictionary with string values (JSON like - can have other Dicts and Lists). Those are easier to compare and they produce nice assertion messages. Also note that JavaInfo object is completely inspectable in Starlark, so the comparison is also complete. Those tests already discovered two problem, that will be fixed in following CLs: - cannot set value of native_headers or manifest - JavaExportsProvider is not set with exports parameter (but other values in JavaInfo are already set correctly) PiperOrigin-RevId: 361131344
- Loading branch information
1 parent
2d13fdb
commit 5e03a2c
Showing
5 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
263 changes: 263 additions & 0 deletions
263
src/test/java/com/google/devtools/build/lib/rules/java/JavaInfoRoundtripTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// Copyright 2021 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. | ||
|
||
package com.google.devtools.build.lib.rules.java; | ||
|
||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import com.google.devtools.build.lib.analysis.ConfiguredTarget; | ||
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; | ||
import com.google.devtools.build.lib.testutil.TestConstants; | ||
import com.google.devtools.build.lib.vfs.ModifiedFileSet; | ||
import com.google.devtools.build.lib.vfs.PathFragment; | ||
import com.google.devtools.build.lib.vfs.Root; | ||
import java.util.Map; | ||
import net.starlark.java.eval.Dict; | ||
import net.starlark.java.eval.Starlark; | ||
import net.starlark.java.eval.StarlarkList; | ||
import org.junit.Before; | ||
import org.junit.Ignore; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
|
||
/** Tests if JavaInfo identical to one returned by Java rules can be constructed. */ | ||
@RunWith(JUnit4.class) | ||
public class JavaInfoRoundtripTest extends BuildViewTestCase { | ||
/** A rule to convert JavaInfo to a structure having only string values. */ | ||
@Before | ||
public void javaInfoToDict() throws Exception { | ||
mockToolsConfig.create("tools/build_defs/inspect/BUILD"); | ||
mockToolsConfig.copyTool( | ||
TestConstants.BAZEL_REPO_SCRATCH + "tools/build_defs/inspect/struct_to_dict.bzl", | ||
"tools/build_defs/inspect/struct_to_dict.bzl"); | ||
|
||
scratch.file( | ||
"javainfo/javainfo_to_dict.bzl", | ||
"load('//tools/build_defs/inspect:struct_to_dict.bzl', 'struct_to_dict')", | ||
"def _impl(ctx):", | ||
" return struct(result = struct_to_dict(ctx.attr.dep[JavaInfo], 10))", | ||
"javainfo_to_dict = rule(_impl, attrs = {'dep' : attr.label()})"); | ||
} | ||
|
||
/** A simple rule that calls JavaInfo constructor using identical attribute as java_library. */ | ||
@Before | ||
public void constructJavaInfo() throws Exception { | ||
setBuildLanguageOptions("--experimental_google_legacy_api"); | ||
|
||
scratch.file( | ||
"foo/construct_javainfo.bzl", | ||
"def _impl(ctx):", | ||
" OUTS = {", | ||
" 'lib':'lib%s.jar',", | ||
" 'hjar': 'lib%s-hjar.jar',", | ||
" 'src': 'lib%s-src.jar',", | ||
" 'compile_jdeps': 'lib%s-hjar.jdeps',", | ||
" 'jdeps': 'lib%s.jdeps',", | ||
" 'manifest': 'lib%s.jar_manifest_proto'}", | ||
" for file, name in OUTS.items():", | ||
" OUTS[file] = ctx.actions.declare_file(name % ctx.label.name)", | ||
" ctx.actions.write(OUTS[file], '')", | ||
" ", | ||
" java_info = JavaInfo(", | ||
" output_jar = OUTS['lib'],", | ||
" compile_jar = OUTS['hjar'],", | ||
" source_jar = OUTS['src'],", | ||
" deps = [d[JavaInfo] for d in ctx.attr.deps],", | ||
" runtime_deps = [d[JavaInfo] for d in ctx.attr.runtime_deps],", | ||
" exports = [d[JavaInfo] for d in ctx.attr.exports],", | ||
" jdeps = OUTS['jdeps'],", | ||
" )", | ||
" return [java_info]", | ||
"", | ||
"construct_javainfo = rule(", | ||
" implementation = _impl,", | ||
" attrs = {", | ||
" 'srcs': attr.label_list(allow_files = True),", | ||
" 'deps': attr.label_list(),", | ||
" 'runtime_deps': attr.label_list(),", | ||
" 'exports': attr.label_list(),", | ||
" },", | ||
" fragments = ['java'],", | ||
")"); | ||
} | ||
|
||
/** For a given target providing JavaInfo returns a Starlark Dict with String values */ | ||
private Dict<Object, Object> getDictFromJavaInfo(String packageName, String javaInfoTarget) | ||
throws Exception { | ||
// Because we're overwriting files to have identical names, we need to invalidate them. | ||
skyframeExecutor.invalidateFilesUnderPathForTesting( | ||
reporter, | ||
new ModifiedFileSet.Builder().modify(PathFragment.create(packageName + "/BUILD")).build(), | ||
Root.fromPath(rootDirectory)); | ||
|
||
scratch.deleteFile("javainfo/BUILD"); | ||
ConfiguredTarget dictTarget = | ||
scratchConfiguredTarget( | ||
"javainfo", | ||
"javainfo", | ||
"load(':javainfo_to_dict.bzl', 'javainfo_to_dict')", | ||
"javainfo_to_dict(", | ||
" name = 'javainfo',", | ||
" dep = '//" + packageName + ':' + javaInfoTarget + "',", | ||
")"); | ||
@SuppressWarnings("unchecked") // deserialization | ||
Dict<Object, Object> javaInfo = (Dict<Object, Object>) dictTarget.get("result"); | ||
return javaInfo; | ||
} | ||
|
||
// TODO(b/163811682): remove once JavaInfo supports setting manifest_proto and native_headers. | ||
private Dict<Object, Object> removeManifestAndNativeHeaders(Dict<Object, Object> javaInfo) { | ||
@SuppressWarnings("unchecked") // safe by specification | ||
Dict<Object, Object> outputs = (Dict<Object, Object>) javaInfo.get("outputs"); | ||
@SuppressWarnings("unchecked") // safe by specification | ||
StarlarkList<Object> jars = (StarlarkList<Object>) outputs.get("jars"); | ||
@SuppressWarnings("unchecked") // safe by specification | ||
Dict<Object, Object> jar0 = (Dict<Object, Object>) jars.get(0); | ||
|
||
jar0 = Dict.builder().putAll(jar0).put("manifest_proto", Starlark.NONE).buildImmutable(); | ||
jars = StarlarkList.immutableOf(jar0); | ||
outputs = | ||
Dict.builder() | ||
.putAll((Map<?, ?>) javaInfo.get("outputs")) | ||
.put("jars", jars) | ||
.put("native_headers", Starlark.NONE) | ||
.buildImmutable(); | ||
return Dict.builder().putAll(javaInfo).put("outputs", outputs).buildImmutable(); | ||
} | ||
|
||
private Dict<Object, Object> removeCompilationInfo(Dict<Object, Object> javaInfo) { | ||
return Dict.builder().putAll(javaInfo).put("compilation_info", Starlark.NONE).buildImmutable(); | ||
} | ||
|
||
@Test | ||
public void dictFromJavaInfo_nonEmpty() throws Exception { | ||
scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); | ||
|
||
Dict<Object, Object> javaInfo = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
assertThat((Map<?, ?>) javaInfo).isNotEmpty(); | ||
} | ||
|
||
@Test | ||
public void dictFromJavaInfo_detectsDifference() throws Exception { | ||
|
||
scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); | ||
Dict<Object, Object> javaInfoA = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib2', srcs = ['A.java'])"); | ||
Dict<Object, Object> javaInfoB = getDictFromJavaInfo("foo", "java_lib2"); | ||
|
||
assertThat((Map<?, ?>) javaInfoA).isNotEqualTo(javaInfoB); | ||
} | ||
|
||
@Test | ||
public void roundtripJavainfo_srcs() throws Exception { | ||
|
||
scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); | ||
Dict<Object, Object> javaInfoA = getDictFromJavaInfo("foo", "java_lib"); | ||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"load('//foo:construct_javainfo.bzl', 'construct_javainfo')", | ||
"construct_javainfo(name = 'java_lib', srcs = ['A.java'])"); | ||
Dict<Object, Object> javaInfoB = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
javaInfoA = removeManifestAndNativeHeaders(javaInfoA); | ||
javaInfoA = removeCompilationInfo(javaInfoA); | ||
assertThat((Map<?, ?>) javaInfoB).isEqualTo(javaInfoA); | ||
} | ||
|
||
@Test | ||
public void roundtripJavaInfo_deps() throws Exception { | ||
scratch.file("bar/BUILD", "java_library(name = 'javalib', srcs = ['A.java'])"); | ||
|
||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"java_library(", | ||
" name = 'java_lib',", | ||
" srcs = ['A.java'],", | ||
" deps = ['//bar:javalib']", | ||
")"); | ||
Dict<Object, Object> javaInfoA = getDictFromJavaInfo("foo", "java_lib"); | ||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"load('//foo:construct_javainfo.bzl', 'construct_javainfo')", | ||
"construct_javainfo(", | ||
" name = 'java_lib', ", | ||
" srcs = ['A.java'], ", | ||
" deps = ['//bar:javalib'],", | ||
")"); | ||
Dict<Object, Object> javaInfoB = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
javaInfoA = removeManifestAndNativeHeaders(javaInfoA); | ||
javaInfoA = removeCompilationInfo(javaInfoA); | ||
assertThat((Map<?, ?>) javaInfoB).isEqualTo(javaInfoA); | ||
} | ||
|
||
@Test | ||
public void roundtipJavaInfo_runtimeDeps() throws Exception { | ||
scratch.file("bar/BUILD", "java_library(name = 'deplib', srcs = ['A.java'])"); | ||
|
||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"java_library(", | ||
" name = 'java_lib',", | ||
" srcs = ['A.java'],", | ||
" runtime_deps = ['//bar:deplib']", | ||
")"); | ||
Dict<Object, Object> javaInfoA = getDictFromJavaInfo("foo", "java_lib"); | ||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"load('//foo:construct_javainfo.bzl', 'construct_javainfo')", | ||
"construct_javainfo(", | ||
" name = 'java_lib', ", | ||
" srcs = ['A.java'], ", | ||
" runtime_deps = ['//bar:deplib'],", | ||
")"); | ||
Dict<Object, Object> javaInfoB = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
javaInfoA = removeManifestAndNativeHeaders(javaInfoA); | ||
javaInfoA = removeCompilationInfo(javaInfoA); | ||
assertThat((Map<?, ?>) javaInfoB).isEqualTo(javaInfoA); | ||
} | ||
|
||
// TODO(b/159269546): enable once JavaInfo call is fixed to return JavaExportsProvider") | ||
@Test | ||
@Ignore | ||
public void roundtipJavaInfo_exports() throws Exception { | ||
scratch.file("bar/BUILD", "java_library(name = 'exportlib', srcs = ['A.java'])"); | ||
|
||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"java_library(", | ||
" name = 'java_lib',", | ||
" srcs = ['A.java'],", | ||
" exports = ['//bar:exportlib']", | ||
")"); | ||
Dict<Object, Object> javaInfoA = getDictFromJavaInfo("foo", "java_lib"); | ||
scratch.overwriteFile( | ||
"foo/BUILD", | ||
"load('//foo:construct_javainfo.bzl', 'construct_javainfo')", | ||
"construct_javainfo(", | ||
" name = 'java_lib', ", | ||
" srcs = ['A.java'], ", | ||
" exports = ['//bar:exportlib'],", | ||
")"); | ||
Dict<Object, Object> javaInfoB = getDictFromJavaInfo("foo", "java_lib"); | ||
|
||
javaInfoA = removeManifestAndNativeHeaders(javaInfoA); | ||
javaInfoA = removeCompilationInfo(javaInfoA); | ||
assertThat((Map<?, ?>) javaInfoB).isEqualTo(javaInfoA); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
load("@bazel_skylib//:bzl_library.bzl", "bzl_library") | ||
|
||
licenses(["notice"]) | ||
|
||
filegroup( | ||
name = "srcs", | ||
srcs = glob(["**"]), | ||
visibility = ["//tools/build_defs:__pkg__"], | ||
) | ||
|
||
bzl_library( | ||
name = "defs", | ||
srcs = glob(["*.bzl"]), | ||
visibility = ["//visibility:public"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Copyright 2021 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. | ||
|
||
"""struct_to_dict() tries to convert struct-like objects to dicts "recursively". | ||
Useful to dump arbitrary provider data. Objects below the | ||
specified depth are copied literally. | ||
""" | ||
|
||
def struct_to_dict(x, depth = 5): | ||
root = {} | ||
queue = [(root, x)] | ||
for i in range(depth): | ||
nextlevel = [] if i < depth - 1 else None | ||
for dest, obj in queue: | ||
if _is_depset(obj): | ||
obj = obj.to_list() | ||
if _is_list(obj): | ||
for item in list(obj): | ||
converted = _convert_one(item, nextlevel) | ||
dest.append(converted) | ||
elif type(obj) == type({}): | ||
for key, value in dest.items(): | ||
converted = _convert_one(value, nextlevel) | ||
dest[key] = converted | ||
else: # struct or object | ||
dest["_type"] = type(obj) | ||
for propname in dir(obj): | ||
_token = struct() | ||
value = getattr(obj, propname, _token) | ||
if value == _token: | ||
continue # Native methods are not inspectable. Ignore. | ||
converted = _convert_one(value, nextlevel) | ||
dest[propname] = converted | ||
if type(obj) == "Target": | ||
if JavaInfo in obj: | ||
dest["JavaInfo"] = _convert_one(obj[JavaInfo], nextlevel) | ||
if CcInfo in obj: | ||
dest["CcInfo"] = _convert_one(obj[CcInfo], nextlevel) | ||
|
||
queue = nextlevel | ||
return root | ||
|
||
def _convert_one(val, nextlevel): | ||
nest = nextlevel != None | ||
if _is_sequence(val) and nest: | ||
out = [] | ||
nextlevel.append((out, val)) | ||
return out | ||
elif _is_atom(val) or not nest: | ||
return val | ||
elif type(val) == "File": | ||
return val.path | ||
elif type(val) == "Label": | ||
return str(val) | ||
else: # by default try to convert object to dict | ||
out = {} | ||
nextlevel.append((out, val)) | ||
return out | ||
|
||
def _is_sequence(val): | ||
return _is_list(val) or _is_depset(val) | ||
|
||
def _is_list(val): | ||
return type(val) == type([]) | ||
|
||
def _is_depset(val): | ||
return type(val) == type(depset()) | ||
|
||
def _is_atom(val): | ||
return (type(val) == type("") or | ||
type(val) == type(0) or | ||
type(val) == type(False) or | ||
type(val) == type(None)) |