Skip to content

Commit 1a1eb05

Browse files
feat(#4846): Implement simple cache mechanism for EO compilation
1 parent 7148c77 commit 1a1eb05

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.security.MessageDigest;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.util.Base64;
13+
import org.cactoos.Func;
14+
import org.cactoos.func.UncheckedFunc;
15+
16+
/**
17+
* Simple cache mechanism.
18+
* @since 0.60
19+
*/
20+
final class Cache {
21+
22+
/**
23+
* Base cache directory.
24+
*/
25+
private final Path base;
26+
27+
/**
28+
* Compilation function.
29+
*/
30+
private final Func<Path, String> compilation;
31+
32+
/**
33+
* Ctor.
34+
* @param base Base cache directory
35+
* @param compilation Compilation function
36+
*/
37+
Cache(final Path base, final Func<Path, String> compilation) {
38+
this.base = base;
39+
this.compilation = compilation;
40+
}
41+
42+
/**
43+
* Check cache and apply compilation if needed.
44+
* @param source From file
45+
* @param target To file
46+
* @param tail Tail path in cache
47+
*/
48+
public void apply(final Path source, final Path target, final Path tail) {
49+
try {
50+
final String hash = Cache.sha(source);
51+
final Path hfile = this.hash(tail);
52+
final Path cfile = this.base.resolve(tail);
53+
if (Files.notExists(hfile) || !Files.readString(hfile).equals(hash)) {
54+
final String content = new UncheckedFunc<>(this.compilation).apply(source);
55+
Files.writeString(this.hash(tail), hash);
56+
Files.writeString(cfile, content);
57+
Files.writeString(target, content);
58+
} else {
59+
Files.writeString(target, Files.readString(cfile));
60+
}
61+
} catch (final IOException ioexception) {
62+
throw new IllegalStateException(
63+
"Failed to perform an IO operation with cache",
64+
ioexception
65+
);
66+
} catch (final NoSuchAlgorithmException exception) {
67+
throw new IllegalStateException("SHA-256 hashing algorithm isn't found", exception);
68+
}
69+
}
70+
71+
/**
72+
* Get hash file path for the given tail.
73+
* @param tail Tail path
74+
* @return Hash file path
75+
*/
76+
private Path hash(final Path tail) {
77+
final Path full = this.base.resolve(tail);
78+
return full.getParent().resolve(String.format("%s.sha256", full.getFileName().toString()));
79+
}
80+
81+
/**
82+
* Calculate SHA-256 hash of a file and return it as Base64 string.
83+
* @param file File path
84+
* @return Base64-encoded SHA-256 hash
85+
* @throws NoSuchAlgorithmException If SHA-256 algorithm is not available
86+
* @throws IOException If an I/O error occurs reading the file
87+
*/
88+
private static String sha(final Path file) throws NoSuchAlgorithmException, IOException {
89+
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
90+
final byte[] hash = digest.digest(Files.readAllBytes(file));
91+
return Base64.getEncoder().encodeToString(hash);
92+
}
93+
}
94+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import com.yegor256.Mktmp;
8+
import com.yegor256.MktmpResolver;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
import org.hamcrest.MatcherAssert;
13+
import org.hamcrest.Matchers;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
17+
/**
18+
* Test for {@link Cache}.
19+
* @since 0.60
20+
*/
21+
@ExtendWith(MktmpResolver.class)
22+
final class CacheTest {
23+
24+
@Test
25+
void compilesSourceAndAddsToCache(@Mktmp final Path temp) throws Exception {
26+
final var base = temp.resolve("cache");
27+
Files.createDirectories(base);
28+
final var source = temp.resolve("source.eo");
29+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO!\") > @\n");
30+
final var target = temp.resolve("target.xmir");
31+
final var tail = source.getFileName();
32+
final String content = "compiled";
33+
new Cache(base, p -> content).apply(source, target, tail);
34+
MatcherAssert.assertThat(
35+
"Target file must be created from source",
36+
Files.readString(target),
37+
Matchers.equalTo(content)
38+
);
39+
MatcherAssert.assertThat(
40+
"Cache file must be created",
41+
Files.exists(base.resolve(tail)),
42+
Matchers.is(true)
43+
);
44+
MatcherAssert.assertThat(
45+
"Hash file must be created",
46+
Files.exists(base.resolve(String.format("%s.sha256", tail))),
47+
Matchers.is(true)
48+
);
49+
}
50+
51+
@Test
52+
void readsFromCacheWhenUnchanged(@Mktmp final Path temp) throws Exception {
53+
final var base = temp.resolve("cache-directory");
54+
Files.createDirectories(base);
55+
final var source = temp.resolve("stdin.eo");
56+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO!\") > @\n");
57+
final var target = temp.resolve("stdin.xmir");
58+
final var counter = new AtomicInteger(0);
59+
final var cache = new Cache(
60+
base,
61+
p -> String.format("stdin %d", counter.incrementAndGet())
62+
);
63+
final Path tail = source.getFileName();
64+
cache.apply(source, target, tail);
65+
MatcherAssert.assertThat(
66+
"Compilation should happen only once",
67+
counter.get(),
68+
Matchers.equalTo(1)
69+
);
70+
cache.apply(source, target, tail);
71+
MatcherAssert.assertThat(
72+
"Compilation should not happen again",
73+
counter.get(),
74+
Matchers.equalTo(1)
75+
);
76+
}
77+
78+
@Test
79+
void compilesAgainWhenChanged(@Mktmp final Path temp) throws Exception {
80+
final var base = temp.resolve("cache-base-dir");
81+
Files.createDirectories(base);
82+
final var source = temp.resolve("stdout.eo");
83+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO!\") > @\n");
84+
final var target = temp.resolve("stdout.xmir");
85+
final var counter = new AtomicInteger(0);
86+
final var cache = new Cache(
87+
base,
88+
p -> String.format("compiled %d", counter.incrementAndGet())
89+
);
90+
cache.apply(source, target, source.getFileName());
91+
MatcherAssert.assertThat(
92+
"Compilation should happen once",
93+
counter.get(),
94+
Matchers.equalTo(1)
95+
);
96+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO! Modified\") > @\n");
97+
cache.apply(source, target, source.getFileName());
98+
MatcherAssert.assertThat(
99+
"Compilation should happen again after source change",
100+
counter.get(),
101+
Matchers.equalTo(2)
102+
);
103+
}
104+
}

0 commit comments

Comments
 (0)