Iris downloader / History update

- Allow people to download in place and update in place the media
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-18 21:52:19 -04:00
1 parent b18681a3e2
commit edadd14f4b
40 files changed
+836 -122

No files matched your search

@@ -1,11 +1,28 @@
package art.arcane.iris.diagnostics.splash;
import art.arcane.iris.pack.BuiltInPackUpdates;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class IrisSplashComposerTest {
@Test
public void packLinesShowUpdatesAndKeepCustomPacks() {
List<IrisSplashPackScanner.SplashPackMetadata> packs = List.of(
new IrisSplashPackScanner.SplashPackMetadata("custom", "7"),
new IrisSplashPackScanner.SplashPackMetadata("overworld", "4009"),
new IrisSplashPackScanner.SplashPackMetadata("underworld", "1012"));
assertEquals(List.of("Custom Dimensions: 3", " custom v7", " overworld v4009 -> v4010 available",
" underworld v1012", "Update overworld: /iris download pack=overworld overwrite=true"),
IrisSplashComposer.composePackLines(packs, Map.of(
"overworld", new BuiltInPackUpdates.Update("4010"),
"underworld", new BuiltInPackUpdates.Update("1012"))));
}
@Test
public void composeInfoUsesCurrentVersionWithoutStaleReleaseTag() {
String[] info = IrisSplashComposer.composeInfo("4.0.0-26.2", "Paper 26.2", IrisSplashComposer.InfoStyle.PLAIN);
@@ -18,6 +18,31 @@ public class AtomicDirectoryPublisherTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void retainedBackupSurvivesCommitAndRemainsAvailableForRollback() throws Exception {
Path root = temporaryFolder.getRoot().toPath();
Path target = Files.createDirectory(root.resolve("target"));
Files.writeString(target.resolve("value.txt"), "old");
Path staged = Files.createDirectory(root.resolve("stage"));
Files.writeString(staged.resolve("value.txt"), "new");
Path retained = root.resolve("retained");
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staged, target)) {
assertTrue(publication.retainBackup(retained));
}
assertEquals("old", Files.readString(target.resolve("value.txt")));
assertFalse(Files.exists(retained));
Files.createDirectory(staged);
Files.writeString(staged.resolve("value.txt"), "new");
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staged, target)) {
assertTrue(publication.retainBackup(retained));
publication.commit();
}
assertEquals("old", Files.readString(retained.resolve("value.txt")));
assertEquals("new", Files.readString(target.resolve("value.txt")));
}
@Test
public void commitPublishesStagedDirectoryAndRemovesBackup() throws Exception {
Path root = temporaryFolder.getRoot().toPath();
@@ -0,0 +1,129 @@
package art.arcane.iris.pack;
import com.sun.net.httpserver.HttpServer;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BuiltInPackUpdatesTest {
@Test
public void comparesNumericVersionsWithoutDowngradeNotices() {
BuiltInPackUpdates.Update update = new BuiltInPackUpdates.Update("4010");
assertEquals(" -> v4010 available", update.suffix("999"));
assertEquals("", update.suffix("4010"));
assertEquals("", update.suffix("5000"));
assertEquals(" (latest v4010)", update.suffix("custom"));
assertFalse(update.newerThan("custom"));
assertEquals(" (update check unavailable)", BuiltInPackUpdates.Update.unavailable().suffix("4010"));
}
@Test
public void checksOnlyBuiltInPacksAndKeepsSuccessfulResultsOnPartialFailure() throws Exception {
AtomicInteger requests = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", exchange -> {
requests.incrementAndGet();
boolean overworld = exchange.getRequestURI().getPath().equals("/overworld");
byte[] body = (overworld ? release("v4010", "overworld.zip", false) : "rate limited")
.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(overworld ? 200 : 429, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
try (HttpClient client = HttpClient.newHttpClient()) {
Map<String, BuiltInPackUpdates.Update> updates = BuiltInPackUpdates.check(
List.of("overworld", "underworld", "custom", "overworld"), client,
pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/" + pack))
.get(10, TimeUnit.SECONDS);
assertEquals(2, requests.get());
assertEquals("4010", updates.get("overworld").version());
assertEquals(BuiltInPackUpdates.Update.unavailable(), updates.get("underworld"));
assertFalse(updates.containsKey("custom"));
} finally {
server.stop(0);
}
}
@Test
public void rejectsMalformedPrereleaseAndMissingArchiveMetadata() throws Exception {
for (String body : List.of("invalid", "{}", release("4010", "source.zip", false),
release("4010", "overworld.zip", true), release("not-a-version", "overworld.zip", false))) {
assertEquals(BuiltInPackUpdates.Update.unavailable(), checkResponse(body));
}
}
@Test
public void acceptsStableReleaseWithMatchingArchive() throws Exception {
assertTrue(checkResponse(release("V+4010", "overworld.zip", false)).newerThan("4009"));
}
@Test
public void boundsReleaseMetadataSize() throws Exception {
assertEquals(BuiltInPackUpdates.Update.unavailable(), checkResponse(" ".repeat(1024 * 1024 + 1)));
}
@Test
public void timesOutWithoutResponseHeaders() throws Exception {
assertStalledResponseUnavailable(false);
}
@Test
public void timesOutWhenResponseBodyStalls() throws Exception {
assertStalledResponseUnavailable(true);
}
private void assertStalledResponseUnavailable(boolean sendHeaders) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", exchange -> {
if (sendHeaders) {
exchange.sendResponseHeaders(200, 100);
exchange.getResponseBody().write('{');
exchange.getResponseBody().flush();
}
});
server.start();
try (HttpClient client = HttpClient.newHttpClient()) {
assertEquals(BuiltInPackUpdates.Update.unavailable(), BuiltInPackUpdates.check(
List.of("overworld"), client,
pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"))
.get(8, TimeUnit.SECONDS).get("overworld"));
} finally {
server.stop(0);
}
}
private BuiltInPackUpdates.Update checkResponse(String response) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", exchange -> {
byte[] body = response.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
try (HttpClient client = HttpClient.newHttpClient()) {
return BuiltInPackUpdates.check(List.of("overworld"), client,
pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"))
.get(10, TimeUnit.SECONDS).get("overworld");
} finally {
server.stop(0);
}
}
private static String release(String version, String asset, boolean prerelease) {
return "{\"tag_name\":\"" + version + "\",\"draft\":false,\"prerelease\":" + prerelease
+ ",\"assets\":[{\"name\":\"" + asset + "\"}]}";
}
}
@@ -606,6 +606,13 @@ public class PackDownloaderTest {
assertTrue(result.restartRequired());
assertEquals("new", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8));
assertFalse(Files.exists(target.toPath().resolve("old-only.txt")));
File[] backups = new File(packsFolder, ".backups").listFiles();
assertNotNull(backups);
assertEquals(1, backups.length);
assertEquals("old", Files.readString(backups[0].toPath().resolve("state.txt")));
assertEquals("old", Files.readString(backups[0].toPath().resolve("old-only.txt")));
assertTrue(feedback.stream().anyMatch(message -> message.contains(backups[0].getPath())));
assertFalse(PackDirectoryResolver.isVisiblePackDirectory(new File(packsFolder, ".backups")));
assertTransactionStateClean(packsFolder);
assertEquals(0, PackDownloader.downloadLockCount());
}
@@ -7,7 +7,12 @@ import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.task.J;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.Assume;
import org.junit.After;
@@ -35,6 +40,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class StudioSVCWorldPackPublishTest {
@ClassRule
@@ -54,6 +60,34 @@ public class StudioSVCWorldPackPublishTest {
IrisServices.remove(PreservationRegistry.class);
}
@Test
public void brokenSourceIsRejectedBeforeOpeningOrChangingWorldHistory() throws Exception {
Path source = temporaryFolder.newFolder("invalid-generation-update").toPath();
writeValidPack(source);
Files.writeString(source.resolve("biomes/biome.json"), "{");
Path world = temporaryFolder.newFolder("untouched-generation-world").toPath();
Files.writeString(world.resolve("level.dat"), "world-state");
IrisData sourceData = mock(IrisData.class);
when(sourceData.getDataFolder()).thenReturn(source.toFile());
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("main");
dimension.setLoader(sourceData);
VolmitSender sender = mock(VolmitSender.class);
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<GenerationHistory> history = mockStatic(GenerationHistory.class);
MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
assertNull(new StudioSVC().replaceIntoWorld(sender, dimension, world.toFile(), 42L));
history.verifyNoInteractions();
configurator.verifyNoInteractions();
assertFalse(Files.exists(world.resolve("iris")));
assertEquals("world-state", Files.readString(world.resolve("level.dat")));
assertTrue(PackValidationRegistry.isBroken(source));
}
}
@Test
public void copiesToStageAndPublishesTheCompletePack() throws IOException {
Path root = temporaryFolder.newFolder("world").toPath();