View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.File;
25  import java.lang.reflect.Field;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.LinkedHashMap;
29  import java.util.Map;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  
33  import javax.xml.parsers.DocumentBuilder;
34  import javax.xml.parsers.DocumentBuilderFactory;
35  
36  import org.junit.jupiter.api.Test;
37  import org.junit.jupiter.api.io.TempDir;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.Element;
40  import org.w3c.dom.NodeList;
41  
42  /**
43   * Verifies that {@link SearchIndexGenerator}'s check category display names
44   * stay in sync with the labels actually shown in the site's sidebar
45   * navigation ({@code src/site/site.xml}).
46   *
47   * <p>A mismatch here means search results would show a different label for
48   * a category than the sidebar navigation does for the same category, which
49   * is confusing to users and easy to miss in review - this was flagged as a
50   * gap on the {@code SearchIndexGenerator} PR (issue #16214).</p>
51   */
52  public class SearchIndexGeneratorTest {
53  
54      /**
55       * Path to the Maven site navigation descriptor, relative to the project
56       * root. Adjust if the descriptor lives elsewhere in this checkout.
57       */
58      private static final String SITE_XML_PATH = "src/site/site.xml";
59  
60      /**
61       * Matches a sidebar {@code <item>}'s {@code href} of the form
62       * {@code checks/<category>/index.html}, capturing the lowercase category
63       * directory name in group 1. Per-check pages (e.g.
64       * {@code checks/blocks/emptyblock.html}) intentionally do not match.
65       */
66      private static final Pattern CHECKS_CATEGORY_HREF =
67              Pattern.compile("^checks/([a-z]+)/index\\.html$");
68  
69      /** Name of the field under test on {@link SearchIndexGenerator}. */
70      private static final String CATEGORY_FIELD_NAME = "CHECKS_CATEGORY_DISPLAY_NAMES";
71  
72      private static String minimalXdoc(String sectionName) {
73          return "<?xml version=\"1.0\"?>\n"
74                  + "<document>\n"
75                  + "  <body>\n"
76                  + "    <section name=\"" + sectionName + "\">\n"
77                  + "      <subsection name=\"Description\">\n"
78                  + "        <p>Description of " + sectionName + ".</p>\n"
79                  + "      </subsection>\n"
80                  + "    </section>\n"
81                  + "  </body>\n"
82                  + "</document>\n";
83      }
84  
85      @Test
86      public void checksCategoryDisplayNamesMatchSidebarNavigation() throws Exception {
87          final Map<String, String> navCategoryNames = readNavCategoryNames();
88          final Map<String, String> generatorCategoryNames = readGeneratorCategoryNames();
89  
90          assertWithMessage("No 'checks/<category>/index.html' entries found in " + SITE_XML_PATH
91                          + " - has the nav structure or href pattern changed?")
92                  .that(navCategoryNames.isEmpty())
93                  .isFalse();
94  
95          assertWithMessage("SearchIndexGenerator." + CATEGORY_FIELD_NAME + " must match the check"
96                          + " category labels shown in the sidebar navigation ("
97                          + SITE_XML_PATH + "). Update whichever one is out of date.")
98                  .that(generatorCategoryNames)
99                  .isEqualTo(navCategoryNames);
100     }
101 
102     /**
103      * Reads the directory-name-to-display-name map directly from
104      * {@link SearchIndexGenerator}'s private {@code CHECKS_CATEGORY_DISPLAY_NAMES}
105      * field via reflection, so the test can catch drift without needing to
106      * widen that field's visibility just for testing.
107      *
108      * @return the generator's category display name map
109      * @throws ReflectiveOperationException if the field cannot be accessed
110      */
111     @SuppressWarnings("unchecked")
112     private static Map<String, String> readGeneratorCategoryNames()
113             throws ReflectiveOperationException {
114         final Field field = SearchIndexGenerator.class.getDeclaredField(CATEGORY_FIELD_NAME);
115         field.setAccessible(true);
116         return (Map<String, String>) field.get(null);
117     }
118 
119     /**
120      * Parses {@code site.xml} and extracts the check category display name
121      * for every {@code checks/<category>/index.html} entry found anywhere in
122      * the navigation, keyed by lowercase directory name.
123      *
124      * @return map of directory name to sidebar display label
125      * @throws Exception if site.xml cannot be found or parsed
126      */
127     private static Map<String, String> readNavCategoryNames() throws Exception {
128         final Map<String, String> result = new LinkedHashMap<>();
129         final File siteXml = new File(SITE_XML_PATH);
130 
131         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
132         final DocumentBuilder builder = factory.newDocumentBuilder();
133         final Document doc = builder.parse(siteXml);
134 
135         final NodeList items = doc.getElementsByTagName("item");
136         for (int index = 0; index < items.getLength(); index++) {
137             final Element item = (Element) items.item(index);
138             final String href = item.getAttribute("href");
139             final Matcher matcher = CHECKS_CATEGORY_HREF.matcher(href);
140             if (matcher.matches()) {
141                 final String dirName = matcher.group(1);
142                 final String displayName = item.getAttribute("name");
143                 result.put(dirName, displayName);
144             }
145         }
146         return result;
147     }
148 
149     /**
150      * Release note pages ({@code release-notes.xml},
151      * {@code release-notes-old-*.xml}) must not appear in the search index.
152      * Before the fix, all plain {@code .xml} files in the xdocs root were
153      * processed without exclusion, causing release note entries to pollute
154      * search results. The fix adds a
155      * {@code !name.startsWith("release-notes")} predicate in
156      * This test ensures that predicate is never accidentally removed.
157      */
158     @Test
159     public void testReleaseNotesAreExcludedFromSearchIndex(@TempDir Path tempDir) throws Exception {
160         final Path xdocsDir = tempDir.resolve("xdocs");
161         Files.createDirectories(xdocsDir);
162 
163         Files.writeString(xdocsDir.resolve("release-notes.xml"),
164                 minimalXdoc("Release 10.21.0"));
165         Files.writeString(xdocsDir.resolve("release-notes-old-8-0-8-34.xml"),
166                 minimalXdoc("Release 8.0 to 8.34"));
167 
168         final Path outputFile = tempDir.resolve("search-index.json");
169 
170         SearchIndexGenerator.main(
171                 xdocsDir.toString(),
172                 outputFile.toString()
173         );
174 
175         final String output = Files.readString(outputFile);
176 
177         assertWithMessage(
178                 "Release note pages must not produce entries in the search index")
179                 .that(output)
180                 .doesNotContain("release-notes");
181     }
182 
183 }