Skip to content

Commit

Permalink
Test if JavaInfo identical to one returned by Java rules can be const…
Browse files Browse the repository at this point in the history
…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
comius authored and copybara-github committed Mar 5, 2021
1 parent 2d13fdb commit 5e03a2c
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/test/java/com/google/devtools/build/lib/rules/java/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ filegroup(
visibility = ["//src:__subpackages__"],
)

java_test(
name = "JavaInfoRoundtripTest",
srcs = ["JavaInfoRoundtripTest.java"],
data = ["//tools/build_defs/inspect:defs"],
deps = [
"//src/main/java/com/google/devtools/build/lib/analysis:configured_target",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/net/starlark/java/eval",
"//src/test/java/com/google/devtools/build/lib/analysis/util",
"//src/test/java/com/google/devtools/build/lib/testutil:TestConstants",
"//third_party:junit4",
"//third_party:truth",
],
)

java_test(
name = "JavaInfoStarlarkApiTest",
srcs = ["JavaInfoStarlarkApiTest.java"],
Expand Down
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);
}
}
1 change: 1 addition & 0 deletions tools/build_defs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ filegroup(
"//tools/build_defs/cc/whitelists/parse_headers_and_layering_check:srcs",
"//tools/build_defs/cc/whitelists/starlark_hdrs_check:srcs",
"//tools/build_defs/hash:srcs",
"//tools/build_defs/inspect:srcs",
"//tools/build_defs/pkg:srcs",
"//tools/build_defs/repo:srcs",
"//tools/build_defs/cc/tests:cc_import_tests_files",
Expand Down
15 changes: 15 additions & 0 deletions tools/build_defs/inspect/BUILD
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"],
)
85 changes: 85 additions & 0 deletions tools/build_defs/inspect/struct_to_dict.bzl
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))

0 comments on commit 5e03a2c

Please sign in to comment.