Skip to content

Commit

Permalink
Issues/159: Support Gradle configuration cache
Browse files Browse the repository at this point in the history
  • Loading branch information
fmck3516 authored Oct 25, 2024
1 parent 23ab0da commit 649931e
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 82 deletions.
5 changes: 3 additions & 2 deletions skippy-core/src/main/java/io/skippy/core/SkippyBuildApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public final class SkippyBuildApi {
private final SkippyConfiguration skippyConfiguration;
private final ClassFileCollector classFileCollector;
private final SkippyRepository skippyRepository;
private final Set<String> failedTests = new HashSet<>();

/**
* C'tor.
Expand All @@ -61,6 +60,7 @@ public void deleteSkippyFolder() {
*/
public void buildStarted() {
skippyRepository.deleteLogFiles();
skippyRepository.deleteListOfFailedTests();
skippyRepository.saveConfiguration(skippyConfiguration);
}

Expand Down Expand Up @@ -101,7 +101,7 @@ private void generateCoverageForSkippedTests(TestImpactAnalysis testImpactAnalys
* @param className the class name of the failed tests
*/
public void testFailed(String className) {
failedTests.add(className);
skippyRepository.recordFailedTest(className);
}

private TestImpactAnalysis getTestImpactAnalysis() {
Expand All @@ -117,6 +117,7 @@ private List<AnalyzedTest> getAnalyzedTests(
TestWithJacocoExecutionDataAndCoveredClasses testWithExecutionData,
ClassFileContainer classFileContainer
) {
var failedTests = skippyRepository.readListOfFailedTests();
var testResult = failedTests.contains(testWithExecutionData.testClassName()) ? TestResult.FAILED : TestResult.PASSED;
var ids = classFileContainer.getIdsByClassName(testWithExecutionData.testClassName());
var executionId = skippyConfiguration.generateCoverageForSkippedTests() ?
Expand Down
38 changes: 37 additions & 1 deletion skippy-core/src/main/java/io/skippy/core/SkippyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static java.nio.file.Files.*;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;

/**
Expand Down Expand Up @@ -284,4 +286,38 @@ private void deleteTemporaryExecutionDataFilesForCurrentBuild() {
}
}

}
void deleteListOfFailedTests() {
var failedTestsFile = SkippyFolder.get(projectDir).resolve("failed-tests.txt");
if (exists(failedTestsFile)) {
try {
delete(failedTestsFile);
} catch (IOException e) {
throw new UncheckedIOException("Unable to delete %s.".formatted(failedTestsFile), e);
}
}

}

void recordFailedTest(String className) {
var failedTestsFile = SkippyFolder.get(projectDir).resolve("failed-tests.txt");
try {
Files.write(failedTestsFile, asList(className), StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
} catch (IOException e) {
throw new UncheckedIOException("Unable record failed test %s in %s: %s.".formatted(className, failedTestsFile, e.getMessage()), e);
}
}

List<String> readListOfFailedTests() {
var failedTestsFile = SkippyFolder.get(projectDir).resolve("failed-tests.txt");
try {
if (false == exists(failedTestsFile)) {
return emptyList();
}
return Files.readAllLines(failedTestsFile, StandardCharsets.UTF_8);

} catch (IOException e) {
throw new UncheckedIOException("Unable to read %s: %s.".formatted(failedTestsFile, e.getMessage()), e);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ void testEmptySkippyFolderWithTwoExecFilesOneFailedTests() throws JSONException
));

buildApi.testFailed("com.example.FooTest");
verify(skippyRepository).recordFailedTest("com.example.FooTest");
when(skippyRepository.readListOfFailedTests()).thenReturn(asList("com.example.FooTest"));

var tiaCaptor = ArgumentCaptor.forClass(TestImpactAnalysis.class);
buildApi.buildFinished();
Expand Down Expand Up @@ -586,6 +588,8 @@ void testExistingJsonFileNewTestFailure() throws JSONException {
));

buildApi.testFailed("com.example.FooTest");
verify(skippyRepository).recordFailedTest("com.example.FooTest");
when(skippyRepository.readListOfFailedTests()).thenReturn(asList("com.example.FooTest"));

var tiaCaptor = ArgumentCaptor.forClass(TestImpactAnalysis.class);
buildApi.buildFinished();
Expand Down
18 changes: 18 additions & 0 deletions skippy-core/src/test/java/io/skippy/core/SkippyRepositoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import static java.nio.file.Files.*;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -211,4 +212,21 @@ void readLatestTestImpactAnalysis_json_file_does_match_version_in_latest() throw
assertEquals(testImpactAnalysis, skippyRepository.readLatestTestImpactAnalysis());
}

@Test
void testDeleteListOfFailedTests() {
skippyRepository.recordFailedTest("com.example.FooTest");
assertTrue(exists(skippyFolder.resolve("failed-tests.txt")));
skippyRepository.deleteListOfFailedTests();;
assertFalse(exists(skippyFolder.resolve("failed-tests.txt")));
}

@Test
void testRecordAndReadListOfFailedTest() throws IOException {
assertEquals(emptyList(), skippyRepository.readListOfFailedTests());
skippyRepository.recordFailedTest("com.example.Test1");
assertEquals(asList("com.example.Test1"), skippyRepository.readListOfFailedTests());
skippyRepository.recordFailedTest("com.example.Test2");
assertEquals(asList("com.example.Test1", "com.example.Test2"), skippyRepository.readListOfFailedTests());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.skippy.gradle;

import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSetContainer;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

/**
* A sub-set of relevant {@link Project} properties that are compatible with Gradle's Configuration Cache.
*/
class CachableProperties {

final boolean sourceSetContainerAvailable;
final List<File> classesDirs;
final SkippyPluginExtension skippyPluginExtension;
final Path projectDir;
final Path buildDir;

private CachableProperties(boolean sourceSetContainerAvailable, List<File> classesDirs, SkippyPluginExtension skippyExtension, Path projectDir, Path buildDir) {
this.sourceSetContainerAvailable = sourceSetContainerAvailable;
this.classesDirs = classesDirs;
this.skippyPluginExtension = skippyExtension;
this.projectDir = projectDir;
this.buildDir = buildDir;
}

static CachableProperties from(Project project) {
var sourceSetContainer = project.getExtensions().findByType(SourceSetContainer.class);
if (sourceSetContainer != null) {
// new ArrayList<>() is a workaround for https://github.com/gradle/gradle/issues/26942
var classesDirs = new ArrayList<>(sourceSetContainer.stream().flatMap(sourceSet -> sourceSet.getOutput().getClassesDirs().getFiles().stream()).toList());
var skippyExtension = project.getExtensions().getByType(SkippyPluginExtension.class);
var projectDir = project.getProjectDir().toPath();
var buildDir = project.getLayout().getBuildDirectory().getAsFile().get().toPath();
return new CachableProperties(sourceSetContainer != null, classesDirs, skippyExtension, projectDir, buildDir);
} else {
return new CachableProperties(false, null, null, null, null);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@
import io.skippy.core.ClassFile;
import io.skippy.core.Profiler;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;

import java.io.File;
import java.nio.file.Path;
import java.util.*;

import static java.util.Comparator.comparing;

/**
* Collects {@link ClassFile}s across all {@link SourceSet}s in a project.
*
Expand All @@ -36,16 +33,11 @@
final class GradleClassFileCollector implements ClassFileCollector {

private final Path projectDir;
private final SourceSetContainer sourceSetContainer;
private final List<File> classesDirs;

/**
* C'tor
*
* @param sourceSetContainer a {@link SourceSetContainer}
*/
GradleClassFileCollector(Path projectDir, SourceSetContainer sourceSetContainer) {
GradleClassFileCollector(Path projectDir, List<File> classesDirs) {
this.projectDir = projectDir;
this.sourceSetContainer = sourceSetContainer;
this.classesDirs = classesDirs;
}

/**
Expand All @@ -57,22 +49,13 @@ final class GradleClassFileCollector implements ClassFileCollector {
public List<ClassFile> collect() {
return Profiler.profile("GradleClassFileCollector#collect", () -> {
var result = new ArrayList<ClassFile>();
for (var sourceSet : sourceSetContainer) {
result.addAll(collect(sourceSet));
for (var classesDir : classesDirs) {
result.addAll(sort(collect(classesDir, classesDir)));
}
return result;
});
}

private List<ClassFile> collect(SourceSet sourceSet) {
var classesDirs = sourceSet.getOutput().getClassesDirs().getFiles();
var result = new ArrayList<ClassFile>();
for (var classesDir : classesDirs) {
result.addAll(sort(collect(classesDir, classesDir)));
}
return result;
}

private List<ClassFile> collect(File outputFolder, File directory) {
var result = new LinkedList<ClassFile>();
File[] files = directory.listFiles();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,29 @@
package io.skippy.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.testing.Test;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Internal;

import javax.inject.Inject;

import org.gradle.api.tasks.testing.TestDescriptor;
import org.gradle.api.tasks.testing.TestListener;
import org.gradle.api.tasks.testing.TestResult;

import java.util.ArrayList;
import java.util.List;

import static io.skippy.gradle.SkippyGradleUtils.*;

/**
* Informs Skippy that the relevant parts of the build (e.g., compilation and testing) have finished.
*/
class SkippyAnalyzeTask extends DefaultTask {
abstract class SkippyAnalyzeTask extends DefaultTask {

@Internal
abstract Property<CachableProperties> getSettings();

@Inject
public SkippyAnalyzeTask() {
setGroup("skippy");
var testFailedListener = new TestFailedListener();
getProject().getTasks().withType(Test.class, testTask -> testTask.addTestListener(testFailedListener));
doLast(task -> {
ifBuildSupportsSkippy(getProject(), skippyBuildApi -> {
for (var failedTest : testFailedListener.failedTests) {
skippyBuildApi.testFailed(failedTest.getClassName());
}
ifBuildSupportsSkippy(getSettings().get(), skippyBuildApi -> {
skippyBuildApi.buildFinished();
});
});
}

static private class TestFailedListener implements TestListener {
private final List<TestDescriptor> failedTests = new ArrayList<>();

@Override
public void beforeSuite(TestDescriptor testDescriptor) {
}

@Override
public void afterSuite(TestDescriptor testDescriptor, TestResult testResult) {
}

@Override
public void beforeTest(TestDescriptor testDescriptor) {
}

@Override
public void afterTest(TestDescriptor testDescriptor, TestResult testResult) {
if (testResult.getResultType() == TestResult.ResultType.FAILURE) {
failedTests.add(testDescriptor);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package io.skippy.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;

import javax.inject.Inject;

Expand All @@ -29,13 +31,16 @@
*
* @author Florian McKee
*/
class SkippyCleanTask extends DefaultTask {
abstract class SkippyCleanTask extends DefaultTask {

@Input
abstract Property<CachableProperties> getSettings();

@Inject
public SkippyCleanTask() {
setGroup("skippy");
doLast(task -> {
ifBuildSupportsSkippy(getProject(), skippyBuildApi -> {
ifBuildSupportsSkippy(getSettings().get(), skippyBuildApi -> {
skippyBuildApi.deleteSkippyFolder();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,22 @@

import io.skippy.core.SkippyBuildApi;
import io.skippy.core.SkippyRepository;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSetContainer;

import java.util.function.Consumer;

final class SkippyGradleUtils {

static void ifBuildSupportsSkippy(Project project, Consumer<SkippyBuildApi> action) {
var sourceSetContainer = project.getExtensions().findByType(SourceSetContainer.class);
if (sourceSetContainer != null) {
var skippyExtension = project.getExtensions().getByType(SkippyPluginExtension.class);
var skippyConfiguration = skippyExtension.toSkippyConfiguration();
var projectDir = project.getProjectDir().toPath();
static void ifBuildSupportsSkippy(CachableProperties settings, Consumer<SkippyBuildApi> action) {
if (settings.sourceSetContainerAvailable) {
var skippyConfiguration = settings.skippyPluginExtension.toSkippyConfiguration();
var projectDir = settings.projectDir;
var skippyBuildApi = new SkippyBuildApi(
skippyConfiguration,
new GradleClassFileCollector(projectDir, sourceSetContainer),
new GradleClassFileCollector(projectDir, settings.classesDirs),
SkippyRepository.getInstance(
skippyConfiguration,
projectDir,
project.getLayout().getBuildDirectory().getAsFile().get().toPath()
settings.buildDir
)
);
action.accept(skippyBuildApi);
Expand Down
Loading

0 comments on commit 649931e

Please sign in to comment.