diff --git a/CHANGELOG.md b/CHANGELOG.md index 345b2e9..c818e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- New plugin [`com.diffplug.if-git-diff`](IF_GIT_DIFF.md) ([#31](https://github.com/diffplug/spotless-changelog/pull/31)). ## [2.3.2] - 2021-11-29 ### Fixed diff --git a/IF_GIT_DIFF.md b/IF_GIT_DIFF.md new file mode 100644 index 0000000..c3dcfe1 --- /dev/null +++ b/IF_GIT_DIFF.md @@ -0,0 +1,69 @@ +# If Git Diff + +[![Gradle plugin](https://img.shields.io/badge/plugins.gradle.org-com.diffplug.if--git--diff-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.if-git-diff) +[![Maven central](https://img.shields.io/badge/mavencentral-available-blue.svg)](https://search.maven.org/search?q=g:com.diffplug.spotless-changelog) +[![Apache 2.0](https://img.shields.io/badge/license-apache--2.0-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) + +[![Changelog](https://img.shields.io/badge/changelog-2.3.2-brightgreen.svg)](CHANGELOG.md) +[![Javadoc](https://img.shields.io/badge/javadoc-yes-brightgreen.svg)](https://javadoc.jitpack.io/com/github/diffplug/spotless-changelog/spotless-changelog-agg/release~2.3.2/javadoc/) +[![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless-changelog) +[![CircleCI](https://circleci.com/gh/diffplug/spotless-changelog.svg?style=shield)](https://circleci.com/gh/diffplug/spotless-changelog) + + +This plugin can be applied in `settings.gradle` or `build.gradle`, and it lets you execute a block of code contingent on whether there are changes in the given folder relative to a given baseline git ref. + +```gradle +plugins { + id 'com.diffplug.if-git-diff' +} +ifGitDiff { + baseline 'origin/main' // default value + inFolder 'a', { include 'a' } + inFolder 'b', { include 'b' } +} +``` + +## Limitations + +This plugin does not work well with the configuration cache. Using the example above: + +- run `gradlew test` on a clean checkout of `origin/main`, and you would see that `:test` ran but `:a:test` and `:b:test` did not; so far so good. +- now add a file `a/blah` +- now if you run `gradlew test` + - without configuration-cache -> `:test` and `:a:test` -> good! + - with configuration-cache -> only `:test` -> bad, cached configuration doesn't know that `a/blah` was added + +A different approach which could work with configuration-cache is to mark tasks as up-to-date based on git status, see the [`GitDiffUpToDatePlugin`](https://github.com/thahnen/GitDiffUpToDatePlugin) for that. + +## Roadmap + +This plugin was built to solve [a fairly specific problem in the Spotless build](https://github.com/diffplug/spotless-changelog/issues/30). It is packaged with `spotless-changelog` because it's vaguely related, and it might make sense someday for `spotless-changelog` to assert "if files changed in X dir, then changelog Y must be updated". + +## Reference + + + +[Plugin DSL javadoc](https://javadoc.io/static/com.diffplug.spotless-changelog/spotless-changelog-plugin-gradle/2.3.2/com/diffplug/spotless/changelog/gradle/IfGitDiffExtension.html). For requirements see [spotless-changelog](https://github.com/diffplug/spotless-changelog#requirements). + + + +## Acknowledgments + +- Git stuff by [jgit](https://www.eclipse.org/jgit/). +- Built by [gradle](https://gradle.org/). +- Maintained by [DiffPlug](https://www.diffplug.com/). diff --git a/gradle.properties b/gradle.properties index 7388f5c..ff5fbd6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,16 @@ license=apache git_url=github.com/diffplug/spotless-changelog plugin_tags=changelog keepachangelog version git -plugin_list=spotlessChangelog +plugin_list=spotlessChangelog ifGitDiff plugin_spotlessChangelog_id=com.diffplug.spotless-changelog plugin_spotlessChangelog_impl=com.diffplug.spotless.changelog.gradle.ChangelogPlugin plugin_spotlessChangelog_name=Spotless Changelog plugin_spotlessChangelog_desc=The changelog is cast, let the versions fall where they may. +plugin_ifGitDiff_id=com.diffplug.if-git-diff +plugin_ifGitDiff_impl=com.diffplug.spotless.changelog.gradle.IfGitDiffPlugin +plugin_ifGitDiff_name=If Git Diff +plugin_ifGitDiff_desc=Decide what to configure based on changes relative to `origin/main` + maven_group=com.diffplug.spotless-changelog javadoc_links=\ https://docs.oracle.com/javase/8/docs/api/ \ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffExtension.java b/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffExtension.java new file mode 100644 index 0000000..61a8a77 --- /dev/null +++ b/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffExtension.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 DiffPlug + * + * 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 + * + * https://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.diffplug.spotless.changelog.gradle; + + +import com.diffplug.common.base.Preconditions; +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.initialization.Settings; + +public abstract class IfGitDiffExtension { + static final String NAME = "ifGitDiff"; + + public static class ForProject extends IfGitDiffExtension { + public ForProject(Project owner) { + super(owner); + } + + @Override + protected File file(Object fileArg) { + return owner.file(fileArg); + } + } + + public static class ForSettings extends IfGitDiffExtension { + public ForSettings(Settings owner) { + super(owner); + } + + @Override + protected File file(Object fileArg) { + if (fileArg instanceof File) { + return (File) fileArg; + } else if (fileArg instanceof String) { + return new File(owner.getRootDir(), (String) fileArg); + } else { + throw new IllegalArgumentException("We only support String or File, this was " + fileArg.getClass()); + } + } + } + + final T owner; + + IfGitDiffExtension(T owner) { + this.owner = owner; + } + + private String baseline = "origin/main"; + + public void setBaseline(String baseline) { + this.baseline = baseline; + } + + public String getBaseline() { + return baseline; + } + + protected abstract File file(Object fileArg); + + private TreeFilter filterTo(Repository repo, File child) { + String rootAbs = repo.getWorkTree().getAbsolutePath(); + String childAbs = child.getAbsolutePath(); + if (rootAbs.equals(childAbs)) { + return TreeFilter.ALL; + } else if (childAbs.startsWith(rootAbs)) { + String filter = childAbs.substring(rootAbs.length()).replace('\\', '/'); + Preconditions.checkState(filter.charAt(0) == '/'); + return PathFilter.create(filter.substring(1)); + } else { + throw new GradleException(childAbs + " is not contained within the git repo " + rootAbs); + } + } + + public void inFolder(Object folder, Action onChanged) { + try (Repository repo = new FileRepositoryBuilder() + .findGitDir(file("")) + .build()) { + ObjectId baselineSha = repo.resolve(baseline); + if (baselineSha == null) { + throw new GradleException("Unable to resolve " + baseline); + } + + CanonicalTreeParser baselineTree = new CanonicalTreeParser(); + try (ObjectReader reader = repo.newObjectReader()) { + RevWalk walk = new RevWalk(reader); + baselineTree.reset(reader, walk.parseCommit(baselineSha).getTree()); + } + Git git = new Git(repo); + List changes = git.diff() + .setOldTree(baselineTree) + .setShowNameAndStatusOnly(true) + .setPathFilter(filterTo(repo, file(folder))) + .call(); + if (!changes.isEmpty()) { + onChanged.execute(owner); + } + } catch (IOException e) { + throw new GradleException("Unable to find git repository", e); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } +} diff --git a/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPlugin.java b/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPlugin.java new file mode 100644 index 0000000..b4d084d --- /dev/null +++ b/spotless-changelog-plugin-gradle/src/main/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPlugin.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019-2022 DiffPlug + * + * 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 + * + * https://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.diffplug.spotless.changelog.gradle; + + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.initialization.Settings; +import org.gradle.api.plugins.ExtensionAware; + +/** @see IfGitDiffExtension */ +public class IfGitDiffPlugin implements Plugin { + @Override + public void apply(ExtensionAware projectOrSettings) { + if (projectOrSettings instanceof Project) { + Project project = (Project) projectOrSettings; + projectOrSettings.getExtensions().create(IfGitDiffExtension.NAME, IfGitDiffExtension.ForProject.class, project); + } else if (projectOrSettings instanceof Settings) { + Settings settings = (Settings) projectOrSettings; + projectOrSettings.getExtensions().create(IfGitDiffExtension.NAME, IfGitDiffExtension.ForSettings.class, settings); + } else { + throw new IllegalArgumentException("We support build.gradle and settings.gradle, this was " + projectOrSettings.getClass()); + } + } +} diff --git a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ChangelogPluginTest.java b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ChangelogPluginTest.java index 875f952..b9f3b90 100644 --- a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ChangelogPluginTest.java +++ b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ChangelogPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 DiffPlug + * Copyright (C) 2019-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,8 @@ public class ChangelogPluginTest extends GradleHarness { private static final String DATE_NOW = "2019-01-30"; private void writeSpotlessChangelog(String... lines) throws IOException { - write("settings.gradle", "rootProject.name='undertest'"); - write("build.gradle", + setFile("settings.gradle").toContent("rootProject.name='undertest'"); + setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless-changelog'", "}", @@ -62,15 +62,15 @@ public void missingChangelog() throws IOException { @Test public void changelogCheck() throws IOException { writeSpotlessChangelog(); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "", "## [1.0.0]"); assertFailOutput("changelogCheck") - .contains("CHANGELOG.md:5: '] - ' is missing from the expected '## [x.y.z] - yyyy-mm-dd"); + .contains("CHANGELOG.md:4: '] - ' is missing from the expected '## [x.y.z] - yyyy-mm-dd"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "", @@ -81,14 +81,14 @@ public void changelogCheck() throws IOException { @Test public void changelogPrint() throws IOException { writeSpotlessChangelog(); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "", "## [1.0.0] - 2020-10-10"); assertOutput("changelogPrint") .startsWith("> Task :changelogPrint\nundertest 1.0.0 (no unreleased changes)"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "Some minor change", @@ -96,7 +96,7 @@ public void changelogPrint() throws IOException { "## [1.0.0] - 2020-10-10"); assertOutput("changelogPrint") .startsWith("> Task :changelogPrint\nundertest 1.0.0 -> 1.0.1"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "### Added", @@ -104,7 +104,7 @@ public void changelogPrint() throws IOException { "## [1.0.0] - 2020-10-10"); assertOutput("changelogPrint") .startsWith("> Task :changelogPrint\nundertest 1.0.0 -> 1.1.0"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "**BREAKING**", @@ -117,7 +117,7 @@ public void changelogPrint() throws IOException { @Test public void changelogBump() throws IOException { writeSpotlessChangelog(); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "", @@ -126,7 +126,7 @@ public void changelogBump() throws IOException { gradleRunner().withArguments("changelogBump").build(); assertFile("CHANGELOG.md").hasContent(noUnreleasedChanges); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "### Added", @@ -147,7 +147,7 @@ public void changelogBump() throws IOException { @Test public void changelogBumpCustomNextVersionFunction() throws IOException { writeSpotlessChangelog("versionSchema(com.diffplug.spotless.changelog.NextVersionFunction.SemverBrandPrefix)"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "", @@ -156,7 +156,7 @@ public void changelogBumpCustomNextVersionFunction() throws IOException { gradleRunner().withArguments("changelogBump").build(); assertFile("CHANGELOG.md").hasContent(noUnreleasedChanges); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "### Added", @@ -177,7 +177,7 @@ public void changelogBumpCustomNextVersionFunction() throws IOException { @Test public void snapshot() throws IOException { writeSpotlessChangelog("appendDashSnapshotUnless_dashPrelease=true"); - write("CHANGELOG.md", + setFile("CHANGELOG.md").toLines( "", "## [Unreleased]", "### Added", diff --git a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/GradleHarness.java b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/GradleHarness.java index d36653b..7de4b5a 100644 --- a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/GradleHarness.java +++ b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/GradleHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 DiffPlug + * Copyright (C) 2019-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ protected boolean isConfigCache() { protected GradleRunner gradleRunner() throws IOException { String version; if (isConfigCache()) { - write("gradle.properties", "org.gradle.unsafe.configuration-cache=true"); + setFile("gradle.properties").toContent("org.gradle.unsafe.configuration-cache=true"); version = V_GRADLE_CONFIG_CACHE; } else { version = V_GRADLE_OLDEST_SUPPORTED; diff --git a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPluginTest.java b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPluginTest.java new file mode 100644 index 0000000..d9c4702 --- /dev/null +++ b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/IfGitDiffPluginTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 DiffPlug + * + * 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 + * + * https://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.diffplug.spotless.changelog.gradle; + + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.eclipse.jgit.api.Git; +import org.gradle.testkit.runner.BuildTask; +import org.junit.Assume; +import org.junit.Test; + +public class IfGitDiffPluginTest extends GradleHarness { + @Test + public void ifGitDiff() throws Exception { + Assume.assumeFalse(isConfigCache()); + Git git = Git.init().setDirectory(rootFolder()).setInitialBranch("main").call(); + setFile("build.gradle").toContent("tasks.register('test')"); + setFile("a/build.gradle").toContent("tasks.register('test')"); + setFile("b/build.gradle").toContent("tasks.register('test')"); + setFile("settings.gradle").toLines( + "plugins {", + " id 'com.diffplug.if-git-diff'", + "}", + "ifGitDiff {", + " baseline 'main'", + " inFolder 'a', { include 'a' }", + " inFolder 'b', { include 'b' }", + "}"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Initial").call(); + + assertRan(":test"); + + setFile("blah").toContent(""); + assertRan(":test"); + + setFile("a/blah").toContent(""); + assertRan(":test", ":a:test"); + + setFile("b/blah").toContent(""); + assertRan(":test", ":a:test", ":b:test"); + } + + private void assertRan(String... expectedPaths) throws IOException { + List tasks = gradleRunner().withArguments("test").forwardOutput().build().getTasks(); + Set actualPaths = tasks.stream().map(BuildTask::getPath).collect(Collectors.toSet()); + Assertions.assertThat(actualPaths).containsExactlyInAnyOrder(expectedPaths); + } +} diff --git a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ResourceHarness.java b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ResourceHarness.java index ef35d43..bb62ede 100644 --- a/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ResourceHarness.java +++ b/spotless-changelog-plugin-gradle/src/test/java/com/diffplug/spotless/changelog/gradle/ResourceHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 DiffPlug + * Copyright (C) 2019-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,24 @@ */ package com.diffplug.spotless.changelog.gradle; +import static org.assertj.core.api.Assertions.assertThat; +import com.diffplug.common.base.Errors; import com.diffplug.common.io.Resources; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import org.assertj.core.api.AbstractFileAssert; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ListAssert; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import org.assertj.core.api.AbstractCharSequenceAssert; +import org.assertj.core.util.CheckReturnValue; import org.junit.Rule; import org.junit.rules.TemporaryFolder; @@ -37,61 +41,156 @@ public class ResourceHarness { * On OS X, the temp folder is a symlink, * and some of gradle's stuff breaks symlinks. * By only accessing it through the {@link #rootFolder()} - * and {@link #newFile()} apis, we can guarantee there + * and {@link #newFile(String)} apis, we can guarantee there * will be no symlink problems. */ @Rule public TemporaryFolder folderDontUseDirectly = new TemporaryFolder(); /** Returns the root folder (canonicalized to fix OS X issue) */ - protected File rootFolder() throws IOException { - return folderDontUseDirectly.getRoot().getCanonicalFile(); + protected File rootFolder() { + return Errors.rethrow().get(() -> folderDontUseDirectly.getRoot().getCanonicalFile()); } - /** Returns a File (in a temporary folder) which has the given contents. */ - protected File file(String subpath) throws IOException { + /** Returns a new child of the root folder. */ + protected File newFile(String subpath) throws IOException { return new File(rootFolder(), subpath); } + /** Creates and returns a new child-folder of the root folder. */ + protected File newFolder(String subpath) throws IOException { + File targetDir = newFile(subpath); + if (!targetDir.mkdir()) { + throw new IOException("Failed to create " + targetDir); + } + return targetDir; + } + protected String read(String path) throws IOException { - return new String(Files.readAllBytes(file(path).toPath()), StandardCharsets.UTF_8); + return read(newFile(path).toPath(), StandardCharsets.UTF_8); } - /** Returns a File (in a temporary folder) which has the given contents. */ - protected File write(String subpath, byte[] content) throws IOException { - File file = file(subpath); - file.getParentFile().mkdirs(); - Files.write(file.toPath(), content); - return file; + protected String read(Path path, Charset encoding) throws IOException { + return new String(Files.readAllBytes(path), encoding); + } + + protected void replace(String path, String toReplace, String replaceWith) throws IOException { + String before = read(path); + String after = before.replace(toReplace, replaceWith); + if (before.equals(after)) { + throw new IllegalArgumentException("Replace was ineffective! '" + toReplace + "' was not found in " + path); + } + setFile(path).toContent(after); + } + + /** Returns the contents of the given file from the src/test/resources directory. */ + protected static String getTestResource(String filename) throws IOException { + URL url = ResourceHarness.class.getResource("/" + filename); + if (url == null) { + throw new IllegalArgumentException("No such resource " + filename); + } + return Resources.toString(url, StandardCharsets.UTF_8); + } + + /** Returns Files (in a temporary folder) which has the contents of the given file from the src/test/resources directory. */ + protected List createTestFiles(String... filenames) throws IOException { + List files = new ArrayList<>(filenames.length); + for (String filename : filenames) { + files.add(createTestFile(filename)); + } + return files; + } + + /** Returns a File (in a temporary folder) which has the contents of the given file from the src/test/resources directory. */ + protected File createTestFile(String filename) throws IOException { + return createTestFile(filename, UnaryOperator.identity()); } - /** Writes the given content to the given path. */ - protected File write(String path, String... lines) throws IOException { - File file = file(path); + /** + * Returns a File (in a temporary folder) which has the contents, possibly processed, of the given file from the + * src/test/resources directory. + */ + protected File createTestFile(String filename, UnaryOperator fileContentsProcessor) throws IOException { + int lastSlash = filename.lastIndexOf('/'); + String name = lastSlash >= 0 ? filename.substring(lastSlash) : filename; + File file = newFile(name); file.getParentFile().mkdirs(); - Files.write(file.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8); + Files.write(file.toPath(), fileContentsProcessor.apply(getTestResource(filename)).getBytes(StandardCharsets.UTF_8)); return file; } - protected AbstractFileAssert assertFile(String path) throws IOException { - return Assertions.assertThat(file(path)); + @CheckReturnValue + protected ReadAsserter assertFile(String path) throws IOException { + return new ReadAsserter(newFile(path)); + } + + @CheckReturnValue + protected ReadAsserter assertFile(File file) throws IOException { + return new ReadAsserter(file); } - protected ListAssert assertFolderContent(String path) throws IOException { - List children = new ArrayList<>(); - for (File child : file(path).listFiles()) { - children.add(child.getName()); + public static class ReadAsserter { + private final File file; + + private ReadAsserter(File file) { + this.file = file; + } + + public void hasContent(String expected) { + hasContent(expected, StandardCharsets.UTF_8); + } + + public void hasContent(String expected, Charset charset) { + assertThat(file).usingCharset(charset).hasContent(expected); + } + + public void hasLines(String... lines) { + hasContent(String.join("\n", Arrays.asList(lines))); + } + + public void sameAsResource(String resource) throws IOException { + hasContent(getTestResource(resource)); + } + + public void matches(Consumer> conditions) throws IOException { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + conditions.accept(assertThat(content)); } - Collections.sort(children); - return Assertions.assertThat(children); } - /** Returns the contents of the given file from the src/test/resources directory. */ - protected static byte[] readTestResource(String filename) throws IOException { - URL url = ResourceHarness.class.getResource("/" + filename); - if (url == null) { - throw new IllegalArgumentException("No such resource " + filename); + protected WriteAsserter setFile(String path) throws IOException { + return new WriteAsserter(newFile(path)); + } + + public static class WriteAsserter { + private File file; + + private WriteAsserter(File file) { + file.getParentFile().mkdirs(); + this.file = file; + } + + public File toLines(String... lines) throws IOException { + return toContent(String.join("\n", Arrays.asList(lines))); + } + + public File toContent(String content) throws IOException { + return toContent(content, StandardCharsets.UTF_8); + } + + public File toContent(String content, Charset charset) throws IOException { + Files.write(file.toPath(), content.getBytes(charset)); + return file; + } + + public File toResource(String path) throws IOException { + Files.write(file.toPath(), getTestResource(path).getBytes(StandardCharsets.UTF_8)); + return file; + } + + public File deleted() throws IOException { + Files.delete(file.toPath()); + return file; } - return Resources.toByteArray(url); } }