You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@flink.apache.org by aljoscha <gi...@git.apache.org> on 2018/05/03 08:09:39 UTC

[GitHub] flink pull request #5515: [FLINK-8683][docs] Add test for configuration docs...

Github user aljoscha commented on a diff in the pull request:

    https://github.com/apache/flink/pull/5515#discussion_r185720854
  
    --- Diff: flink-docs/src/test/java/org/apache/flink/docs/configuration/ConfigOptionsDocsCompletenessTest.java ---
    @@ -0,0 +1,225 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + * or more contributor license agreements.  See the NOTICE file
    + * distributed with this work for additional information
    + * regarding copyright ownership.  The ASF licenses this file
    + * to you 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 org.apache.flink.docs.configuration;
    +
    +import org.apache.flink.configuration.ConfigOption;
    +
    +import org.jsoup.Jsoup;
    +import org.jsoup.nodes.Document;
    +import org.junit.Assert;
    +import org.junit.Test;
    +
    +import java.io.IOException;
    +import java.nio.charset.StandardCharsets;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.nio.file.Paths;
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.stream.Collectors;
    +import java.util.stream.Stream;
    +
    +import static org.apache.flink.docs.configuration.ConfigOptionsDocGenerator.LOCATIONS;
    +import static org.apache.flink.docs.configuration.ConfigOptionsDocGenerator.stringifyDefault;
    +import static org.apache.flink.docs.configuration.ConfigOptionsDocGenerator.extractConfigOptions;
    +import static org.apache.flink.docs.configuration.ConfigOptionsDocGenerator.processConfigOptions;
    +
    +/**
    + * This test verifies that all {@link ConfigOption ConfigOptions} in the configured
    + * {@link ConfigOptionsDocGenerator#LOCATIONS locations} are documented and well-defined (i.e. no 2 options exist for
    + * the same key with different descriptions/default values), and that the documentation does not refer to non-existent
    + * options.
    + */
    +public class ConfigOptionsDocsCompletenessTest {
    +
    +	@Test
    +	public void testDocsCompleteness() throws IOException, ClassNotFoundException {
    +		Map<String, DocumentedOption> documentedOptions = parseDocumentedOptions();
    +		Map<String, ExistingOption> existingOptions = findExistingOptions();
    +
    +		final Collection<String> problems = new ArrayList<>(0);
    +
    +		// first check that all existing options are properly documented
    +		existingOptions.forEach((key, supposedState) -> {
    +			DocumentedOption documentedState = documentedOptions.remove(key);
    +
    +			// if nothing matches the docs for this option are up-to-date
    +			if (documentedState == null) {
    +				// option is not documented at all
    +				problems.add("Option " + supposedState.key + " in " + supposedState.containingClass + " is not documented.");
    +			} else if (!supposedState.defaultValue.equals(documentedState.defaultValue)) {
    +				// default is outdated
    +				problems.add("Documented default of " + supposedState.key + " in " + supposedState.containingClass +
    +					" is outdated. Expected: " + supposedState.defaultValue + " Actual: " + documentedState.defaultValue);
    +			} else if (!supposedState.description.equals(documentedState.description)) {
    +				// description is outdated
    +				problems.add("Documented description of " + supposedState.key + " in " + supposedState.containingClass +
    +					" is outdated.");
    +			}
    +		});
    +
    +		// documentation contains an option that no longer exists
    +		if (!documentedOptions.isEmpty()) {
    +			for (DocumentedOption documentedOption : documentedOptions.values()) {
    +				problems.add("Documented option " + documentedOption.key + " does not exist.");
    +			}
    +		}
    +
    +		if (!problems.isEmpty()) {
    +			StringBuilder sb = new StringBuilder("Documentation is outdated, please regenerate it according to the" +
    +				" instructions in flink-docs/README.md.");
    +			sb.append(System.lineSeparator());
    +			sb.append("\tProblems:");
    +			for (String problem : problems) {
    +				sb.append(System.lineSeparator());
    +				sb.append("\t\t");
    +				sb.append(problem);
    +			}
    +			Assert.fail(sb.toString());
    +		}
    +	}
    +
    +	private static Map<String, DocumentedOption> parseDocumentedOptions() throws IOException {
    +		Path includeFolder = Paths.get("..", "docs", "_includes", "generated").toAbsolutePath();
    +		return Files.list(includeFolder)
    +			.filter(path -> path.getFileName().toString().contains("configuration"))
    +			.flatMap(file -> {
    +				try {
    +					return parseDocumentedOptionsFromFile(file).stream();
    +				} catch (IOException ignored) {
    +					return Stream.empty();
    +				}
    +			})
    +			.collect(Collectors.toMap(option -> option.key, option -> option, (option1, option2) -> {
    +				if (option1.equals(option2)) {
    +					// we allow multiple instances of ConfigOptions with the same key if they are identical
    +					return option1;
    +				} else {
    +					// found a ConfigOption pair with the same key that aren't equal
    +					// we fail here outright as this is not a documentation-completeness problem
    +					if (!option1.defaultValue.equals(option2.defaultValue)) {
    +						throw new AssertionError("Documentation contains distinct defaults for " +
    +							option1.key + " in " + option1.containingFile + " and " + option2.containingFile + '.');
    +					} else {
    +						throw new AssertionError("Documentation contains distinct descriptions for " +
    +							option1.key + " in " + option1.containingFile + " and " + option2.containingFile + '.');
    +					}
    +				}
    +			}));
    +	}
    +
    +	private static Collection<DocumentedOption> parseDocumentedOptionsFromFile(Path file) throws IOException {
    +		Document document = Jsoup.parse(file.toFile(), StandardCharsets.UTF_8.name());
    +		return document.getElementsByTag("table").stream()
    +			.map(element -> element.getElementsByTag("tbody").get(0))
    +			.flatMap(element -> element.getElementsByTag("tr").stream())
    +			.map(tableRow -> {
    +				String key = tableRow.child(0).text();
    +				String defaultValue = tableRow.child(1).text();
    +				String description = tableRow.child(2).text();
    +				return new DocumentedOption(key, defaultValue, description, file.getName(file.getNameCount() - 1));
    +			})
    +			.collect(Collectors.toList());
    +	}
    +
    +	private static Map<String, ExistingOption> findExistingOptions() throws IOException, ClassNotFoundException {
    +		Map<String, ExistingOption> existingOptions = new HashMap<>(32);
    +
    +		for (OptionsClassLocation location : LOCATIONS) {
    +			processConfigOptions("..", location.getModule(), location.getPackage(), optionsClass -> {
    +				List<ConfigOptionsDocGenerator.OptionWithMetaInfo> configOptions = extractConfigOptions(optionsClass);
    +				for (ConfigOptionsDocGenerator.OptionWithMetaInfo option : configOptions) {
    +					String key = option.option.key();
    +					String defaultValue = stringifyDefault(option);
    +					String description = option.option.description();
    +					ExistingOption duplicate = existingOptions.put(key, new ExistingOption(key, defaultValue, description, optionsClass));
    +					if (duplicate != null) {
    +						// multiple documented options have the same key
    +						// we fail here outright as this is not a documentation-completeness problem
    +						if (!(duplicate.description.equals(description))) {
    +							throw new AssertionError("Ambiguous option " + key + " due to distinct descriptions.");
    +						} else if (!duplicate.defaultValue.equals(defaultValue)) {
    +							throw new AssertionError("Ambiguous option " + key + " due to distinct default values (" + defaultValue + " vs " + duplicate.defaultValue + ").");
    +						}
    +					}
    +				}
    +			});
    +		}
    +
    +		return existingOptions;
    +	}
    +
    +	private static final class ExistingOption extends Option {
    +
    +		private final Class<?> containingClass;
    +
    +		private ExistingOption(String key, String defaultValue, String description, Class<?> containingClass) {
    +			super(key, defaultValue, description);
    +			this.containingClass = containingClass;
    +		}
    +	}
    +
    +	private static final class DocumentedOption extends Option {
    +
    +		private final Path containingFile;
    +
    +		private DocumentedOption(String key, String defaultValue, String description, Path containingFile) {
    +			super(key, defaultValue, description);
    +			this.containingFile = containingFile;
    +		}
    +	}
    +
    +	private abstract static class Option {
    +		protected final String key;
    +		protected final String defaultValue;
    +		protected final String description;
    +
    +		private Option(String key, String defaultValue, String description) {
    +			this.key = key;
    +			this.defaultValue = defaultValue;
    +			this.description = description;
    +		}
    +
    +		@Override
    +		public int hashCode() {
    +			return key.hashCode() + defaultValue.hashCode() + description.hashCode();
    +		}
    +
    +		@Override
    +		public boolean equals(Object obj) {
    +			if (!(obj instanceof Option)) {
    +				return false;
    +			}
    +
    +			Option other = (Option) obj;
    +
    +			return this.key.equals(other.key)
    +				&& this.defaultValue.equals(other.defaultValue)
    +				&& this.description.equals(other.description);
    +		}
    +
    +		@Override
    +		public String toString() {
    +			return "DocumentedOption(key=" + key + ", default=" + defaultValue + ", description=" + description + ')';
    --- End diff --
    
    This should be `"Option(...`?


---