implements PR-026 walker cache input and comparison policy
This commit is contained in:
parent
2870a0677b
commit
1442adc7b3
@ -5,6 +5,7 @@ import java.nio.file.Path;
|
|||||||
public record PackerFileProbe(
|
public record PackerFileProbe(
|
||||||
Path path,
|
Path path,
|
||||||
String mimeType,
|
String mimeType,
|
||||||
|
long size,
|
||||||
long lastModified,
|
long lastModified,
|
||||||
byte[] content) {
|
byte[] content) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.apache.tika.Tika;
|
import org.apache.tika.Tika;
|
||||||
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||||
|
import p.packer.models.PackerAssetCacheEntry;
|
||||||
import p.packer.models.PackerDiagnostic;
|
import p.packer.models.PackerDiagnostic;
|
||||||
|
import p.packer.models.PackerFileCacheEntry;
|
||||||
import p.packer.models.PackerFileProbe;
|
import p.packer.models.PackerFileProbe;
|
||||||
import p.packer.models.PackerProbeResult;
|
import p.packer.models.PackerProbeResult;
|
||||||
import p.packer.models.PackerWalkResult;
|
import p.packer.models.PackerWalkResult;
|
||||||
@ -12,8 +14,12 @@ import p.packer.models.PackerWalkResult;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -37,7 +43,7 @@ public abstract class PackerAbstractBankWalker<R> {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
final var lastModified = filePath.toFile().lastModified();
|
final var lastModified = filePath.toFile().lastModified();
|
||||||
final var fileProbe = new PackerFileProbe(filePath, mimeType, lastModified, bytes);
|
final var fileProbe = new PackerFileProbe(filePath, mimeType, bytes.length, lastModified, bytes);
|
||||||
return Optional.of(fileProbe);
|
return Optional.of(fileProbe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +59,13 @@ public abstract class PackerAbstractBankWalker<R> {
|
|||||||
return path.getFileName().toString().equalsIgnoreCase("asset.json");
|
return path.getFileName().toString().equalsIgnoreCase("asset.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PackerFileProbe> processFileProbeWhenSupported(
|
private List<PackerProbeResult> processFileProbeWhenSupported(
|
||||||
final Path assetRoot,
|
final Path assetRoot,
|
||||||
|
final Optional<PackerAssetCacheEntry> priorAssetCache,
|
||||||
|
final R requirements,
|
||||||
final Set<String> supportedMimeTypes,
|
final Set<String> supportedMimeTypes,
|
||||||
final List<PackerDiagnostic> diagnostics) {
|
final List<PackerDiagnostic> diagnostics) {
|
||||||
final List<PackerFileProbe> supportedFileProbes = new ArrayList<>();
|
final List<PackerProbeResult> probeResults = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
final var validPaths = retrieveValidPaths(assetRoot);
|
final var validPaths = retrieveValidPaths(assetRoot);
|
||||||
for (final var filePath : validPaths) {
|
for (final var filePath : validPaths) {
|
||||||
@ -66,7 +74,12 @@ public abstract class PackerAbstractBankWalker<R> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final var fileProbe = fileProbeMaybe.get();
|
final var fileProbe = fileProbeMaybe.get();
|
||||||
supportedFileProbes.add(fileProbe);
|
final var cachedEntry = resolvePriorFileCache(assetRoot, fileProbe.path(), priorAssetCache);
|
||||||
|
if (cachedEntry.isPresent() && canReuseMetadata(fileProbe, cachedEntry.get())) {
|
||||||
|
probeResults.add(new PackerProbeResult(fileProbe, cachedEntry.get().metadata(), List.of()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
probeResults.add(processFileProbe(fileProbe, requirements));
|
||||||
}
|
}
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
diagnostics.add(new PackerDiagnostic(
|
diagnostics.add(new PackerDiagnostic(
|
||||||
@ -77,16 +90,68 @@ public abstract class PackerAbstractBankWalker<R> {
|
|||||||
true
|
true
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return supportedFileProbes;
|
return probeResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PackerWalkResult walk(
|
public PackerWalkResult walk(
|
||||||
final Path assetRoot,
|
final Path assetRoot,
|
||||||
final R requirements) {
|
final R requirements) {
|
||||||
|
return walk(assetRoot, requirements, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackerWalkResult walk(
|
||||||
|
final Path assetRoot,
|
||||||
|
final R requirements,
|
||||||
|
final Optional<PackerAssetCacheEntry> priorAssetCache) {
|
||||||
final List<PackerDiagnostic> rootDiagnostics = new ArrayList<>();
|
final List<PackerDiagnostic> rootDiagnostics = new ArrayList<>();
|
||||||
final List<PackerProbeResult> probeResults = new ArrayList<>();
|
final List<PackerProbeResult> probeResults = processFileProbeWhenSupported(
|
||||||
processFileProbeWhenSupported(assetRoot, getSupportedMimeTypes(), rootDiagnostics)
|
assetRoot,
|
||||||
.forEach(fileProbe -> probeResults.add(processFileProbe(fileProbe, requirements)));
|
Objects.requireNonNull(priorAssetCache, "priorAssetCache"),
|
||||||
|
requirements,
|
||||||
|
getSupportedMimeTypes(),
|
||||||
|
rootDiagnostics);
|
||||||
return new PackerWalkResult(probeResults, rootDiagnostics);
|
return new PackerWalkResult(probeResults, rootDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean canReuseMetadata(
|
||||||
|
final PackerFileProbe fileProbe,
|
||||||
|
final PackerFileCacheEntry cachedEntry) {
|
||||||
|
if (fileProbe.size() != cachedEntry.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fileProbe.lastModified() > cachedEntry.lastModified()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cachedEntry.mimeType() != null && !cachedEntry.mimeType().equals(fileProbe.mimeType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cachedEntry.fingerprint() == null || cachedEntry.fingerprint().isBlank()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return cachedEntry.fingerprint().equals(computeFingerprint(fileProbe));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String computeFingerprint(final PackerFileProbe fileProbe) {
|
||||||
|
try {
|
||||||
|
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
return HexFormat.of().formatHex(digest.digest(fileProbe.content()));
|
||||||
|
} catch (NoSuchAlgorithmException exception) {
|
||||||
|
throw new IllegalStateException("SHA-256 fingerprint is unavailable", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<PackerFileCacheEntry> resolvePriorFileCache(
|
||||||
|
final Path assetRoot,
|
||||||
|
final Path filePath,
|
||||||
|
final Optional<PackerAssetCacheEntry> priorAssetCache) {
|
||||||
|
if (priorAssetCache.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
final String relativePath = assetRoot.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.relativize(filePath.toAbsolutePath().normalize())
|
||||||
|
.toString()
|
||||||
|
.replace('\\', '/');
|
||||||
|
return priorAssetCache.get().findFile(relativePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class PackerAssetWalker {
|
public class PackerAssetWalker {
|
||||||
@ -34,6 +35,13 @@ public class PackerAssetWalker {
|
|||||||
public PackerWalkResult walk(
|
public PackerWalkResult walk(
|
||||||
final Path assetRoot,
|
final Path assetRoot,
|
||||||
final PackerAssetDeclaration declaration) {
|
final PackerAssetDeclaration declaration) {
|
||||||
|
return walk(assetRoot, declaration, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackerWalkResult walk(
|
||||||
|
final Path assetRoot,
|
||||||
|
final PackerAssetDeclaration declaration,
|
||||||
|
final Optional<PackerAssetCacheEntry> priorAssetCache) {
|
||||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||||
switch (declaration.assetFamily()) {
|
switch (declaration.assetFamily()) {
|
||||||
case TILE_BANK -> {
|
case TILE_BANK -> {
|
||||||
@ -57,7 +65,7 @@ public class PackerAssetWalker {
|
|||||||
true));
|
true));
|
||||||
return new PackerWalkResult(List.of(), diagnostics);
|
return new PackerWalkResult(List.of(), diagnostics);
|
||||||
}
|
}
|
||||||
final var walkResult = tileBankWalker.walk(assetRoot, requirementBuildResult.requirements);
|
final var walkResult = tileBankWalker.walk(assetRoot, requirementBuildResult.requirements, priorAssetCache);
|
||||||
diagnostics.addAll(walkResult.diagnostics());
|
diagnostics.addAll(walkResult.diagnostics());
|
||||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||||
}
|
}
|
||||||
@ -82,7 +90,7 @@ public class PackerAssetWalker {
|
|||||||
true));
|
true));
|
||||||
return new PackerWalkResult(List.of(), diagnostics);
|
return new PackerWalkResult(List.of(), diagnostics);
|
||||||
}
|
}
|
||||||
final var walkResult = soundBankWalker.walk(assetRoot, result.requirements);
|
final var walkResult = soundBankWalker.walk(assetRoot, result.requirements, priorAssetCache);
|
||||||
diagnostics.addAll(walkResult.diagnostics());
|
diagnostics.addAll(walkResult.diagnostics());
|
||||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,177 @@
|
|||||||
|
package p.packer.repositories;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.models.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class PackerAbstractBankWalkerTest {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidatesReuseImmediatelyWhenSizeChanges() throws Exception {
|
||||||
|
final var walker = new CountingWalker();
|
||||||
|
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||||
|
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||||
|
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||||
|
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||||
|
new PackerFileCacheEntry("entry.txt", "text/plain", 1L, lastModified, "cached", Map.of("source", "cache"))));
|
||||||
|
|
||||||
|
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
|
||||||
|
|
||||||
|
assertEquals(1, walker.processCount());
|
||||||
|
assertEquals(0, walker.fingerprintCount());
|
||||||
|
assertEquals(Boolean.TRUE, result.probeResults().getFirst().metadata().get("processed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidatesReuseImmediatelyWhenCurrentLastModifiedIsNewer() throws Exception {
|
||||||
|
final var walker = new CountingWalker();
|
||||||
|
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||||
|
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||||
|
final long currentLastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||||
|
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||||
|
new PackerFileCacheEntry(
|
||||||
|
"entry.txt",
|
||||||
|
"text/plain",
|
||||||
|
Files.size(filePath),
|
||||||
|
currentLastModified - 1L,
|
||||||
|
"cached",
|
||||||
|
Map.of("source", "cache"))));
|
||||||
|
|
||||||
|
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
|
||||||
|
|
||||||
|
assertEquals(1, walker.processCount());
|
||||||
|
assertEquals(0, walker.fingerprintCount());
|
||||||
|
assertEquals(Boolean.TRUE, result.probeResults().getFirst().metadata().get("processed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void evaluatesFingerprintOnlyAfterCheaperChecksRemainValid() throws Exception {
|
||||||
|
final var walker = new CountingWalker();
|
||||||
|
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||||
|
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||||
|
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||||
|
final String fingerprint = sha256(Files.readAllBytes(filePath));
|
||||||
|
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||||
|
new PackerFileCacheEntry(
|
||||||
|
"entry.txt",
|
||||||
|
"text/plain",
|
||||||
|
Files.size(filePath),
|
||||||
|
lastModified,
|
||||||
|
fingerprint,
|
||||||
|
Map.of("source", "cache"))));
|
||||||
|
|
||||||
|
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
|
||||||
|
|
||||||
|
assertEquals(0, walker.processCount());
|
||||||
|
assertEquals(1, walker.fingerprintCount());
|
||||||
|
assertEquals("cache", result.probeResults().getFirst().metadata().get("source"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reusesStableMetadataWithoutFingerprintWhenCheaperChecksStayValid() throws Exception {
|
||||||
|
final var walker = new CountingWalker();
|
||||||
|
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||||
|
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||||
|
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||||
|
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||||
|
new PackerFileCacheEntry(
|
||||||
|
"entry.txt",
|
||||||
|
"text/plain",
|
||||||
|
Files.size(filePath),
|
||||||
|
lastModified,
|
||||||
|
null,
|
||||||
|
Map.of("source", "cache"))));
|
||||||
|
|
||||||
|
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
|
||||||
|
|
||||||
|
assertEquals(0, walker.processCount());
|
||||||
|
assertEquals(0, walker.fingerprintCount());
|
||||||
|
assertEquals("cache", result.probeResults().getFirst().metadata().get("source"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesFileScopedDiagnosticsInFreshWalkOutput() throws Exception {
|
||||||
|
final var walker = new CountingWalker();
|
||||||
|
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||||
|
writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||||
|
|
||||||
|
final var result = walker.walk(assetRoot, "requirements");
|
||||||
|
|
||||||
|
assertEquals(1, result.probeResults().size());
|
||||||
|
assertEquals(1, result.probeResults().getFirst().diagnostics().size());
|
||||||
|
assertTrue(result.probeResults().getFirst().diagnostics().getFirst().message().contains("file-scoped"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path writeText(Path path, String content) throws Exception {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, content);
|
||||||
|
Files.setLastModifiedTime(path, FileTime.fromMillis(Math.max(1L, Files.getLastModifiedTime(path).toMillis())));
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sha256(byte[] bytes) throws Exception {
|
||||||
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CountingWalker extends PackerAbstractBankWalker<String> {
|
||||||
|
private int processCount;
|
||||||
|
private int fingerprintCount;
|
||||||
|
|
||||||
|
private CountingWalker() {
|
||||||
|
super(MAPPER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getSupportedMimeTypes() {
|
||||||
|
return Set.of("text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PackerProbeResult processFileProbe(PackerFileProbe fileProbe, String requirements) {
|
||||||
|
processCount += 1;
|
||||||
|
return new PackerProbeResult(
|
||||||
|
fileProbe,
|
||||||
|
Map.of("processed", true),
|
||||||
|
List.of(new PackerDiagnostic(
|
||||||
|
p.packer.messages.diagnostics.PackerDiagnosticSeverity.WARNING,
|
||||||
|
p.packer.messages.diagnostics.PackerDiagnosticCategory.HYGIENE,
|
||||||
|
"file-scoped diagnostic",
|
||||||
|
fileProbe.path(),
|
||||||
|
false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String computeFingerprint(PackerFileProbe fileProbe) {
|
||||||
|
fingerprintCount += 1;
|
||||||
|
try {
|
||||||
|
return sha256(fileProbe.content());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int processCount() {
|
||||||
|
return processCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fingerprintCount() {
|
||||||
|
return fingerprintCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user