diff --git a/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java b/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java index e301d786ce5a4..4b0a0c5e77ebb 100644 --- a/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java +++ b/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java @@ -11,29 +11,26 @@ import org.elasticsearch.Build; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentParserConfiguration; -import org.elasticsearch.xcontent.XContentType; +import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; -import java.util.LinkedHashMap; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.regex.Pattern; /** * Encapsulates links to pages in the reference docs, so that for example we can include URLs in logs and API outputs. Each instance's * {@link #toString()} yields (a string representation of) a URL for the relevant docs. Links are defined in the resource file - * {@code reference-docs-links.json} which must include definitions for exactly the set of values of this enum. + * {@code reference-docs-links.txt} which must include definitions for exactly the set of values of this enum. */ public enum ReferenceDocs { /* - * Note that the docs subsystem parses {@code reference-docs-links.json} with regexes, not a JSON parser, so the whitespace in the file - * is important too. See {@code sub check_elasticsearch_links} in {@code https://github.com/elastic/docs/blob/master/build_docs.pl} for - * more details. + * Note that the docs subsystem parses {@code reference-docs-links.txt} differently. See {@code sub check_elasticsearch_links} in + * {@code https://github.com/elastic/docs/blob/master/build_docs.pl} for more details. * * Also note that the docs are built from the HEAD of each minor release branch, so in principle docs can move around independently of * the ES release process. To avoid breaking any links that have been baked into earlier patch releases, you may only add links in a @@ -89,7 +86,7 @@ public enum ReferenceDocs { private static final Map linksBySymbol; static { - try (var resourceStream = readFromJarResourceUrl(ReferenceDocs.class.getResource("reference-docs-links.json"))) { + try (var resourceStream = readFromJarResourceUrl(ReferenceDocs.class.getResource("reference-docs-links.txt"))) { linksBySymbol = Map.copyOf(readLinksBySymbol(resourceStream)); } catch (Exception e) { assert false : e; @@ -101,34 +98,69 @@ public enum ReferenceDocs { static final String CURRENT_VERSION_COMPONENT = "current"; static final String VERSION_COMPONENT = getVersionComponent(Build.current().version(), Build.current().isSnapshot()); - static Map readLinksBySymbol(InputStream inputStream) throws Exception { - try (var parser = XContentFactory.xContent(XContentType.JSON).createParser(XContentParserConfiguration.EMPTY, inputStream)) { - final var result = parser.map(LinkedHashMap::new, XContentParser::text); - final var iterator = result.keySet().iterator(); - for (int i = 0; i < values().length; i++) { - final var expected = values()[i].name(); - if (iterator.hasNext() == false) { - throw new IllegalStateException("ran out of values at index " + i + ": expecting " + expected); - } - final var actual = iterator.next(); - if (actual.equals(expected) == false) { - throw new IllegalStateException("mismatch at index " + i + ": found " + actual + " but expected " + expected); - } + static final int SYMBOL_COLUMN_WIDTH = 64; // increase as needed to accommodate yet longer symbols + + static Map readLinksBySymbol(InputStream inputStream) throws IOException { + final var padding = " ".repeat(SYMBOL_COLUMN_WIDTH); + + record LinksBySymbolEntry(String symbol, String link) implements Map.Entry { + @Override + public String getKey() { + return symbol; } - if (iterator.hasNext()) { - throw new IllegalStateException("found unexpected extra value: " + iterator.next()); + + @Override + public String getValue() { + return link; + } + + @Override + public String setValue(String value) { + assert false; + throw new UnsupportedOperationException(); } + } - // We must only link to anchors with fixed IDs (defined by [[fragment-name]] in the docs) because auto-generated fragment IDs - // depend on the heading text and are too easy to break inadvertently. Auto-generated fragment IDs begin with an underscore. - for (final var entry : result.entrySet()) { - if (entry.getValue().startsWith("_") || entry.getValue().contains("#_")) { - throw new IllegalStateException("found auto-generated fragment ID at " + entry.getKey()); + final var symbolCount = values().length; + final var linksBySymbolEntries = new LinksBySymbolEntry[symbolCount]; + + try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + for (int i = 0; i < symbolCount; i++) { + final var currentLine = reader.readLine(); + final var symbol = values()[i].name(); + if (currentLine == null) { + throw new IllegalStateException("links resource truncated at line " + (i + 1)); + } + if (currentLine.startsWith(symbol + " ") == false) { + throw new IllegalStateException( + "unexpected symbol at line " + (i + 1) + ": expected line starting with [" + symbol + " ]" + ); + } + final var link = currentLine.substring(SYMBOL_COLUMN_WIDTH).trim(); + if (Strings.hasText(link) == false) { + throw new IllegalStateException("no link found for [" + symbol + "] at line " + (i + 1)); + } + final var expectedLine = (symbol + padding).substring(0, SYMBOL_COLUMN_WIDTH) + link; + if (currentLine.equals(expectedLine) == false) { + throw new IllegalStateException("unexpected content at line " + (i + 1) + ": expected [" + expectedLine + "]"); } + + // We must only link to anchors with fixed IDs (defined by [[fragment-name]] in the docs) because auto-generated fragment + // IDs depend on the heading text and are too easy to break inadvertently. Auto-generated fragment IDs begin with "_" + if (link.startsWith("_") || link.contains("#_")) { + throw new IllegalStateException( + "found auto-generated fragment ID in link [" + link + "] for [" + symbol + "] at line " + (i + 1) + ); + } + linksBySymbolEntries[i] = new LinksBySymbolEntry(symbol, link); } - return result; + if (reader.readLine() != null) { + throw new IllegalStateException("unexpected trailing content at line " + (symbolCount + 1)); + } } + + return Map.ofEntries(linksBySymbolEntries); } /** diff --git a/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json index 1b5dc5b2f31e0..71be3d333ec3f 100644 --- a/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json +++ b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json @@ -1,46 +1,5 @@ -{ - "INITIAL_MASTER_NODES": "important-settings.html#initial_master_nodes", - "DISCOVERY_TROUBLESHOOTING": "discovery-troubleshooting.html", - "UNSTABLE_CLUSTER_TROUBLESHOOTING": "troubleshooting-unstable-cluster.html", - "LAGGING_NODE_TROUBLESHOOTING": "troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-lagging", - "SHARD_LOCK_TROUBLESHOOTING": "troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-shardlockobtainfailedexception", - "NETWORK_DISCONNECT_TROUBLESHOOTING": "troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-network", - "CONCURRENT_REPOSITORY_WRITERS": "diagnosing-corrupted-repositories.html", - "ARCHIVE_INDICES": "archive-indices.html", - "HTTP_TRACER": "modules-network.html#http-rest-request-tracer", - "LOGGING": "logging.html", - "BOOTSTRAP_CHECK_HEAP_SIZE": "bootstrap-checks-heap-size.html", - "BOOTSTRAP_CHECK_FILE_DESCRIPTOR": "bootstrap-checks-file-descriptor.html", - "BOOTSTRAP_CHECK_MEMORY_LOCK": "bootstrap-checks-memory-lock.html", - "BOOTSTRAP_CHECK_MAX_NUMBER_THREADS": "max-number-threads-check.html", - "BOOTSTRAP_CHECK_MAX_FILE_SIZE": "bootstrap-checks-max-file-size.html", - "BOOTSTRAP_CHECK_MAX_SIZE_VIRTUAL_MEMORY": "max-size-virtual-memory-check.html", - "BOOTSTRAP_CHECK_MAXIMUM_MAP_COUNT": "bootstrap-checks-max-map-count.html", - "BOOTSTRAP_CHECK_CLIENT_JVM": "bootstrap-checks-client-jvm.html", - "BOOTSTRAP_CHECK_USE_SERIAL_COLLECTOR": "bootstrap-checks-serial-collector.html", - "BOOTSTRAP_CHECK_SYSTEM_CALL_FILTER": "bootstrap-checks-syscall-filter.html", - "BOOTSTRAP_CHECK_ONERROR_AND_ONOUTOFMEMORYERROR": "bootstrap-checks-onerror.html", - "BOOTSTRAP_CHECK_EARLY_ACCESS": "bootstrap-checks-early-access.html", - "BOOTSTRAP_CHECK_ALL_PERMISSION": "bootstrap-checks-all-permission.html", - "BOOTSTRAP_CHECK_DISCOVERY_CONFIGURATION": "bootstrap-checks-discovery-configuration.html", - "BOOTSTRAP_CHECKS": "bootstrap-checks.html", - "BOOTSTRAP_CHECK_ENCRYPT_SENSITIVE_DATA": "bootstrap-checks-xpack.html#bootstrap-checks-xpack-encrypt-sensitive-data", - "BOOTSTRAP_CHECK_PKI_REALM": "bootstrap-checks-xpack.html#bootstrap-checks-xpack-pki-realm", - "BOOTSTRAP_CHECK_ROLE_MAPPINGS": "bootstrap-checks-xpack.html#bootstrap-checks-xpack-role-mappings", - "BOOTSTRAP_CHECK_TLS": "bootstrap-checks-xpack.html#bootstrap-checks-tls", - "BOOTSTRAP_CHECK_TOKEN_SSL": "bootstrap-checks-xpack.html#bootstrap-checks-xpack-token-ssl", - "BOOTSTRAP_CHECK_SECURITY_MINIMAL_SETUP": "security-minimal-setup.html", - "CONTACT_SUPPORT": "troubleshooting.html#troubleshooting-contact-support", - "UNASSIGNED_SHARDS": "red-yellow-cluster-status.html", - "EXECUTABLE_JNA_TMPDIR": "executable-jna-tmpdir.html", - "NETWORK_THREADING_MODEL": "modules-network.html#modules-network-threading-model", - "ALLOCATION_EXPLAIN_API": "cluster-allocation-explain.html", - "NETWORK_BINDING_AND_PUBLISHING": "modules-network.html#modules-network-binding-publishing", - "SNAPSHOT_REPOSITORY_ANALYSIS": "repo-analysis-api.html", - "S3_COMPATIBLE_REPOSITORIES": "repository-s3.html#repository-s3-compatible-services", - "LUCENE_MAX_DOCS_LIMIT": "size-your-shards.html#troubleshooting-max-docs-limit", - "MAX_SHARDS_PER_NODE": "size-your-shards.html#troubleshooting-max-shards-open", - "FLOOD_STAGE_WATERMARK": "fix-watermark-errors.html", - "X_OPAQUE_ID": "api-conventions.html#x-opaque-id", - "FORMING_SINGLE_NODE_CLUSTERS": "modules-discovery-bootstrap-cluster.html#modules-discovery-bootstrap-cluster-joining" -} +[ + "Content moved to reference-docs-links.txt", + "This is a temporary placeholder to satisfy sub check_elasticsearch_links in the docs build", + "Remove with @UpdateForV10 (if not before)" +] diff --git a/server/src/main/resources/org/elasticsearch/common/reference-docs-links.txt b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.txt new file mode 100644 index 0000000000000..190bbd3c319b4 --- /dev/null +++ b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.txt @@ -0,0 +1,44 @@ +INITIAL_MASTER_NODES important-settings.html#initial_master_nodes +DISCOVERY_TROUBLESHOOTING discovery-troubleshooting.html +UNSTABLE_CLUSTER_TROUBLESHOOTING troubleshooting-unstable-cluster.html +LAGGING_NODE_TROUBLESHOOTING troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-lagging +SHARD_LOCK_TROUBLESHOOTING troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-shardlockobtainfailedexception +NETWORK_DISCONNECT_TROUBLESHOOTING troubleshooting-unstable-cluster.html#troubleshooting-unstable-cluster-network +CONCURRENT_REPOSITORY_WRITERS diagnosing-corrupted-repositories.html +ARCHIVE_INDICES archive-indices.html +HTTP_TRACER modules-network.html#http-rest-request-tracer +LOGGING logging.html +BOOTSTRAP_CHECK_HEAP_SIZE bootstrap-checks-heap-size.html +BOOTSTRAP_CHECK_FILE_DESCRIPTOR bootstrap-checks-file-descriptor.html +BOOTSTRAP_CHECK_MEMORY_LOCK bootstrap-checks-memory-lock.html +BOOTSTRAP_CHECK_MAX_NUMBER_THREADS max-number-threads-check.html +BOOTSTRAP_CHECK_MAX_FILE_SIZE bootstrap-checks-max-file-size.html +BOOTSTRAP_CHECK_MAX_SIZE_VIRTUAL_MEMORY max-size-virtual-memory-check.html +BOOTSTRAP_CHECK_MAXIMUM_MAP_COUNT bootstrap-checks-max-map-count.html +BOOTSTRAP_CHECK_CLIENT_JVM bootstrap-checks-client-jvm.html +BOOTSTRAP_CHECK_USE_SERIAL_COLLECTOR bootstrap-checks-serial-collector.html +BOOTSTRAP_CHECK_SYSTEM_CALL_FILTER bootstrap-checks-syscall-filter.html +BOOTSTRAP_CHECK_ONERROR_AND_ONOUTOFMEMORYERROR bootstrap-checks-onerror.html +BOOTSTRAP_CHECK_EARLY_ACCESS bootstrap-checks-early-access.html +BOOTSTRAP_CHECK_ALL_PERMISSION bootstrap-checks-all-permission.html +BOOTSTRAP_CHECK_DISCOVERY_CONFIGURATION bootstrap-checks-discovery-configuration.html +BOOTSTRAP_CHECKS bootstrap-checks.html +BOOTSTRAP_CHECK_ENCRYPT_SENSITIVE_DATA bootstrap-checks-xpack.html#bootstrap-checks-xpack-encrypt-sensitive-data +BOOTSTRAP_CHECK_PKI_REALM bootstrap-checks-xpack.html#bootstrap-checks-xpack-pki-realm +BOOTSTRAP_CHECK_ROLE_MAPPINGS bootstrap-checks-xpack.html#bootstrap-checks-xpack-role-mappings +BOOTSTRAP_CHECK_TLS bootstrap-checks-xpack.html#bootstrap-checks-tls +BOOTSTRAP_CHECK_TOKEN_SSL bootstrap-checks-xpack.html#bootstrap-checks-xpack-token-ssl +BOOTSTRAP_CHECK_SECURITY_MINIMAL_SETUP security-minimal-setup.html +CONTACT_SUPPORT troubleshooting.html#troubleshooting-contact-support +UNASSIGNED_SHARDS red-yellow-cluster-status.html +EXECUTABLE_JNA_TMPDIR executable-jna-tmpdir.html +NETWORK_THREADING_MODEL modules-network.html#modules-network-threading-model +ALLOCATION_EXPLAIN_API cluster-allocation-explain.html +NETWORK_BINDING_AND_PUBLISHING modules-network.html#modules-network-binding-publishing +SNAPSHOT_REPOSITORY_ANALYSIS repo-analysis-api.html +S3_COMPATIBLE_REPOSITORIES repository-s3.html#repository-s3-compatible-services +LUCENE_MAX_DOCS_LIMIT size-your-shards.html#troubleshooting-max-docs-limit +MAX_SHARDS_PER_NODE size-your-shards.html#troubleshooting-max-shards-open +FLOOD_STAGE_WATERMARK fix-watermark-errors.html +X_OPAQUE_ID api-conventions.html#x-opaque-id +FORMING_SINGLE_NODE_CLUSTERS modules-discovery-bootstrap-cluster.html#modules-discovery-bootstrap-cluster-joining diff --git a/server/src/test/java/org/elasticsearch/common/ReferenceDocsTests.java b/server/src/test/java/org/elasticsearch/common/ReferenceDocsTests.java index a5efd578df36c..ae28b83ae12fc 100644 --- a/server/src/test/java/org/elasticsearch/common/ReferenceDocsTests.java +++ b/server/src/test/java/org/elasticsearch/common/ReferenceDocsTests.java @@ -9,16 +9,17 @@ package org.elasticsearch.common; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.bytes.CompositeBytesReference; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xcontent.XContentParseException; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; +import java.io.IOException; +import java.io.InputStream; import static org.elasticsearch.common.ReferenceDocs.getVersionComponent; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; public class ReferenceDocsTests extends ESTestCase { @@ -39,119 +40,140 @@ public void testVersionComponent() { assertEquals("master", getVersionComponent("ABCDEF", true)); } - public void testReadsValidLinkDefinitions() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - for (ReferenceDocs link : ReferenceDocs.values()) { - builder.field(link.name(), "TEST"); - } - builder.endObject(); + private static final String TEST_LINK_PLACEHOLDER = "TEST_LINK"; - var map = ReferenceDocs.readLinksBySymbol(BytesReference.bytes(builder).streamInput()); - assertEquals(ReferenceDocs.values().length, map.size()); - for (ReferenceDocs link : ReferenceDocs.values()) { - assertEquals("TEST", map.get(link.name())); - } - } + private interface LinkSupplier { + String mutateLinkLine(int index, String lineWithPlaceholder); } - public void testRejectsInvalidJSON() throws Exception { - try (var stream = new ByteArrayInputStream("{\"invalid\":".getBytes(StandardCharsets.UTF_8))) { - expectThrows(XContentParseException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } - } - - public void testRejectsBadStructure() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - for (ReferenceDocs link : ReferenceDocs.values()) { - builder.field(link.name(), "TEST"); - } - builder.startObject("UNEXPECTED").endObject().endObject(); - - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalArgumentException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); + private static InputStream getResourceStream(LinkSupplier linkSupplier) { + final var stringBuilder = new StringBuilder(); + for (int i = 0; i < ReferenceDocs.values().length; i++) { + final var symbol = ReferenceDocs.values()[i]; + final var lineWithPlaceholder = symbol.name() + " ".repeat(ReferenceDocs.SYMBOL_COLUMN_WIDTH - symbol.name().length()) + + TEST_LINK_PLACEHOLDER; + final var updatedLine = linkSupplier.mutateLinkLine(i, lineWithPlaceholder); + if (updatedLine == null) { + break; + } else { + stringBuilder.append(updatedLine).append('\n'); } } + return new BytesArray(stringBuilder.toString()).streamInput(); } - public void testRejectsExtraSymbol() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - for (ReferenceDocs link : ReferenceDocs.values()) { - builder.field(link.name(), "TEST"); - } - builder.field("EXTRA", "TEST").endObject(); - - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } + public void testSuccess() throws IOException { + final var linksMap = ReferenceDocs.readLinksBySymbol(getResourceStream((i, l) -> l)); + assertEquals(ReferenceDocs.values().length, linksMap.size()); + for (ReferenceDocs link : ReferenceDocs.values()) { + assertEquals(TEST_LINK_PLACEHOLDER, linksMap.get(link.name())); } } - public void testRejectsMissingSymbol() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - var skipped = randomFrom(ReferenceDocs.values()); - for (ReferenceDocs link : ReferenceDocs.values()) { - if (link != skipped) { - builder.field(link.name(), "TEST"); - } - } - builder.endObject(); - - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } - } + public void testTruncated() { + final var targetLine = between(0, ReferenceDocs.values().length - 1); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol(getResourceStream((i, l) -> i == targetLine ? null : l)) + ).getMessage(), + equalTo("links resource truncated at line " + (targetLine + 1)) + ); } - public void testRejectsIncorrectOrder() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - var shuffled = Arrays.copyOf(ReferenceDocs.values(), ReferenceDocs.values().length); - var i = between(0, ReferenceDocs.values().length - 1); - var j = randomValueOtherThan(i, () -> between(0, ReferenceDocs.values().length - 1)); - var tmp = shuffled[i]; - shuffled[i] = shuffled[j]; - shuffled[j] = tmp; - - builder.startObject(); - for (ReferenceDocs link : shuffled) { - builder.field(link.name(), "TEST"); - } - builder.endObject(); + public void testMissingLink() { + final var targetLine = between(0, ReferenceDocs.values().length - 1); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol( + getResourceStream((i, l) -> i == targetLine ? l.replace(TEST_LINK_PLACEHOLDER, "") : l) + ) + ).getMessage(), + equalTo("no link found for [" + ReferenceDocs.values()[targetLine].name() + "] at line " + (targetLine + 1)) + ); + } - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } - } + public void testUnexpectedSymbol() { + final var targetSymbol = randomFrom(ReferenceDocs.values()).name(); + final var replacement = "x".repeat(targetSymbol.length()); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol(getResourceStream((i, l) -> l.replace(targetSymbol, replacement))) + ).getMessage(), + startsWith("unexpected symbol at line ") + ); } - public void testRejectsAutoGeneratedFragment() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - for (ReferenceDocs link : ReferenceDocs.values()) { - builder.field(link.name(), "test.html#_auto_generated_fragment"); - } - builder.endObject(); + public void testWhitespace() { + final var leadingWhitespaceLine = between(0, ReferenceDocs.values().length - 1); + final var trailingWhitespaceLine = between(0, ReferenceDocs.values().length - 1); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol( + getResourceStream( + (i, l) -> l.replace( + TEST_LINK_PLACEHOLDER, + (i == leadingWhitespaceLine ? " " : "") + TEST_LINK_PLACEHOLDER + (i == trailingWhitespaceLine ? " " : "") + ) + ) + ) + ).getMessage(), + startsWith("unexpected content at line " + (Math.min(leadingWhitespaceLine, trailingWhitespaceLine) + 1) + ": expected [") + ); + } - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } + public void testTrailingContent() throws IOException { + final byte[] validContent; + try (var stream = getResourceStream((i, l) -> l)) { + validContent = stream.readAllBytes(); } + final BytesReference contentWithTrailingData = CompositeBytesReference.of(new BytesArray(validContent), new BytesArray("x")); + + assertThat( + expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(contentWithTrailingData.streamInput())) + .getMessage(), + equalTo("unexpected trailing content at line " + (ReferenceDocs.values().length + 1)) + ); } - public void testRejectsAutoGeneratedPageName() throws Exception { - try (var builder = XContentFactory.jsonBuilder()) { - builder.startObject(); - for (ReferenceDocs link : ReferenceDocs.values()) { - builder.field(link.name(), "_auto_generated_page.html"); - } - builder.endObject(); + public void testRejectsAutoGeneratedFragment() { + final var targetLine = between(0, ReferenceDocs.values().length - 1); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol( + getResourceStream( + (i, l) -> i == targetLine ? l.replace(TEST_LINK_PLACEHOLDER, "test.html#_auto_generated_fragment") : l + ) + ) + ).getMessage(), + equalTo( + "found auto-generated fragment ID in link [test.html#_auto_generated_fragment] for [" + + ReferenceDocs.values()[targetLine].name() + + "] at line " + + (targetLine + 1) + ) + ); + } - try (var stream = BytesReference.bytes(builder).streamInput()) { - expectThrows(IllegalStateException.class, () -> ReferenceDocs.readLinksBySymbol(stream)); - } - } + public void testRejectsAutoGeneratedPageName() { + final var targetLine = between(0, ReferenceDocs.values().length - 1); + assertThat( + expectThrows( + IllegalStateException.class, + () -> ReferenceDocs.readLinksBySymbol( + getResourceStream((i, l) -> i == targetLine ? l.replace(TEST_LINK_PLACEHOLDER, "_auto_generated_page.html") : l) + ) + ).getMessage(), + equalTo( + "found auto-generated fragment ID in link [_auto_generated_page.html] for [" + + ReferenceDocs.values()[targetLine].name() + + "] at line " + + (targetLine + 1) + ) + ); } }