| // Copyright 2023 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.docgen; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth8.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.LinkedListMultimap; | |
| import com.google.common.collect.ListMultimap; | |
| import com.google.devtools.build.docgen.BuildDocCollector.DocumentationOrigin; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeInfo; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeType; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.OriginKey; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import javax.annotation.Nullable; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for BuildDocCollector. */ | |
| @RunWith(JUnit4.class) | |
| public final class BuildDocCollectorTest { | |
| // The following are initialized by setUpCollectorState to simplify boilerplate. | |
| Map<String, DocumentationOrigin> ruleDocOrigin; | |
| Map<String, RuleDocumentation> ruleDocEntries; | |
| ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries; | |
| SourceUrlMapper urlMapper = | |
| new SourceUrlMapper( | |
| /* sourceUrlRoot= */ "https://example.com/", | |
| /* inputRoot= */ "/tmp/io_bazel/", | |
| ImmutableMap.of("@_builtins//:", "//src/main/starlark/builtins_bzl:")); | |
| @Before | |
| public void setUpCollectorState() { | |
| ruleDocOrigin = new HashMap<>(); | |
| ruleDocEntries = new HashMap<>(); | |
| attributeDocEntries = LinkedListMultimap.create(); | |
| } | |
| private int collectModuleInfoDocs(ModuleInfo moduleInfo) throws Exception { | |
| return BuildDocCollector.collectModuleInfoDocs( | |
| ruleDocOrigin, | |
| ruleDocEntries, | |
| ImmutableSet.of(), | |
| attributeDocEntries, | |
| moduleInfo, | |
| urlMapper); | |
| } | |
| private int collectModuleInfoDocsWithDenyList(ModuleInfo moduleInfo, Set<String> denyList) | |
| throws Exception { | |
| return BuildDocCollector.collectModuleInfoDocs( | |
| ruleDocOrigin, ruleDocEntries, denyList, attributeDocEntries, moduleInfo, urlMapper); | |
| } | |
| @Nullable | |
| private RuleDocumentationAttribute getAttribute( | |
| Set<RuleDocumentationAttribute> attributes, String name) { | |
| for (RuleDocumentationAttribute attribute : attributes) { | |
| if (attribute.getAttributeName().equals(name)) { | |
| return attribute; | |
| } | |
| } | |
| return null; | |
| } | |
| @Test | |
| public void collectModuleInfoDocs_basicFunctionality() throws Exception { | |
| ModuleInfo moduleInfo = | |
| ModuleInfo.newBuilder() | |
| .setModuleDocstring("My Language") | |
| .setFile("//:test.bzl") | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("binary_rules.my_binary") | |
| .setDocString("My language binary") | |
| .setExecutable(true) | |
| .setOriginKey( | |
| OriginKey.newBuilder() | |
| .setName("_my_binary") | |
| .setFile("@_builtins//:my_lang/my_binary.bzl")) | |
| .addAttribute( | |
| // starlark_doc_extract always injects the implicit "name" attribute | |
| AttributeInfo.newBuilder() | |
| .setName("name") | |
| .setType(AttributeType.NAME) | |
| .setMandatory(true) | |
| .setDocString("A unique name for this target.") | |
| .build()) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| .setName("srcs") | |
| .setDocString("My sources") | |
| .setType(AttributeType.LABEL_LIST) | |
| .setMandatory(true)) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| .setName("deps") | |
| .setDocString("My deps") | |
| .setType(AttributeType.LABEL_LIST) | |
| .setDefaultValue("[]")) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| .setName("old") | |
| .setDocString("Deprecated: do not use") | |
| .setType(AttributeType.STRING) | |
| .setDefaultValue("\"???\""))) | |
| .build(); | |
| assertThat(collectModuleInfoDocs(moduleInfo)).isEqualTo(1); | |
| assertThat(ruleDocEntries.keySet()).containsExactly("my_binary"); | |
| RuleDocumentation ruleDoc = ruleDocEntries.get("my_binary"); | |
| assertThat(ruleDoc.getRuleName()).isEqualTo("my_binary"); | |
| assertThat(ruleDoc.getRuleType()).isEqualTo(DocgenConsts.RuleType.BINARY); | |
| assertThat(ruleDoc.getRuleFamily()).isEqualTo("My Language"); | |
| assertThat(ruleDoc.getFamilySummary()).isEmpty(); | |
| assertThat(ruleDoc.getSourceUrl()) | |
| .isEqualTo("https://example.com/src/main/starlark/builtins_bzl/my_lang/my_binary.bzl"); | |
| assertThat(ruleDoc.getHtmlDocumentation()).isEqualTo("My language binary"); | |
| Set<RuleDocumentationAttribute> attributes = ruleDoc.getAttributes(); | |
| assertThat(attributes) | |
| .containsAtLeastElementsIn(PredefinedAttributes.COMMON_ATTRIBUTES.values()); | |
| assertThat(attributes) | |
| .containsAtLeastElementsIn(PredefinedAttributes.BINARY_ATTRIBUTES.values()); | |
| assertThat( | |
| attributes.stream() | |
| .map(RuleDocumentationAttribute::getAttributeName) | |
| .filter( | |
| attr -> | |
| !(PredefinedAttributes.COMMON_ATTRIBUTES.containsKey(attr) | |
| || PredefinedAttributes.BINARY_ATTRIBUTES.containsKey(attr)))) | |
| // We do not want the implicit "name" attribute - we inject "name" at template level | |
| .containsExactly("srcs", "deps", "old"); | |
| RuleDocumentationAttribute srcsAttribute = getAttribute(attributes, "srcs"); | |
| assertThat(srcsAttribute.getAttributeName()).isEqualTo("srcs"); | |
| assertThat(srcsAttribute.getHtmlDocumentation()).isEqualTo("My sources"); | |
| assertThat(srcsAttribute.getSynopsis()) | |
| .isEqualTo("List of <a href=\"${link build-ref#labels}\">labels</a>; required"); | |
| RuleDocumentationAttribute depsAttribute = getAttribute(attributes, "deps"); | |
| assertThat(depsAttribute.getAttributeName()).isEqualTo("deps"); | |
| assertThat(depsAttribute.getHtmlDocumentation()).isEqualTo("My deps"); | |
| assertThat(depsAttribute.getSynopsis()) | |
| .isEqualTo( | |
| "List of <a href=\"${link build-ref#labels}\">labels</a>; default is <code>[]</code>"); | |
| RuleDocumentationAttribute oldAttribute = getAttribute(attributes, "old"); | |
| assertThat(oldAttribute.getAttributeName()).isEqualTo("old"); | |
| assertThat(oldAttribute.isDeprecated()).isTrue(); | |
| assertThat(oldAttribute.getSynopsis()).isEqualTo("String; default is <code>\"???\"</code>"); | |
| assertThat(ruleDocOrigin) | |
| .containsExactly( | |
| "my_binary", DocumentationOrigin.create("//:test.bzl", "binary_rules.my_binary")); | |
| } | |
| @Test | |
| public void collectModuleInfoDocs_respectsDenyList() throws Exception { | |
| ModuleInfo moduleInfo = | |
| ModuleInfo.newBuilder() | |
| .setModuleDocstring("My Language") | |
| .setFile("//:test.bzl") | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_library") | |
| .setDocString("My language library") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_library").setFile("//:my_library.bzl"))) | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_plugin") | |
| .setDocString("My language plugin") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_plugin").setFile("//:my_plugin.bzl"))) | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_import") | |
| .setDocString("My language import") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_import").setFile("//:my_import.bzl"))) | |
| .build(); | |
| assertThat(collectModuleInfoDocsWithDenyList(moduleInfo, ImmutableSet.of("my_library"))) | |
| .isEqualTo(2); | |
| assertThat(ruleDocEntries.keySet()).containsExactly("my_plugin", "my_import"); | |
| } | |
| @Test | |
| public void collectModuleInfoDocs_expectsNonemptyModuleDocstring() throws Exception { | |
| BuildEncyclopediaDocException e = | |
| assertThrows( | |
| BuildEncyclopediaDocException.class, | |
| () -> collectModuleInfoDocs(ModuleInfo.newBuilder().setModuleDocstring("").build())); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .contains("expected to be a single line representing a rule family name"); | |
| } | |
| @Test | |
| public void collectModuleInfoDocs_multilineModuleDocstring() throws Exception { | |
| ModuleInfo moduleInfo = | |
| ModuleInfo.newBuilder() | |
| .setModuleDocstring("My Language\n\nBlah blah blah") | |
| .setFile("//:test.bzl") | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_library") | |
| .setDocString("My language library") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_library").setFile("//:my_library.bzl"))) | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_plugin") | |
| .setDocString("My language plugin") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_plugin").setFile("//:my_plugin.bzl"))) | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("test_rules.my_test") | |
| .setDocString("My language test") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_test").setFile("//:my_test.bzl"))) | |
| .build(); | |
| assertThat(collectModuleInfoDocs(moduleInfo)).isEqualTo(3); | |
| // We expect family summary to be set only on the first rule, to avoid duplication in final | |
| // rendered output. | |
| assertThat(ruleDocEntries.get("my_library").getRuleFamily()).isEqualTo("My Language"); | |
| assertThat(ruleDocEntries.get("my_library").getFamilySummary()).isEqualTo("Blah blah blah"); | |
| assertThat(ruleDocEntries.get("my_plugin").getRuleFamily()).isEqualTo("My Language"); | |
| assertThat(ruleDocEntries.get("my_plugin").getFamilySummary()).isEmpty(); | |
| assertThat(ruleDocEntries.get("my_test").getRuleFamily()).isEqualTo("My Language"); | |
| assertThat(ruleDocEntries.get("my_test").getFamilySummary()).isEmpty(); | |
| } | |
| @Test | |
| public void collectModuleInfoDocs_linksCommonAttrsWithEmptyDocstringToCommonType() | |
| throws Exception { | |
| ModuleInfo moduleInfo = | |
| ModuleInfo.newBuilder() | |
| .setModuleDocstring("My Language") | |
| .setFile("//:test.bzl") | |
| .addRuleInfo( | |
| RuleInfo.newBuilder() | |
| .setRuleName("library_rules.my_library") | |
| .setDocString("My language library") | |
| .setOriginKey( | |
| OriginKey.newBuilder().setName("_my_library").setFile("//:my_library.bzl")) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| // Empty docstring | |
| .setName("srcs") | |
| .setType(AttributeType.LABEL_LIST) | |
| .setDefaultValue("[]")) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| // Empty docstring | |
| .setName("deps") | |
| .setType(AttributeType.LABEL_LIST) | |
| .setDefaultValue("[]")) | |
| .addAttribute( | |
| AttributeInfo.newBuilder() | |
| // Empty docstring | |
| .setName("uncommonly_named_attr") | |
| .setType(AttributeType.LABEL_LIST) | |
| .setDefaultValue("[]"))) | |
| .build(); | |
| assertThat(collectModuleInfoDocs(moduleInfo)).isEqualTo(1); | |
| Set<RuleDocumentationAttribute> attributes = ruleDocEntries.get("my_library").getAttributes(); | |
| assertThat(getAttribute(attributes, "srcs").isCommonType()).isTrue(); | |
| assertThat(getAttribute(attributes, "srcs").getGeneratedInRule("my_library")) | |
| .isEqualTo(DocgenConsts.TYPICAL_ATTRIBUTES); | |
| assertThat(getAttribute(attributes, "deps").isCommonType()).isTrue(); | |
| assertThat(getAttribute(attributes, "deps").getGeneratedInRule("my_library")) | |
| .isEqualTo(DocgenConsts.TYPICAL_ATTRIBUTES); | |
| assertThat(getAttribute(attributes, "uncommonly_named_attr").isCommonType()).isFalse(); | |
| assertThat(getAttribute(attributes, "uncommonly_named_attr").getGeneratedInRule("my_library")) | |
| .isEqualTo("my_library"); | |
| } | |
| } |