From 271b8cec28ebae35887adb37c36ff126389f3db7 Mon Sep 17 00:00:00 2001 From: gregce Date: Tue, 4 Feb 2020 08:33:28 -0800 Subject: [PATCH] $ blaze config: add --output=json This also generally refactors ConfigCommand to make a clean separation between structural logic and output formatting logic. ConfigCommand is responsible for taking a raw BuildConfiguration and translating it into appropriate data structures that represent what it wants to output: what data is output, how data relates to other data, and how results are ordered. An output formatter then consumes these data structures to style the output accordingly. This also moves the logic that handled both out of BuildConfiguration & FragmentOptions. It's neat how simple this makes JsonOutputFormatter. :) Serves https://github.com/bazelbuild/bazel/issues/10613. PiperOrigin-RevId: 293152292 --- .../java/com/google/devtools/build/lib/BUILD | 1 + .../analysis/config/BuildConfiguration.java | 30 -- .../lib/analysis/config/FragmentOptions.java | 11 - .../lib/runtime/commands/ConfigCommand.java | 423 +++++++++++++----- .../ConfigCommandOutputFormatter.java | 116 +++++ .../config/BuildConfigurationTest.java | 38 -- 6 files changed, 427 insertions(+), 192 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD index 86e186566c2763..59cf5ec5da616d 100644 --- a/src/main/java/com/google/devtools/build/lib/BUILD +++ b/src/main/java/com/google/devtools/build/lib/BUILD @@ -1320,6 +1320,7 @@ java_library( "//src/main/protobuf:test_status_java_proto", "//third_party:auto_value", "//third_party:flogger", + "//third_party:gson", "//third_party:guava", "//third_party:jsr305", "//third_party/protobuf:protobuf_java", diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java index 25d925aee7a77b..2932cb3c36b053 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java @@ -14,8 +14,6 @@ package com.google.devtools.build.lib.analysis.config; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.joining; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; @@ -208,34 +206,6 @@ private int computeHashCode() { return Objects.hash(fragments, buildOptions.getNativeOptions()); } - public void describe(StringBuilder sb) { - sb.append("BuildConfiguration ").append(checksum()).append(":\n"); - // Fragments. - sb.append(" fragments: ") - .append( - getFragmentsMap().keySet().stream() - .sorted(comparing(Class::getName)) - .map(Class::getName) - .collect(joining(","))) - .append("\n"); - // Options. - getOptions().getFragmentClasses().stream() - .sorted(comparing(Class::getName)) - .map(optionsClass -> getOptions().get(optionsClass)) - .forEach(options -> options.describe(sb)); - // User-defined options. - sb.append("Fragment user-defined {\n"); - buildOptions.getStarlarkOptions().entrySet().stream() - .sorted(Comparator.comparing(Map.Entry::getKey)) - .forEach( - entry -> - sb.append(" ") - .append(entry.getKey().toString()) - .append(": ") - .append(entry.getValue())); - sb.append("\n}"); - } - @Override public int hashCode() { return hashCode; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java index fb8027ffd32968..4f1de45c5fb4c4 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java @@ -14,7 +14,6 @@ package com.google.devtools.build.lib.analysis.config; -import static java.util.Map.Entry.comparingByKey; import com.google.common.collect.ImmutableMap; import com.google.devtools.common.options.OptionDefinition; @@ -136,14 +135,4 @@ public String getErrorMessage() { public Map getSelectRestrictions() { return ImmutableMap.of(); } - - public void describe(StringBuilder sb) { - sb.append("Fragment ").append(getClass().getName()).append(" {\n"); - Map options = asMap(); - options.entrySet().stream() - .sorted(comparingByKey()) - .forEach( - e -> sb.append(" ").append(e.getKey()).append(": ").append(e.getValue()).append("\n")); - sb.append("}\n"); - } } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java index 09b91b06ee66d3..99f08a2d8792d4 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java @@ -13,15 +13,14 @@ // limitations under the License. package com.google.devtools.build.lib.runtime.commands; -import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Comparator.comparing; -import static java.util.Map.Entry.comparingByKey; -import com.google.common.base.Functions; import com.google.common.collect.HashBasedTable; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; @@ -33,11 +32,14 @@ import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigOptions; +import com.google.devtools.build.lib.runtime.commands.ConfigCommandOutputFormatter.JsonOutputFormatter; +import com.google.devtools.build.lib.runtime.commands.ConfigCommandOutputFormatter.TextOutputFormatter; import com.google.devtools.build.lib.skyframe.BuildConfigurationValue; import com.google.devtools.build.lib.skyframe.SkyFunctions; import com.google.devtools.build.lib.util.ExitCode; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; +import com.google.devtools.common.options.EnumConverter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDefinition; import com.google.devtools.common.options.OptionDocumentationCategory; @@ -47,9 +49,11 @@ import com.google.devtools.common.options.OptionsParsingResult; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -66,6 +70,17 @@ hidden = true, help = "resource:config.txt") public class ConfigCommand implements BlazeCommand { + /** Defines the types of output this command can produce. */ + public enum OutputType { + TEXT("text"), + JSON("json"); + + private final String formatName; + + OutputType(String formatName) { + this.formatName = formatName; + } + } /** Options for the "config" command. */ public static class ConfigOptions extends OptionsBase { @@ -76,72 +91,132 @@ public static class ConfigOptions extends OptionsBase { effectTags = {OptionEffectTag.AFFECTS_OUTPUTS}, help = "If set, dump all known configurations instead of just the ids.") public boolean dumpAll; + + /** Converter for --output. */ + public static class OutputTypeConverter extends EnumConverter { + public OutputTypeConverter() { + super(OutputType.class, "output type"); + } + } + + @Option( + name = "output", + converter = OutputTypeConverter.class, + defaultValue = "text", + documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS, + effectTags = {OptionEffectTag.AFFECTS_OUTPUTS}, + help = "Formats the output of displayed results. Can be one of: 'text', 'json'. ") + public OutputType outputType; + } + + /** + * Data structure defining a {@link BuildConfiguration} from the point of this command's output. + * + *

Includes all data representing a "configuration" and defines their relative structure and + * list order. + * + *

A {@link ConfigCommandOutputFormatter} uses this to lightly format output from a logically + * consistent core structure. + */ + protected static class ConfigurationForOutput { + final String skyKey; + final String configHash; + final Collection fragments; + + ConfigurationForOutput( + String skyKey, String configHash, Collection fragments) { + this.skyKey = skyKey; + this.configHash = configHash; + this.fragments = fragments; + } + } + + /** + * Data structure defining a {@link FragmentOptions} from the point of this command's output. + * + *

See {@link ConfigurationForOutput} for further details. + */ + protected static class FragmentForOutput { + final String name; + final Map options; + + FragmentForOutput(String name, Map options) { + this.name = name; + this.options = options; + } + } + + /** + * Data structure defining the difference between two {@link BuildConfiguration}s from the point + * of this command's output. + * + *

See {@link ConfigurationForOutput} for further details. + */ + protected static class ConfigurationDiffForOutput { + final String configHash1; + final String configHash2; + final Collection fragmentsDiff; + + ConfigurationDiffForOutput( + String configHash1, String configHash2, Collection fragmentsDiff) { + this.configHash1 = configHash1; + this.configHash2 = configHash2; + this.fragmentsDiff = fragmentsDiff; + } + } + + /** + * Data structure defining the difference between two {@link BuildConfiguration}s for a given + * {@link FragmentOptions }from the point of this command's output. + * + *

See {@link ConfigurationForOutput} for further details. + */ + protected static class FragmentDiffForOutput { + final String name; + final Map> optionsDiff; + + FragmentDiffForOutput(String name, Map> optionsDiff) { + this.name = name; + this.optionsDiff = optionsDiff; + } } + /** + * Main entry point into the blaze config command. + * + *

Its purpose is to parse all options, figure out what variation of the command that implies, + * run the right logic, and return the right exit code. + */ @Override public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) { - ImmutableMap configurations = findConfigurations(env); + ImmutableSortedMap configurations = + findConfigurations(env); try (PrintWriter writer = new PrintWriter( new OutputStreamWriter(env.getReporter().getOutErr().getOutputStream(), UTF_8))) { + ConfigOptions configCommandOptions = options.getOptions(ConfigOptions.class); + ConfigCommandOutputFormatter outputFormatter = + configCommandOptions.outputType == OutputType.TEXT + ? new TextOutputFormatter(writer) + : new JsonOutputFormatter(writer); + if (options.getResidue().isEmpty()) { - if (options.getOptions(ConfigOptions.class).dumpAll) { - return reportAllConfigurations(writer, env); + if (configCommandOptions.dumpAll) { + return reportAllConfigurations(outputFormatter, forOutput(configurations)); } else { - return reportConfigurationIds(writer, configurations.keySet()); + return reportConfigurationIds(outputFormatter, forOutput(configurations)); } - } - - if (options.getResidue().size() == 1) { + } else if (options.getResidue().size() == 1) { String configHash = options.getResidue().get(0); - env.getReporter() - .handle(Event.info(String.format("Displaying config with id %s", configHash))); - - BuildConfiguration config = configurations.get(configHash); - if (config == null) { - env.getReporter() - .handle(Event.error(String.format("No configuration found with id: %s", configHash))); - return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); - } - - StringBuilder sb = new StringBuilder(); - config.describe(sb); - writer.print(sb.toString()); - - return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + return reportSingleConfiguration( + outputFormatter, env, forOutput(configurations), configHash); } else if (options.getResidue().size() == 2) { String configHash1 = options.getResidue().get(0); String configHash2 = options.getResidue().get(1); - env.getReporter() - .handle( - Event.info( - String.format( - "Displaying diff between configs" + " %s and" + " %s", - configHash1, configHash2))); - - BuildConfiguration config1 = configurations.get(configHash1); - if (config1 == null) { - env.getReporter() - .handle( - Event.error(String.format("No configuration found with id: %s", configHash1))); - return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); - } - BuildConfiguration config2 = configurations.get(configHash2); - if (config2 == null) { - env.getReporter() - .handle( - Event.error(String.format("No configuration found with id: %s", configHash2))); - return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); - } - - writer.printf( - "Displaying diff between configs" + " %s and" + " %s\n", configHash1, configHash2); - Table, String, Pair> diffs = - diffConfigurations(config1, config2); - writer.print(describeConfigDiff(diffs)); - return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + return reportConfigurationDiff( + configurations.values(), configHash1, configHash2, outputFormatter, env); } else { env.getReporter().handle(Event.error("Too many config ids.")); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); @@ -149,50 +224,172 @@ public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult opti } } - private ImmutableMap findConfigurations(CommandEnvironment env) { + /** + * Returns all {@link BuildConfiguration}s in Skyframe as a map from their {@link + * BuildConfigurationValue.Key} to instance. + */ + private static ImmutableSortedMap + findConfigurations(CommandEnvironment env) { InMemoryMemoizingEvaluator evaluator = (InMemoryMemoizingEvaluator) env.getRuntime().getWorkspace().getSkyframeExecutor().getEvaluatorForTesting(); return evaluator.getDoneValues().entrySet().stream() .filter(e -> SkyFunctions.BUILD_CONFIGURATION.equals(e.getKey().functionName())) - .map(Map.Entry::getValue) - .map(v -> (BuildConfigurationValue) v) - .map(BuildConfigurationValue::getConfiguration) .collect( - toImmutableMap( - BuildConfiguration::checksum, Functions.identity(), (config1, config2) -> config1)); + toImmutableSortedMap( + comparing(BuildConfigurationValue.Key::toString), + e -> (BuildConfigurationValue.Key) e.getKey(), + e -> ((BuildConfigurationValue) e.getValue()).getConfiguration())); } - private BlazeCommandResult reportAllConfigurations(PrintWriter writer, CommandEnvironment env) { - InMemoryMemoizingEvaluator evaluator = - (InMemoryMemoizingEvaluator) - env.getRuntime().getWorkspace().getSkyframeExecutor().getEvaluatorForTesting(); - ImmutableMap configs = - evaluator.getDoneValues().entrySet().stream() - .filter(e -> SkyFunctions.BUILD_CONFIGURATION.equals(e.getKey().functionName())) - .collect( - toImmutableMap( - e -> (BuildConfigurationValue.Key) e.getKey(), - e -> (BuildConfigurationValue) e.getValue())); - - for (Map.Entry entry : - configs.entrySet()) { - writer.print("BuildConfigurationValue.Key: "); - writer.println(entry.getKey().toString()); - - writer.print("BuildConfigurationValue:\n"); - StringBuilder sb = new StringBuilder(); - entry.getValue().getConfiguration().describe(sb); - writer.print(sb.toString()); + /** + * Converts {@link #findConfigurations}'s output into a list of {@link ConfigurationForOutput} + * instances. + */ + private ImmutableSortedSet forOutput( + ImmutableSortedMap asSkyKeyMap) { + ImmutableSortedSet.Builder ans = + ImmutableSortedSet.orderedBy(comparing(e -> e.configHash)); + for (Map.Entry entry : + asSkyKeyMap.entrySet()) { + BuildConfigurationValue.Key key = entry.getKey(); + BuildConfiguration config = entry.getValue(); + ans.add(getConfigurationForOutput(key, config.checksum(), config)); + } + return ans.build(); + } + + /** Constructs a {@link ConfigurationForOutput} from the given input daata. */ + ConfigurationForOutput getConfigurationForOutput( + BuildConfigurationValue.Key skyKey, String configHash, BuildConfiguration config) { + ImmutableSortedSet.Builder fragments = + ImmutableSortedSet.orderedBy(comparing(e -> e.getClass().getName())); + config.getOptions().getFragmentClasses().stream() + .map(optionsClass -> config.getOptions().get(optionsClass)) + .forEach( + fragmentOptions -> { + fragments.add( + new FragmentForOutput( + fragmentOptions.getClass().getName(), + getOrderedNativeOptions(fragmentOptions))); + }); + fragments.add( + new FragmentForOutput( + UserDefinedFragment.DESCRIPTIVE_NAME, getOrderedUserDefinedOptions(config))); + return new ConfigurationForOutput(skyKey.toString(), configHash, fragments.build()); + } + + /** + * Returns a {@link FragmentOptions}'s native option settings in canonical order. + * + *

While actual option values are objects, we serialize them to strings to prevent command + * output from interpreting them more deeply than we want for simple "name=value" output. + */ + private static ImmutableSortedMap getOrderedNativeOptions( + FragmentOptions options) { + return options.asMap().entrySet().stream() + .collect( + toImmutableSortedMap( + Ordering.natural(), e -> e.getKey(), e -> String.valueOf(e.getValue()))); + } + + /** + * Returns a configuration's Starlark settings in canonical order. + * + *

While actual option values are objects, we serialize them to strings to prevent command + * output from interpreting them more deeply than we want for simple "name=value" output. + */ + private static ImmutableSortedMap getOrderedUserDefinedOptions( + BuildConfiguration config) { + return config.getOptions().getStarlarkOptions().entrySet().stream() + .collect( + toImmutableSortedMap(Ordering.natural(), e -> e.toString(), e -> String.valueOf(e))); + } + + /** + * Reports the result of blaze config --dump_all and returns the appropriate command + * exit code. + */ + private static BlazeCommandResult reportAllConfigurations( + ConfigCommandOutputFormatter writer, + ImmutableSortedSet configurations) { + for (ConfigurationForOutput config : configurations) { + writer.writeConfiguration(config); } return BlazeCommandResult.exitCode(ExitCode.SUCCESS); } + /** + * Reports the result of blaze config and returns the appropriate command exit code. + */ private BlazeCommandResult reportConfigurationIds( - PrintWriter writer, ImmutableSet configurationIds) { - writer.println("Available configurations:"); - writer.println(configurationIds.stream().collect(Collectors.joining("\n"))); + ConfigCommandOutputFormatter writer, + ImmutableSortedSet configurations) { + writer.writeConfigurationIDs( + configurations.stream().map(config -> config.configHash).collect(Collectors.toList())); + return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + } + + /** + * Reports the result of blaze config and returns the appropriate + * command exit code. + */ + private static BlazeCommandResult reportSingleConfiguration( + ConfigCommandOutputFormatter writer, + CommandEnvironment env, + ImmutableSortedSet allConfigurations, + String configHash) { + env.getReporter().handle(Event.info(String.format("Displaying config with id %s", configHash))); + + Optional match = + allConfigurations.stream().filter(entry -> entry.configHash.equals(configHash)).findFirst(); + + if (!match.isPresent()) { + env.getReporter() + .handle(Event.error(String.format("No configuration found with id: %s", configHash))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + + writer.writeConfiguration(match.get()); + return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + } + /** + * Reports the result of blaze config and returns the + * appropriate command exit code. + */ + private static BlazeCommandResult reportConfigurationDiff( + Collection allConfigs, + String configHash1, + String configHash2, + ConfigCommandOutputFormatter writer, + CommandEnvironment env) { + env.getReporter() + .handle( + Event.info( + String.format( + "Displaying diff between configs" + " %s and" + " %s", + configHash1, configHash2))); + + Optional config1 = + allConfigs.stream().filter(config -> config.checksum().equals(configHash1)).findFirst(); + + if (!config1.isPresent()) { + env.getReporter() + .handle(Event.error(String.format("No configuration found with id: %s", configHash1))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + Optional config2 = + allConfigs.stream().filter(config -> config.checksum().equals(configHash2)).findFirst(); + if (!config2.isPresent()) { + env.getReporter() + .handle(Event.error(String.format("No configuration found with id: %s", configHash2))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + + Table, String, Pair> diffs = + diffConfigurations(config1.get(), config2.get()); + writer.writeConfigurationDiff(getConfigurationDiffForOutput(configHash1, configHash2, diffs)); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); } @@ -201,11 +398,12 @@ private BlazeCommandResult reportConfigurationIds( * consistent with native options, i.e. to include "user-defined" section in the output list. */ private static class UserDefinedFragment extends FragmentOptions { + static final String DESCRIPTIVE_NAME = "user-defined"; // Intentionally empty: we read the actual options directly from BuildOptions. } - private Table, String, Pair> diffConfigurations( - BuildConfiguration config1, BuildConfiguration config2) { + private static Table, String, Pair> + diffConfigurations(BuildConfiguration config1, BuildConfiguration config2) { Table, String, Pair> diffs = HashBasedTable.create(); @@ -254,33 +452,32 @@ private static Map> diffStarlarkOptions( return diffs; } - private static String describeConfigDiff( - Table, String, Pair> diff) { - StringBuilder sb = new StringBuilder(); - - diff.rowKeySet().stream() - .sorted(comparing(Class::getName)) - .forEach(fragmentClass -> displayFragmentDiff(fragmentClass, diff.row(fragmentClass), sb)); - - return sb.toString(); + private static ConfigurationDiffForOutput getConfigurationDiffForOutput( + String configHash1, + String configHash2, + Table, String, Pair> diffs) { + ImmutableSortedSet.Builder fragmentDiffs = + ImmutableSortedSet.orderedBy(comparing(e -> e.getClass().getName())); + diffs.rowKeySet().stream() + .forEach( + fragmentClass -> { + String fragmentName = + fragmentClass.equals(UserDefinedFragment.class) + ? UserDefinedFragment.DESCRIPTIVE_NAME + : fragmentClass.getName(); + ImmutableSortedMap> sortedOptionDiffs = + diffs.row(fragmentClass).entrySet().stream() + .collect( + toImmutableSortedMap( + Ordering.natural(), + Map.Entry::getKey, + e -> toNullableStringPair(e.getValue()))); + fragmentDiffs.add(new FragmentDiffForOutput(fragmentName, sortedOptionDiffs)); + }); + return new ConfigurationDiffForOutput(configHash1, configHash2, fragmentDiffs.build()); } - private static void displayFragmentDiff( - Class fragmentClass, - Map> diff, - StringBuilder sb) { - sb.append("Fragment ").append(fragmentClass.getName()).append(" {\n"); - diff.entrySet().stream() - .sorted(comparingByKey()) - .forEach( - e -> - sb.append(" ") - .append(e.getKey()) - .append(": ") - .append(e.getValue().getFirst()) - .append(", ") - .append(e.getValue().getSecond()) - .append("\n")); - sb.append("}\n"); + private static Pair toNullableStringPair(Pair pair) { + return Pair.of(String.valueOf(pair.first), String.valueOf(pair.second)); } } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java new file mode 100644 index 00000000000000..b4b9db5e55d43d --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java @@ -0,0 +1,116 @@ +// Copyright 2019 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.runtime.commands; + +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigurationDiffForOutput; +import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigurationForOutput; +import com.google.devtools.build.lib.runtime.commands.ConfigCommand.FragmentDiffForOutput; +import com.google.devtools.build.lib.runtime.commands.ConfigCommand.FragmentForOutput; +import com.google.devtools.build.lib.util.Pair; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.Map; + +/** + * Formats output for {@link ConfigCommand}. + * + *

The basic contract is @link ConfigCommand} makes all important structural decisions: what data + * gets reported, how different pieces of data relate to each other, and how data is ordered. A + * {@link ConfigCommandOutputFormatter} then outputs this in a format-appropriate way. + */ +abstract class ConfigCommandOutputFormatter { + protected final PrintWriter writer; + + /** Constructs a formatter that writes output to the given {@link PrintWriter}. */ + ConfigCommandOutputFormatter(PrintWriter writer) { + this.writer = writer; + } + + /** Outputs a list of configuration hash IDS. * */ + public abstract void writeConfigurationIDs(Iterable configurationIDs); + + /** Outputs a single configuration. * */ + public abstract void writeConfiguration(ConfigurationForOutput configuration); + + /** Outputs the diff between two configurations * */ + public abstract void writeConfigurationDiff(ConfigurationDiffForOutput diff); + + /** A {@link ConfigCommandOutputFormatter} that outputs plan user-readable text. */ + static class TextOutputFormatter extends ConfigCommandOutputFormatter { + TextOutputFormatter(PrintWriter writer) { + super(writer); + } + + @Override + public void writeConfigurationIDs(Iterable configurationIDs) { + writer.println("Available configurations:"); + configurationIDs.forEach(id -> writer.println(id)); + } + + @Override + public void writeConfiguration(ConfigurationForOutput configuration) { + writer.println("BuildConfiguration " + configuration.configHash + ":"); + writer.println("Skyframe Key: " + configuration.skyKey); + for (FragmentForOutput fragment : configuration.fragments) { + writer.println("Fragment " + fragment.name + " {"); + for (Map.Entry optionSetting : fragment.options.entrySet()) { + writer.printf(" %s: %s", optionSetting.getKey(), optionSetting.getValue()); + } + writer.println("}"); + } + } + + @Override + public void writeConfigurationDiff(ConfigurationDiffForOutput diff) { + writer.printf( + "Displaying diff between configs %s and %s", diff.configHash1, diff.configHash2); + for (FragmentDiffForOutput fragmentDiff : diff.fragmentsDiff) { + writer.println("Fragment " + fragmentDiff.name + " {"); + for (Map.Entry> optionDiff : + fragmentDiff.optionsDiff.entrySet()) { + writer.printf( + " %s: %s, %s", + optionDiff.getKey(), optionDiff.getValue().first, optionDiff.getValue().second); + } + writer.println("}"); + } + } + } + + /** A {@link ConfigCommandOutputFormatter} that outputs structured JSON. */ + static class JsonOutputFormatter extends ConfigCommandOutputFormatter { + private final Gson gson; + + JsonOutputFormatter(PrintWriter writer) { + super(writer); + this.gson = new Gson(); + } + + @Override + public void writeConfigurationIDs(Iterable configurationIDs) { + writer.println(gson.toJson(ImmutableMap.of("configuration-IDs", configurationIDs))); + } + + @Override + public void writeConfiguration(ConfigurationForOutput configuration) { + writer.println(gson.toJson(configuration)); + } + + @Override + public void writeConfigurationDiff(ConfigurationDiffForOutput diff) { + writer.println(gson.toJson(diff)); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationTest.java b/src/test/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationTest.java index 0d18c277fc72fe..c1feadb16dbe5d 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationTest.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationTest.java @@ -31,7 +31,6 @@ import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.common.options.Options; import java.util.Map; -import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -381,41 +380,4 @@ private static void verifyDeserialized( BuildConfiguration subject, BuildConfiguration deserialized) { assertThat(deserialized.getOptions()).isEqualTo(subject.getOptions()); } - - @Test - public void testDescribe() throws Exception { - scratch.file( - "test/defs.bzl", - "def _rule_impl(ctx):", - " return []", - "string_flag = rule(", - " implementation = _rule_impl,", - " build_setting = config.string()", - ")"); - scratch.file( - "test/BUILD", - "load('//test:defs.bzl', 'string_flag')", - "string_flag(", - " name = 'my_flag1',", - " build_setting_default = '')", - "string_flag(", - " name = 'my_flag2',", - " build_setting_default = '')"); - - BuildConfiguration config = create(ImmutableMap.of("//test:my_flag1", "custom")); - StringBuilder sb = new StringBuilder(); - config.describe(sb); - String desc = sb.toString(); - - // Just sample the expected lines. Testing against the full expected output would be too spammy - // and also brittle, as fragments and their options and default option values change. - String expected = - "fragments: .*com.google.devtools.build.lib.analysis.PlatformConfiguration.*" - + "Fragment com.google.devtools.build.lib.analysis.PlatformOptions \\{.*" - + "Fragment user-defined \\{.*" - + " //test:my_flag1: custom.*" - + "\\}"; - - assertThat(desc).containsMatch(Pattern.compile(expected, Pattern.DOTALL)); - } }