diff --git a/README.md b/README.md index 9e0303eab..33f586003 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,7 @@ A scenario can define changes that should be applied to the source before each b - `clear-project-cache-before`: Deletes the contents of the `.gradle` and `buildSrc/.gradle` project cache directories before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`). - `clear-transform-cache-before`: Deletes the contents of the transform cache before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`). - `clear-jars-cache-before`: Deletes the contents of the instrumented jars cache before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`). +- `execute-command-before`: Execute a command before the scenario is executed (SCENARIO) or before the build is executed (BUILD). This can be applied to Gradle, Bazel, Buck, or Maven builds. - `git-checkout`: Checks out a specific commit for the build step, and a different one for the cleanup step. - `git-revert`: Reverts a given set of commits before the build and resets it afterward. - `iterations`: Number of builds to actually measure @@ -325,11 +326,43 @@ You can compare Gradle against Bazel, Buck, and Maven by specifying their equiva build_some_target { tasks = ["assemble"] + execute-command-before = [ + # Gradle specific executions + # Execute a command in the bash shell + { + schedule = BUILD + commands = ["/bin/bash","-c","echo helloworld"] + }, + } bazel { # If empty, it will be infered from BAZEL_HOME environment variable home = "/path/to/bazel/home" targets = ["build" "//some/target"] + execute-command-before = [ + # Bazel specific executions + # execute a command prior to the scenario running. + { + schedule = SCENARIO + # Note: Ensure that this is calling the same Bazel that is used in the benchmarks + commands = ["bazel","clean","--expunge"] + }, + # Execute a command in the bash shell + { + schedule = BUILD + commands = ["/bin/bash","-c","echo helloworld"] + }, + # Remove the contents of the remote cache Bazel bucket + { + schedule = BUILD + commands = ["gsutil","-o","Credentials:gs_service_key_file=bazel-benchmark-bucket.json","-m","rm","gs://bazel-benchmark-bucket/**"] + }, + # Display the total size of the bucket + { + schedule = BUILD + commands = ["gsutil","-o","Credentials:gs_service_key_file=bazel-benchmark-bucket.json","du","-sh","gs://bazel-benchmark-bucket"] + }, + ] } } @@ -344,6 +377,14 @@ You can compare Gradle against Bazel, Buck, and Maven by specifying their equiva # If empty, it will be infered from BUCK_HOME environment variable home = "/path/to/buck/home" type = "android_binary" // can be a Buck build rule type or "all" + execute-command-before = [ + # Buck specific executions + # Execute a command in the bash shell + { + schedule = BUILD + commands = ["/bin/bash","-c","echo helloworld"] + }, + } } } build_resources { diff --git a/src/main/java/org/gradle/profiler/ScenarioLoader.java b/src/main/java/org/gradle/profiler/ScenarioLoader.java index 6d6d3fa63..87ba8b423 100644 --- a/src/main/java/org/gradle/profiler/ScenarioLoader.java +++ b/src/main/java/org/gradle/profiler/ScenarioLoader.java @@ -6,6 +6,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigParseOptions; +import java.util.Collections; import org.gradle.profiler.mutations.ApplyAbiChangeToSourceFileMutator; import org.gradle.profiler.mutations.ApplyChangeToAndroidLayoutFileMutator; import org.gradle.profiler.mutations.ApplyChangeToAndroidManifestFileMutator; @@ -15,6 +16,7 @@ import org.gradle.profiler.mutations.ApplyNonAbiChangeToSourceFileMutator; import org.gradle.profiler.mutations.ApplyValueChangeToAndroidResourceFileMutator; import org.gradle.profiler.mutations.BuildMutatorConfigurator; +import org.gradle.profiler.mutations.ExecuteCommandBuildMutator; import org.gradle.profiler.mutations.ClearArtifactTransformCacheMutator; import org.gradle.profiler.mutations.ClearBuildCacheMutator; import org.gradle.profiler.mutations.ClearConfigurationCacheStateMutator; @@ -37,6 +39,10 @@ import java.util.stream.Collectors; class ScenarioLoader { + + static final String BAZEL = "bazel"; + static final String BUCK = "buck"; + static final String MAVEN = "maven"; private static final String TITLE = "title"; private static final String VERSIONS = "versions"; private static final String TASKS = "tasks"; @@ -45,9 +51,6 @@ class ScenarioLoader { private static final String RUN_USING = "run-using"; private static final String DAEMON = "daemon"; private static final String SYSTEM_PROPERTIES = "system-properties"; - private static final String BAZEL = "bazel"; - private static final String BUCK = "buck"; - private static final String MAVEN = "maven"; private static final String TOOL_HOME = "home"; private static final String WARM_UP_COUNT = "warm-ups"; private static final String ITERATIONS = "iterations"; @@ -61,6 +64,7 @@ class ScenarioLoader { private static final String APPLY_ANDROID_MANIFEST_CHANGE_TO = "apply-android-manifest-change-to"; private static final String APPLY_CPP_SOURCE_CHANGE_TO = "apply-cpp-change-to"; private static final String APPLY_H_SOURCE_CHANGE_TO = "apply-h-change-to"; + private static final String EXECUTE_COMMAND_BEFORE = "execute-command-before"; private static final String CLEAR_BUILD_CACHE_BEFORE = "clear-build-cache-before"; private static final String CLEAR_GRADLE_USER_HOME_BEFORE = "clear-gradle-user-home-before"; private static final String CLEAR_INSTANT_EXECUTION_STATE_BEFORE = "clear-instant-execution-state-before"; @@ -97,6 +101,7 @@ class ScenarioLoader { .put(SHOW_BUILD_CACHE_SIZE, new ShowBuildCacheSizeMutator.Configurator()) .put(GIT_CHECKOUT, new GitCheckoutMutator.Configurator()) .put(GIT_REVERT, new GitRevertMutator.Configurator()) + .put(EXECUTE_COMMAND_BEFORE, new ExecuteCommandBuildMutator.Configurator()) .build(); private static final List ALL_SCENARIO_KEYS = ImmutableList.builder() @@ -105,6 +110,7 @@ class ScenarioLoader { TITLE, VERSIONS, TASKS, + EXECUTE_COMMAND_BEFORE, CLEANUP_TASKS, GRADLE_ARGS, RUN_USING, @@ -123,9 +129,9 @@ class ScenarioLoader { )) .build(); - private static final List BAZEL_KEYS = Arrays.asList(TARGETS, TOOL_HOME); - private static final List BUCK_KEYS = Arrays.asList(TARGETS, TYPE, TOOL_HOME); - private static final List MAVEN_KEYS = Arrays.asList(TARGETS, TOOL_HOME); + private static final List BAZEL_KEYS = Arrays.asList(TARGETS, TOOL_HOME, EXECUTE_COMMAND_BEFORE); + private static final List BUCK_KEYS = Arrays.asList(TARGETS, TYPE, TOOL_HOME, EXECUTE_COMMAND_BEFORE); + private static final List MAVEN_KEYS = Arrays.asList(TARGETS, TOOL_HOME, EXECUTE_COMMAND_BEFORE); private final GradleBuildConfigurationReader gradleBuildConfigurationReader; @@ -212,7 +218,6 @@ static List loadScenarios(File scenarioFile, InvocationSetti String title = scenario.hasPath(TITLE) ? scenario.getString(TITLE) : null; List mutators = BUILD_MUTATOR_CONFIGURATORS.entrySet().stream() - .filter(entry -> scenario.hasPath(entry.getKey())) .map(entry -> entry.getValue().configure(scenario, scenarioName, settings, entry.getKey())) .filter(mutator -> mutator != BuildMutator.NOOP) .collect(Collectors.toList()); @@ -220,64 +225,11 @@ static List loadScenarios(File scenarioFile, InvocationSetti int buildCount = getBuildCount(settings, scenario); File scenarioBaseDir = new File(settings.getOutputDir(), GradleScenarioDefinition.safeFileName(scenarioName)); - if (scenario.hasPath(BAZEL) && settings.isBazel()) { - Config executionInstructions = getConfig(scenarioFile, settings, scenarioName, scenario, BAZEL, BAZEL_KEYS); - - List targets = ConfigUtil.strings(executionInstructions, TARGETS); - File bazelHome = getToolHome(executionInstructions); - File outputDir = new File(scenarioBaseDir, "bazel"); - int warmUpCount = getWarmUpCount(settings, scenario); - definitions.add(new BazelScenarioDefinition(scenarioName, title, targets, mutators, warmUpCount, buildCount, outputDir, bazelHome)); - } else if (scenario.hasPath(BUCK) && settings.isBuck()) { - Config executionInstructions = getConfig(scenarioFile, settings, scenarioName, scenario, BUCK, BUCK_KEYS); - List targets = ConfigUtil.strings(executionInstructions, TARGETS); - String type = ConfigUtil.string(executionInstructions, TYPE, null); - File buckHome = getToolHome(executionInstructions); - File outputDir = new File(scenarioBaseDir, "buck"); - int warmUpCount = getWarmUpCount(settings, scenario); - definitions.add(new BuckScenarioDefinition(scenarioName, title, targets, type, mutators, warmUpCount, buildCount, outputDir, buckHome)); - } else if (scenario.hasPath(MAVEN) && settings.isMaven()) { - Config executionInstructions = getConfig(scenarioFile, settings, scenarioName, scenario, MAVEN, MAVEN_KEYS); - List targets = ConfigUtil.strings(executionInstructions, TARGETS); - File mavenHome = getToolHome(executionInstructions); - File outputDir = new File(scenarioBaseDir, "maven"); - int warmUpCount = getWarmUpCount(settings, scenario); - definitions.add(new MavenScenarioDefinition(scenarioName, title, targets, mutators, warmUpCount, buildCount, outputDir, mavenHome)); - } else if (!settings.isBazel() && !settings.isBuck() && !settings.isMaven()) { - List versions = ConfigUtil.strings(scenario, VERSIONS, settings.getVersions()).stream().map(inspector::readConfiguration).collect( - Collectors.toList()); - if (versions.isEmpty()) { - versions.add(inspector.readConfiguration()); - } - - List gradleArgs = ConfigUtil.strings(scenario, GRADLE_ARGS); - BuildAction buildAction = getBuildAction(scenario, scenarioFile, settings); - GradleBuildInvoker invoker = invoker(scenario, (GradleBuildInvoker) settings.getInvoker(), buildAction); - int warmUpCount = getWarmUpCount(settings, invoker, scenario); - List measuredBuildOperations = getMeasuredBuildOperations(settings, scenario); - BuildAction cleanupAction = getCleanupAction(scenario); - Map systemProperties = ConfigUtil.map(scenario, SYSTEM_PROPERTIES, settings.getSystemProperties()); - List jvmArgs = ConfigUtil.strings(scenario, JVM_ARGS); - for (GradleBuildConfiguration version : versions) { - File outputDir = versions.size() == 1 ? scenarioBaseDir : new File(scenarioBaseDir, version.getGradleVersion().getVersion()); - definitions.add(new GradleScenarioDefinition( - scenarioName, - title, - invoker, - version, - buildAction, - cleanupAction, - gradleArgs, - systemProperties, - mutators, - warmUpCount, - buildCount, - outputDir, - jvmArgs, - measuredBuildOperations - )); - } - } + definitions.addAll( + getScenarioDefinitions( + scenarioFile, settings, scenarioName, scenario, title, mutators, buildCount, + scenarioBaseDir, inspector) + ); } definitions.forEach(ScenarioDefinition::validate); @@ -285,7 +237,7 @@ static List loadScenarios(File scenarioFile, InvocationSetti return definitions; } - private static Config getConfig(File scenarioFile, InvocationSettings settings, String scenarioName, Config scenario, String toolName, List toolKeys) { + private static Config getExecutionInstructions(File scenarioFile, InvocationSettings settings, String scenarioName, Config scenario, String toolName, List toolKeys) { if (settings.isProfile()) { throw new IllegalArgumentException("Can only profile scenario '" + scenarioName + "' when building using Gradle."); } @@ -298,6 +250,128 @@ private static Config getConfig(File scenarioFile, InvocationSettings settings, return executionInstructions; } + private static List getScenarioDefinitions(File scenarioFile, + InvocationSettings settings, String scenarioName, Config scenario, String title, + List mutators, int buildCount, File scenarioBaseDir, + GradleBuildConfigurationReader inspector) { + List scenarioDefinitions; + if (scenario.hasPath(BAZEL) && settings.isBazel()) { + scenarioDefinitions = getBazelScenarioDefinitions( + scenarioFile, settings, scenarioName, scenario, title, mutators, buildCount, + scenarioBaseDir); + } else if (scenario.hasPath(BUCK) && settings.isBuck()) { + scenarioDefinitions = getBuckScenarioDefinitions( + scenarioFile, settings, scenarioName, scenario, title, mutators, buildCount, + scenarioBaseDir); + } else if (scenario.hasPath(MAVEN) && settings.isMaven()) { + scenarioDefinitions = getMavenScenarioDefinitions( + scenarioFile, settings, scenarioName, scenario, title, mutators, buildCount, + scenarioBaseDir); + } else if (!settings.isBazel() && !settings.isBuck() && !settings.isMaven()) { + scenarioDefinitions = getGradleScenarioDefinitions( + scenarioFile, settings, scenarioName, scenario, title, mutators, buildCount, + scenarioBaseDir, inspector + ); + } else { + scenarioDefinitions = Collections.emptyList(); + } + return Collections.unmodifiableList(scenarioDefinitions); + } + + private static List getGradleScenarioDefinitions( + File scenarioFile, + InvocationSettings settings, String scenarioName, Config scenario, String title, + List mutators, int buildCount, File scenarioBaseDir, + GradleBuildConfigurationReader inspector + ) { + List scenarioDefinitions = new ArrayList<>(); + List versions = ConfigUtil + .strings(scenario, VERSIONS, settings.getVersions()).stream() + .map(inspector::readConfiguration).collect( + Collectors.toList()); + if (versions.isEmpty()) { + versions.add(inspector.readConfiguration()); + } + + List gradleArgs = ConfigUtil.strings(scenario, GRADLE_ARGS); + BuildAction buildAction = getBuildAction(scenario, scenarioFile, settings); + GradleBuildInvoker invoker = invoker(scenario, + (GradleBuildInvoker) settings.getInvoker(), buildAction); + int warmUpCount = getWarmUpCount(settings, invoker, scenario); + List measuredBuildOperations = getMeasuredBuildOperations(settings, + scenario); + BuildAction cleanupAction = getCleanupAction(scenario); + Map systemProperties = ConfigUtil + .map(scenario, SYSTEM_PROPERTIES, settings.getSystemProperties()); + List jvmArgs = ConfigUtil.strings(scenario, JVM_ARGS); + for (GradleBuildConfiguration version : versions) { + File outputDir = versions.size() == 1 ? scenarioBaseDir + : new File(scenarioBaseDir, version.getGradleVersion().getVersion()); + scenarioDefinitions.add(new GradleScenarioDefinition( + scenarioName, + title, + invoker, + version, + buildAction, + cleanupAction, + gradleArgs, + systemProperties, + mutators, + warmUpCount, + buildCount, + outputDir, + jvmArgs, + measuredBuildOperations + )); + } + return scenarioDefinitions; + } + + private static List getMavenScenarioDefinitions(File scenarioFile, + InvocationSettings settings, String scenarioName, Config scenario, String title, + List mutators, int buildCount, File scenarioBaseDir) { + + Config executionInstructions = getExecutionInstructions(scenarioFile, settings, scenarioName, + scenario, MAVEN, MAVEN_KEYS); + List targets = ConfigUtil.strings(executionInstructions, TARGETS); + File mavenHome = getToolHome(executionInstructions); + File outputDir = new File(scenarioBaseDir, "maven"); + int warmUpCount = getWarmUpCount(settings, scenario); + return Arrays.asList(new MavenScenarioDefinition(scenarioName, + title, targets, mutators, warmUpCount, + buildCount, outputDir, mavenHome)); + } + + private static List getBuckScenarioDefinitions(File scenarioFile, + InvocationSettings settings, String scenarioName, Config scenario, String title, + List mutators, int buildCount, File scenarioBaseDir) { + Config executionInstructions = getExecutionInstructions(scenarioFile, settings, scenarioName, + scenario, BUCK, BUCK_KEYS); + List targets = ConfigUtil.strings(executionInstructions, TARGETS); + String type = ConfigUtil.string(executionInstructions, TYPE, null); + File buckHome = getToolHome(executionInstructions); + File outputDir = new File(scenarioBaseDir, "buck"); + int warmUpCount = getWarmUpCount(settings, scenario); + return Arrays + .asList(new BuckScenarioDefinition(scenarioName, title, targets, type, mutators, + warmUpCount, buildCount, outputDir, buckHome)); + } + + private static List getBazelScenarioDefinitions(File scenarioFile, + InvocationSettings settings, String scenarioName, Config scenario, String title, + List mutators, int buildCount, File scenarioBaseDir) { + Config executionInstructions = getExecutionInstructions(scenarioFile, settings, scenarioName, scenario, + BAZEL, BAZEL_KEYS); + + List targets = ConfigUtil.strings(executionInstructions, TARGETS); + File bazelHome = getToolHome(executionInstructions); + File outputDir = new File(scenarioBaseDir, "bazel"); + int warmUpCount = getWarmUpCount(settings, scenario); + return Arrays.asList(new BazelScenarioDefinition( + scenarioName, title, targets, mutators, warmUpCount, buildCount, outputDir, + bazelHome)); + } + private static ImmutableList getMeasuredBuildOperations(InvocationSettings settings, Config scenario) { return ImmutableSet.builder() .addAll(settings.getMeasuredBuildOperations()) diff --git a/src/main/java/org/gradle/profiler/ScenarioUtil.java b/src/main/java/org/gradle/profiler/ScenarioUtil.java new file mode 100644 index 000000000..f4f9cf2d3 --- /dev/null +++ b/src/main/java/org/gradle/profiler/ScenarioUtil.java @@ -0,0 +1,44 @@ +package org.gradle.profiler; + +import static org.gradle.profiler.ScenarioLoader.BAZEL; +import static org.gradle.profiler.ScenarioLoader.BUCK; +import static org.gradle.profiler.ScenarioLoader.MAVEN; + +import com.typesafe.config.Config; +import javax.annotation.Nullable; + +public class ScenarioUtil { + + /** + * Returns the specific config for type of build that is running, + * if the scenario doesn't define the build, then this default value is returned + * + * @param rootScenario the root scenario config + * @param settings the invocation settings that indicates the type of build selected + * @param defaultScenario if the scenario doesn't define the build, then this default value is returned, this can be null. + * @return if the build config exists or if the scenario doesn't define the build, then this default value is returned + */ + public static Config getBuildConfig(Config rootScenario, InvocationSettings settings, @Nullable + Config defaultScenario) { + Config scenario; + if (settings.isBazel()) { + scenario = getConfigOrDefault(rootScenario, BAZEL, defaultScenario); + } else if (settings.isBuck()) { + scenario = getConfigOrDefault(rootScenario, BUCK, defaultScenario); + } else if (settings.isMaven()) { + scenario = getConfigOrDefault(rootScenario, MAVEN, defaultScenario); + } else { + scenario = rootScenario; + } + return scenario; + } + + private static Config getConfigOrDefault(Config rootScenario, String key, @Nullable Config defaultScenario) { + Config scenario; + if (rootScenario.hasPath(key)) { + scenario = rootScenario.getConfig(key); + } else { + scenario = defaultScenario; + } return scenario; + } +} diff --git a/src/main/java/org/gradle/profiler/mutations/AbstractBuildMutatorWithoutOptionsConfigurator.java b/src/main/java/org/gradle/profiler/mutations/AbstractBuildMutatorWithoutOptionsConfigurator.java index b6d194288..720b4e440 100644 --- a/src/main/java/org/gradle/profiler/mutations/AbstractBuildMutatorWithoutOptionsConfigurator.java +++ b/src/main/java/org/gradle/profiler/mutations/AbstractBuildMutatorWithoutOptionsConfigurator.java @@ -10,7 +10,9 @@ public abstract class AbstractBuildMutatorWithoutOptionsConfigurator implements @Override public BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key) { - boolean enabled = scenario.getBoolean(key); - return enabled ? createBuildMutator(settings) : BuildMutator.NOOP; + return new HasPathBuildMutatorConfigurator(() -> { + boolean enabled = scenario.getBoolean(key); + return enabled ? createBuildMutator(settings) : BuildMutator.NOOP; + }).configure(scenario,scenarioName,settings,key); } } diff --git a/src/main/java/org/gradle/profiler/mutations/AbstractCleanupMutator.java b/src/main/java/org/gradle/profiler/mutations/AbstractCleanupMutator.java index 06d2db1ea..55e881cef 100644 --- a/src/main/java/org/gradle/profiler/mutations/AbstractCleanupMutator.java +++ b/src/main/java/org/gradle/profiler/mutations/AbstractCleanupMutator.java @@ -66,11 +66,14 @@ protected static void delete(File f) { protected static abstract class Configurator implements BuildMutatorConfigurator { @Override public BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key) { - CleanupSchedule schedule = ConfigUtil.enumValue(scenario, key, CleanupSchedule.class, null); - if (schedule == null) { - throw new IllegalArgumentException("Schedule for cleanup is not specified"); - } - return newInstance(scenario, scenarioName, settings, key, schedule); + return new HasPathBuildMutatorConfigurator(() -> { + CleanupSchedule schedule = ConfigUtil + .enumValue(scenario, key, CleanupSchedule.class, null); + if (schedule == null) { + throw new IllegalArgumentException("Schedule for cleanup is not specified"); + } + return newInstance(scenario, scenarioName, settings, key, schedule); + }).configure(scenario, scenarioName, settings, key); } protected abstract BuildMutator newInstance(Config scenario, String scenarioName, InvocationSettings settings, String key, CleanupSchedule schedule); diff --git a/src/main/java/org/gradle/profiler/mutations/BuildMutatorConfigurator.java b/src/main/java/org/gradle/profiler/mutations/BuildMutatorConfigurator.java index ce940fa5a..f5f893b69 100644 --- a/src/main/java/org/gradle/profiler/mutations/BuildMutatorConfigurator.java +++ b/src/main/java/org/gradle/profiler/mutations/BuildMutatorConfigurator.java @@ -5,5 +5,6 @@ import org.gradle.profiler.InvocationSettings; public interface BuildMutatorConfigurator { + BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key); } diff --git a/src/main/java/org/gradle/profiler/mutations/CommandInvoker.java b/src/main/java/org/gradle/profiler/mutations/CommandInvoker.java new file mode 100644 index 000000000..a4d75c79e --- /dev/null +++ b/src/main/java/org/gradle/profiler/mutations/CommandInvoker.java @@ -0,0 +1,16 @@ +package org.gradle.profiler.mutations; + +import java.util.List; + +/** + * Interface which provides a means to execute a command + */ +public interface CommandInvoker { + + /** + * @param command the command to execute + * @return the exit code of the result of executing the command + */ + int execute(List command); + +} diff --git a/src/main/java/org/gradle/profiler/mutations/ExecuteCommandBuildMutator.java b/src/main/java/org/gradle/profiler/mutations/ExecuteCommandBuildMutator.java new file mode 100644 index 000000000..351337963 --- /dev/null +++ b/src/main/java/org/gradle/profiler/mutations/ExecuteCommandBuildMutator.java @@ -0,0 +1,121 @@ +package org.gradle.profiler.mutations; + +import static org.gradle.profiler.ScenarioUtil.getBuildConfig; + +import com.google.common.annotations.VisibleForTesting; +import com.typesafe.config.Config; +import java.util.ArrayList; +import java.util.List; +import org.gradle.profiler.BuildContext; +import org.gradle.profiler.BuildMutator; +import org.gradle.profiler.CompositeBuildMutator; +import org.gradle.profiler.ConfigUtil; +import org.gradle.profiler.InvocationSettings; +import org.gradle.profiler.ScenarioContext; + +public class ExecuteCommandBuildMutator implements BuildMutator { + + private ExecuteCommandSchedule schedule; + private List commands; + private CommandInvoker commandInvoker; + + public ExecuteCommandBuildMutator(ExecuteCommandSchedule schedule, + List commands, CommandInvoker commandInvoker) { + this.schedule = schedule; + this.commands = commands; + this.commandInvoker = commandInvoker; + } + + @Override + public void beforeBuild(BuildContext context) { + if (schedule == ExecuteCommandSchedule.BUILD) { + execute(); + } + } + + @Override + public void beforeScenario(ScenarioContext context) { + if (schedule == ExecuteCommandSchedule.SCENARIO) { + execute(); + } + } + + protected void execute() { + String commandStr = String.join(" ", commands); + System.out.println(String.format("> Executing command `%s`", commandStr)); + int result = commandInvoker.execute(commands); + if (result != 0) { + System.err.println( + String.format("Unexpected exit code %s for command `%s`", result, commandStr) + ); + } + } + + public static class Configurator implements BuildMutatorConfigurator { + + private CommandInvoker commandInvoker; + + public Configurator() { + this(new ProcessBuilderCommandInvoker()); + } + + @VisibleForTesting + Configurator(CommandInvoker commandInvoker) { + this.commandInvoker = commandInvoker; + } + + private BuildMutator newInstance(Config scenario, String scenarioName, + InvocationSettings settings, String key, + CommandInvoker commandInvoker, ExecuteCommandSchedule schedule, List commands) { + return new ExecuteCommandBuildMutator(schedule, commands, commandInvoker); + } + + @Override + public BuildMutator configure(Config rootScenario, String scenarioName, + InvocationSettings settings, String key) { + if (enabled(rootScenario, scenarioName, settings, key)) { + Config scenario = getBuildConfig(rootScenario, settings, null); + final List mutators = new ArrayList<>(); + final List list = scenario.getConfigList(key); + for (Config config : list) { + final ExecuteCommandSchedule schedule = ConfigUtil + .enumValue(config, "schedule", ExecuteCommandSchedule.class, null); + if (schedule == null) { + throw new IllegalArgumentException( + "Schedule for executing commands is not specified"); + } + List commands = ConfigUtil.strings(config, "commands"); + if (commands.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "No commands specified for 'execute-command-before' in scenario %s", + scenarioName) + ); + } + mutators.add( + newInstance(scenario, scenarioName, settings, key, commandInvoker, schedule, + commands)); + } + return new CompositeBuildMutator(mutators); + } else { + return BuildMutator.NOOP; + } + } + + private boolean enabled(Config rootScenario, String scenarioName, InvocationSettings settings, String key) { + Config scenario = getBuildConfig(rootScenario, settings, null); + return scenario != null && scenario.hasPath(key) && !scenario.getConfigList(key) + .isEmpty(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + schedule + ")"; + } + + public enum ExecuteCommandSchedule { + SCENARIO, BUILD + } + +} diff --git a/src/main/java/org/gradle/profiler/mutations/FileChangeMutatorConfigurator.java b/src/main/java/org/gradle/profiler/mutations/FileChangeMutatorConfigurator.java index 41e5c4734..23ac076da 100644 --- a/src/main/java/org/gradle/profiler/mutations/FileChangeMutatorConfigurator.java +++ b/src/main/java/org/gradle/profiler/mutations/FileChangeMutatorConfigurator.java @@ -22,12 +22,15 @@ public FileChangeMutatorConfigurator(Class @Override public BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key) { - List mutatorsForKey = new ArrayList<>(); - for (File sourceFileToChange : sourceFiles(scenario, scenarioName, settings.getProjectDir(), key)) { - mutatorsForKey.add(getBuildMutatorForFile(sourceFileToChange)); - } + return new HasPathBuildMutatorConfigurator(() -> { + List mutatorsForKey = new ArrayList<>(); + for (File sourceFileToChange : sourceFiles(scenario, scenarioName, + settings.getProjectDir(), key)) { + mutatorsForKey.add(getBuildMutatorForFile(sourceFileToChange)); + } - return new CompositeBuildMutator(mutatorsForKey); + return new CompositeBuildMutator(mutatorsForKey); + }).configure(scenario, scenarioName, settings, key); } private BuildMutator getBuildMutatorForFile(File sourceFileToChange) { diff --git a/src/main/java/org/gradle/profiler/mutations/GitCheckoutMutator.java b/src/main/java/org/gradle/profiler/mutations/GitCheckoutMutator.java index eb76edc91..e7425a83c 100644 --- a/src/main/java/org/gradle/profiler/mutations/GitCheckoutMutator.java +++ b/src/main/java/org/gradle/profiler/mutations/GitCheckoutMutator.java @@ -63,13 +63,16 @@ private void checkout(String target) { public static class Configurator implements BuildMutatorConfigurator { @Override public BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key) { - Config config = scenario.getConfig(key); - String cleanup = ConfigUtil.string(config, "cleanup", null); - String build = ConfigUtil.string(config, "build", null); - if (build == null) { - throw new IllegalArgumentException("No git-checkout target specified for build"); - } - return new GitCheckoutMutator(settings.getProjectDir(), cleanup, build); + return new HasPathBuildMutatorConfigurator(() -> { + Config config = scenario.getConfig(key); + String cleanup = ConfigUtil.string(config, "cleanup", null); + String build = ConfigUtil.string(config, "build", null); + if (build == null) { + throw new IllegalArgumentException( + "No git-checkout target specified for build"); + } + return new GitCheckoutMutator(settings.getProjectDir(), cleanup, build); + }).configure(scenario, scenarioName, settings, key); } } diff --git a/src/main/java/org/gradle/profiler/mutations/GitRevertMutator.java b/src/main/java/org/gradle/profiler/mutations/GitRevertMutator.java index bc66acee6..23228aa2b 100644 --- a/src/main/java/org/gradle/profiler/mutations/GitRevertMutator.java +++ b/src/main/java/org/gradle/profiler/mutations/GitRevertMutator.java @@ -60,11 +60,13 @@ private void abortRevert() { public static class Configurator implements BuildMutatorConfigurator { @Override public BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key) { - List commits = ConfigUtil.strings(scenario, key); - if (commits.isEmpty()) { - throw new IllegalArgumentException("No commits specified for git-revert"); - } - return new GitRevertMutator(settings.getProjectDir(), commits); + return new HasPathBuildMutatorConfigurator(() -> { + List commits = ConfigUtil.strings(scenario, key); + if (commits.isEmpty()) { + throw new IllegalArgumentException("No commits specified for git-revert"); + } + return new GitRevertMutator(settings.getProjectDir(), commits); + }).configure(scenario, scenarioName, settings, key); } } diff --git a/src/main/java/org/gradle/profiler/mutations/HasPathBuildMutatorConfigurator.java b/src/main/java/org/gradle/profiler/mutations/HasPathBuildMutatorConfigurator.java new file mode 100644 index 000000000..054085a2d --- /dev/null +++ b/src/main/java/org/gradle/profiler/mutations/HasPathBuildMutatorConfigurator.java @@ -0,0 +1,27 @@ +package org.gradle.profiler.mutations; + +import com.typesafe.config.Config; +import java.util.function.Supplier; +import org.gradle.profiler.BuildMutator; +import org.gradle.profiler.InvocationSettings; + +class HasPathBuildMutatorConfigurator implements BuildMutatorConfigurator { + + private Supplier configurator; + + HasPathBuildMutatorConfigurator(Supplier configurator) { + this.configurator = configurator; + } + + @Override + public BuildMutator configure(Config scenario, String scenarioName, + InvocationSettings settings, String key) { + BuildMutator buildMutator; + if (scenario.hasPath(key)) { + buildMutator = configurator.get(); + } else { + buildMutator = BuildMutator.NOOP; + } + return buildMutator; + } +} diff --git a/src/main/java/org/gradle/profiler/mutations/ProcessBuilderCommandInvoker.java b/src/main/java/org/gradle/profiler/mutations/ProcessBuilderCommandInvoker.java new file mode 100644 index 000000000..bac9b93d8 --- /dev/null +++ b/src/main/java/org/gradle/profiler/mutations/ProcessBuilderCommandInvoker.java @@ -0,0 +1,24 @@ +package org.gradle.profiler.mutations; + +import java.io.IOException; +import java.util.List; + +public class ProcessBuilderCommandInvoker implements CommandInvoker { + + @Override + public int execute(List command) { + try { + if (command == null || command.isEmpty()) { + throw new IllegalArgumentException( + String.format("command cannot be null or empty, was %s", command)); + } + ProcessBuilder processBuilder = new ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT); + Process process = processBuilder.start(); + return process.waitFor(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/groovy/org/gradle/profiler/mutations/ExecuteCommandBuildMutatorTest.groovy b/src/test/groovy/org/gradle/profiler/mutations/ExecuteCommandBuildMutatorTest.groovy new file mode 100644 index 000000000..dfeb62f3e --- /dev/null +++ b/src/test/groovy/org/gradle/profiler/mutations/ExecuteCommandBuildMutatorTest.groovy @@ -0,0 +1,139 @@ +package org.gradle.profiler.mutations + + +import com.typesafe.config.ConfigFactory +import org.gradle.profiler.BuildInvoker +import org.gradle.profiler.InvocationSettings +import org.gradle.profiler.report.CsvGenerator + +class ExecuteCommandBuildMutatorTest extends AbstractMutatorTest { + + + def static createInvocationSettings(BuildInvoker buildInvoker, String target) { + return new InvocationSettings(new File("."), + null, + true, + new File("outputdir"), + buildInvoker, + false, + new File("scenariofile"), + Collections.emptyList(), + Arrays.asList(target), + Collections.emptyMap(), + new File("gradlehome"), + new File("studioinstall"), + 5, + 10, + false, + false, + Collections.emptyList(), + CsvGenerator.Format.LONG, + "benchmarkTitle", + new File("buildLog") + ) + } + + def invoker = new CommandInvoker() { + private List> commands + + @Override + int execute(final List commands) throws InterruptedException, IOException { + this.commands.add(commands) + return 0 + } + + void reset() { + commands = new ArrayList<>() + } + } + + def setup() { + invoker.reset() + } + + def "executes the commands before scenario starts when Schedule is SCENARIO"() { + def mutator = new ExecuteCommandBuildMutator(ExecuteCommandBuildMutator.ExecuteCommandSchedule.SCENARIO, + Arrays.asList("echo", "Hello World"), + invoker) + + when: + mutator.beforeScenario(scenarioContext) + then: + invoker.commands.size() == 1 + invoker.commands.get(0) == Arrays.asList("echo", "Hello World") + } + + def "doesn't execute the commands before scenario starts when Schedule is BUILD"() { + def mutator = new ExecuteCommandBuildMutator(ExecuteCommandBuildMutator.ExecuteCommandSchedule.BUILD, + Arrays.asList("echo", "Hello World"), + invoker) + + when: + mutator.beforeScenario(scenarioContext) + then: + invoker.commands.isEmpty() + } + + def "Configure ExecuteCommandBuildMutator from scenario"() { + def mutator = new ExecuteCommandBuildMutator(ExecuteCommandBuildMutator.ExecuteCommandSchedule.BUILD, + Arrays.asList("echo", "Hello World"), + invoker) + + when: + mutator.beforeScenario(scenarioContext) + then: + invoker.commands.isEmpty() + } + + def "execute the commands before scenario starts when Schedule is BUILD"() { + def mutator = new ExecuteCommandBuildMutator(ExecuteCommandBuildMutator.ExecuteCommandSchedule.BUILD, + Arrays.asList("echo", "Hello World"), + invoker) + + when: + mutator.beforeBuild(buildContext) + then: + invoker.commands.size() == 1 + invoker.commands.get(0) == Arrays.asList("echo", "Hello World") + } + + def "Given Gradle scenario verify that the configuration creates a mutator that executes"() { + given: + def config = ConfigFactory.parseString( + """ + testScenario { + tasks = ["tasks"] + clear-build-cache-before = SCENARIO + gradle-args = [ + "-Pbuild.cache.local.enabled=true" + ] + execute-command-before = [ + { + schedule = SCENARIO + commands = ["gsutil", "version"] + }, + { + schedule = BUILD + commands = ["/bin/bash", "-c", "echo helloworld"] + } + ] + } + """ + ) + + def mutator = new ExecuteCommandBuildMutator.Configurator(invoker).configure( + config.getConfig("testScenario"), + "testScenario", + createInvocationSettings(null, "testScenario"), + "execute-command-before" + ) + when: + mutator.beforeScenario(scenarioContext) + mutator.beforeBuild(buildContext) + then: + invoker.commands.size() == 2 + invoker.commands.get(0) == Arrays.asList("gsutil", "version") + invoker.commands.get(1) == Arrays.asList("/bin/bash", "-c", "echo helloworld") + } + +}