This commit is contained in:
1 parent
886a25a935
commit
24702dc5a0
5 files changed
+289
-62
No files matched your search
+2
-1
@@ -65,6 +65,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
public void start(SettingsMap settings) {
|
||||
}
|
||||
|
||||
/// Creates a task that updates components and synchronizes the client JAR with the resulting manifest.
|
||||
@Override
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.change_version.process"));
|
||||
@@ -83,7 +84,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return gameInstance.getRepository().updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
return dependencyManager.updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
Task<GameInstanceManifest> update = Task.supplyAsync(publishedInstance::getManifest);
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof ComponentRemoteVersion remoteVersion) {
|
||||
|
||||
@@ -66,8 +66,9 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
return new InstallerListPageSkin();
|
||||
}
|
||||
|
||||
/// Rebuilds the component controls for the selected instance, or clears them if none is selected.
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
@Nullable HMCLGameInstance gameInstance = instance.instance();
|
||||
this.gameInstance = gameInstance;
|
||||
if (gameInstance == null) {
|
||||
itemsProperty().clear();
|
||||
@@ -103,7 +104,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(gameInstance, component.getComponentType(), libraryVersion));
|
||||
});
|
||||
|
||||
component.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
component.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -123,7 +124,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(mark.componentType(), InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(mark.version(), false, false));
|
||||
installerItem.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
installerItem.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -148,13 +149,14 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
/// Runs a local component installer for the current modifiable instance and displays its result.
|
||||
private void doInstallOffline(Path file) {
|
||||
if (gameInstance == null || !gameInstance.getManifest().isModifiable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
Task<?> task = repository.updateInstanceAsync(
|
||||
Task<?> task = repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().installComponentLocalAsync(publishedInstance, file));
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -36,6 +37,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/// Provides downloads and game-component installation for one game repository.
|
||||
@@ -163,6 +165,69 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a task that updates an instance's manifest and its own client JAR in one draft.
|
||||
///
|
||||
/// The updater receives the published instance and must return a manifest with the same id.
|
||||
/// If the updated manifest uses this instance's own JAR, that JAR is replaced when the resolved
|
||||
/// client download changes or the previous manifest used another instance's JAR. Downloads are
|
||||
/// compared by SHA-1 when either declares a checksum, otherwise by URL and size. Matching
|
||||
/// checksums identify the same client even when the URLs differ. Referenced JARs are not modified.
|
||||
///
|
||||
/// A replacement is obtained through this manager's download provider and cache before the
|
||||
/// manifest and JAR are committed together. A task failure aborts the draft and retains the
|
||||
/// previously published instance files. Shared cache files written by the tasks are retained.
|
||||
///
|
||||
/// @param <E> the checked exception type thrown while creating the update task
|
||||
/// @param instanceId the instance to update
|
||||
/// @param updater the asynchronous manifest update
|
||||
/// @return the task that commits the updated instance
|
||||
public <E extends Exception> Task<Void> updateInstanceAsync(
|
||||
GameInstanceID instanceId,
|
||||
ExceptionalFunction<GameInstance, Task<GameInstanceManifest>, E> updater) {
|
||||
return Task.composeAsync(() -> {
|
||||
DefaultGameRepositoryDraft draft = repository.openDraft();
|
||||
try {
|
||||
GameInstance instance = draft.getBaseSnapshot().getInstance(instanceId);
|
||||
return Objects.requireNonNull(updater.apply(instance), "Instance updater returned null")
|
||||
.thenComposeAsync(manifest -> {
|
||||
if (!instanceId.equals(manifest.id())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Instance updater changed id from " + instanceId + " to " + manifest.id());
|
||||
}
|
||||
GameInstanceManifest previous = instance.getResolvedManifest();
|
||||
GameInstanceManifest resolved = draft.getBaseSnapshot().resolve(manifest);
|
||||
draft.put(manifest);
|
||||
if (instanceId.equals(resolved.jar())
|
||||
&& (!instanceId.equals(previous.jar())
|
||||
|| !isSameClientDownload(previous.getDownloadInfo(), resolved.getDownloadInfo()))) {
|
||||
return new GameDownloadTask(this, resolved).thenAcceptAsync(
|
||||
jar -> draft.putPrimaryJar(instanceId, jar));
|
||||
}
|
||||
return null;
|
||||
}).thenRunAsync(() -> {
|
||||
draft.commit();
|
||||
}).whenComplete(exception -> {
|
||||
if (draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
});
|
||||
} catch (Throwable exception) {
|
||||
abortDraftAfterFailure(draft, exception);
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Compares client identity by checksum, falling back to URL and size when neither has one.
|
||||
private static boolean isSameClientDownload(DownloadInfo previous, DownloadInfo updated) {
|
||||
@Nullable String previousSha1 = previous.getSha1();
|
||||
@Nullable String updatedSha1 = updated.getSha1();
|
||||
if (previousSha1 != null || updatedSha1 != null) {
|
||||
return previousSha1 != null && previousSha1.equalsIgnoreCase(updatedSha1);
|
||||
}
|
||||
return previous.getUrl().equals(updated.getUrl()) && previous.getSize() == updated.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> checkGameCompletionAsync(
|
||||
GameInstance instance,
|
||||
|
||||
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -525,57 +524,6 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
return Task.supplyAsync(() -> save(instanceManifest));
|
||||
}
|
||||
|
||||
/// Creates a task that updates one registered instance inside an exclusive draft.
|
||||
///
|
||||
/// The updater receives the instance from the immutable published snapshot and must return a
|
||||
/// working manifest with the same id. Its result is staged and committed exactly once. Failure
|
||||
/// or cancellation aborts the draft; shared cache files written by the updater are retained.
|
||||
///
|
||||
/// @param <E> the checked exception type thrown while creating the update task
|
||||
/// @param instanceId the instance to update
|
||||
/// @param updater the asynchronous manifest update
|
||||
/// @return the task that commits the updated manifest
|
||||
public <E extends Exception> Task<Void> updateInstanceAsync(
|
||||
GameInstanceID instanceId,
|
||||
ExceptionalFunction<GameInstance, Task<GameInstanceManifest>, E> updater) {
|
||||
return Task.composeAsync(() -> {
|
||||
DefaultGameRepositoryDraft draft = openDraft();
|
||||
try {
|
||||
return Objects.requireNonNull(
|
||||
updater.apply(draft.getBaseSnapshot().getInstance(instanceId)),
|
||||
"Instance updater returned null")
|
||||
.thenApplyAsync(manifest -> {
|
||||
if (!instanceId.equals(manifest.id())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Instance updater changed id from " + instanceId + " to " + manifest.id());
|
||||
}
|
||||
draft.put(manifest);
|
||||
draft.commit();
|
||||
return manifest;
|
||||
}).whenComplete(exception -> {
|
||||
if (draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
});
|
||||
} catch (Throwable exception) {
|
||||
abortDraftAfterFailure(draft, exception);
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Aborts a draft after task construction fails and preserves any cleanup failure.
|
||||
///
|
||||
/// @param draft the draft to abort
|
||||
/// @param failure the failure that prevented task construction
|
||||
private static void abortDraftAfterFailure(DefaultGameRepositoryDraft draft, Throwable failure) {
|
||||
try {
|
||||
draft.abort();
|
||||
} catch (Exception cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an empty unsealed snapshot for the given layout.
|
||||
///
|
||||
/// @param layout the layout for the new snapshot
|
||||
|
||||
+216
-5
@@ -17,17 +17,29 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.UrlResponseInfo;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -277,18 +289,211 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
assertEquals(GameRepositoryDraft.State.FAILED, draft.getState());
|
||||
}
|
||||
|
||||
/// Runs an asynchronous update using the published instance as immutable context.
|
||||
/// A manifest-only update preserves an existing client JAR.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncCommitsWorkingManifest(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
repository.save(new GameInstanceManifest(id).withMainClass("original.Main"));
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "custom client");
|
||||
|
||||
Task<?> update = repository.updateInstanceAsync(id, workingInstance -> Task.supplyAsync(() ->
|
||||
Task<?> update = createDependencyManager(repository).updateInstanceAsync(id, workingInstance -> Task.supplyAsync(() ->
|
||||
workingInstance.getManifest().withMainClass("updated.Main")));
|
||||
assertTrue(update.executor().test());
|
||||
|
||||
assertEquals("updated.Main", repository.getInstance(id).getManifest().mainClass());
|
||||
assertEquals("custom client", Files.readString(target));
|
||||
}
|
||||
|
||||
/// A version change replaces an existing client JAR with the file selected by the new manifest.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncReplacesClientJar(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory.resolve("game"));
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id).withMainClass("original.Main");
|
||||
repository.save(original);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "old client");
|
||||
Path source = tempDirectory.resolve("new-client.jar");
|
||||
Files.writeString(source, "new client");
|
||||
String sha1 = DigestUtils.digestToString("SHA-1", source);
|
||||
DefaultCacheRepository cache = new DefaultCacheRepository(tempDirectory.resolve("cache"));
|
||||
Path cached = cache.cacheFile(source, "SHA-1", sha1);
|
||||
DefaultDependencyManager manager = new DefaultDependencyManager(
|
||||
repository, new MojangDownloadProvider(), cache);
|
||||
GameInstanceManifest replacement = original.withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/client.jar", sha1)));
|
||||
|
||||
Task<?> update = manager.updateInstanceAsync(id, instance ->
|
||||
new GameDownloadTask(manager, replacement).thenApplyAsync(jar -> {
|
||||
assertEquals("old client", Files.readString(target));
|
||||
assertEquals(original, repository.getInstance(id).getManifest());
|
||||
return replacement;
|
||||
}));
|
||||
assertTrue(update.executor().test());
|
||||
|
||||
assertEquals(replacement, repository.getInstance(id).getManifest());
|
||||
assertTrue(replacement.getDownloadInfo().validateChecksum(target, false));
|
||||
assertEquals("new client", Files.readString(cached));
|
||||
}
|
||||
|
||||
/// Equivalent checksums preserve a customized client even when the download URL changes.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncPreservesClientWithSameChecksum(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id).withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/old.jar", "a".repeat(40))));
|
||||
repository.save(original);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "custom client");
|
||||
GameInstanceManifest replacement = original.withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/new.jar", "A".repeat(40))));
|
||||
|
||||
assertTrue(createDependencyManager(repository).updateInstanceAsync(id,
|
||||
instance -> Task.completed(replacement)).executor().test());
|
||||
|
||||
assertEquals(replacement, repository.getInstance(id).getManifest());
|
||||
assertEquals("custom client", Files.readString(target));
|
||||
}
|
||||
|
||||
/// A changed URL selects a replacement when neither client download declares a checksum.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncReplacesClientWithoutChecksum(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id).withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/old.jar")));
|
||||
repository.save(original);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "old client");
|
||||
String newUrl = "https://example.invalid/new.jar";
|
||||
DefaultDependencyManager manager = createDependencyManager(repository);
|
||||
manager.getCacheRepository().cacheText(new UrlResponseInfo(200, URI.create(newUrl),
|
||||
HttpHeaders.of(Map.of("etag", List.of("new-client"),
|
||||
"cache-control", List.of("max-age=3600")), (name, value) -> true)), "new client");
|
||||
GameInstanceManifest replacement = original.withDownloads(Map.of(DownloadType.CLIENT, new DownloadInfo(newUrl)));
|
||||
|
||||
assertTrue(manager.updateInstanceAsync(id, instance -> Task.completed(replacement)).executor().test());
|
||||
|
||||
assertEquals(replacement, repository.getInstance(id).getManifest());
|
||||
assertEquals("new client", Files.readString(target));
|
||||
}
|
||||
|
||||
/// Client changes supplied only by patches are resolved before choosing the replacement JAR.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncResolvesClientPatch(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id);
|
||||
repository.save(original);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "old client");
|
||||
Path source = tempDirectory.resolve("new-client.jar");
|
||||
Files.writeString(source, "new client");
|
||||
String sha1 = DigestUtils.digestToString("SHA-1", source);
|
||||
DefaultDependencyManager manager = createDependencyManager(repository);
|
||||
manager.getCacheRepository().cacheFile(source, "SHA-1", sha1);
|
||||
GameInstanceManifest replacement = original.withPatches(List.of(GameInstancePatch.fromManifest(
|
||||
original.withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/client.jar", sha1))),
|
||||
GameComponentType.GAME.getPatchId(), "new-version", GameInstancePatch.PRIORITY_MC)));
|
||||
|
||||
assertTrue(manager.updateInstanceAsync(id, instance -> Task.completed(replacement)).executor().test());
|
||||
|
||||
assertEquals(replacement, repository.getInstance(id).getManifest());
|
||||
assertEquals("new client", Files.readString(target));
|
||||
}
|
||||
|
||||
/// Switching JAR ownership materializes an owned client but never replaces a referenced client.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testUpdateInstanceAsyncSwitchesJarOwnership(boolean useOwnJar, @TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceID sharedId = new GameInstanceID("shared");
|
||||
repository.save(new GameInstanceManifest(sharedId));
|
||||
Path sharedJar = repository.getLayout().getInstanceJarFile(sharedId);
|
||||
Files.writeString(sharedJar, "shared client");
|
||||
String sha1 = DigestUtils.digestToString("SHA-1", sharedJar);
|
||||
DownloadInfo download = new DownloadInfo("https://example.invalid/client.jar", sha1);
|
||||
GameInstanceManifest original = new GameInstanceManifest(id)
|
||||
.withJar(useOwnJar ? sharedId : id)
|
||||
.withDownloads(useOwnJar ? Map.of(DownloadType.CLIENT, download) : null);
|
||||
repository.save(original);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "custom client");
|
||||
DefaultDependencyManager manager = createDependencyManager(repository);
|
||||
manager.getCacheRepository().cacheFile(sharedJar, "SHA-1", sha1);
|
||||
GameInstanceManifest replacement = original.withJar(useOwnJar ? id : sharedId)
|
||||
.withDownloads(Map.of(DownloadType.CLIENT, download));
|
||||
|
||||
assertTrue(manager.updateInstanceAsync(id, instance -> Task.completed(replacement)).executor().test());
|
||||
|
||||
assertEquals(useOwnJar ? "shared client" : "custom client", Files.readString(target));
|
||||
assertEquals("shared client", Files.readString(sharedJar));
|
||||
assertEquals(useOwnJar ? target : sharedJar, repository.getInstance(id).getInstanceJarFile());
|
||||
}
|
||||
|
||||
/// A failed JAR lookup leaves both the old manifest and its client JAR intact.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncPreservesFilesWhenJarTaskFails(@TempDir Path tempDirectory) throws Exception {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id).withMainClass("original.Main");
|
||||
repository.save(original);
|
||||
Path manifestFile = repository.getLayout().getInstanceJson(id);
|
||||
String originalJson = Files.readString(manifestFile);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "original");
|
||||
|
||||
GameInstanceManifest replacement = original.withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("file:///missing-client.jar", "0".repeat(40))));
|
||||
Task<?> update = createDependencyManager(repository).updateInstanceAsync(id,
|
||||
instance -> Task.completed(replacement));
|
||||
assertFalse(update.executor().test());
|
||||
|
||||
assertEquals(original, repository.getInstance(id).getManifest());
|
||||
assertEquals(originalJson, Files.readString(manifestFile));
|
||||
assertEquals("original", Files.readString(target));
|
||||
try (DefaultGameRepositoryDraft draft = repository.openDraft()) {
|
||||
assertTrue(draft.isOpen());
|
||||
}
|
||||
}
|
||||
|
||||
/// Publication failure rolls back both files after an asynchronous update stages a new JAR.
|
||||
@Test
|
||||
public void testUpdateInstanceAsyncRollsBackManifestAndJar(@TempDir Path tempDirectory) throws Exception {
|
||||
FailingRepository repository = new FailingRepository(tempDirectory.resolve("game"));
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
GameInstanceManifest original = new GameInstanceManifest(id).withMainClass("original.Main");
|
||||
repository.save(original);
|
||||
Path manifestFile = repository.getLayout().getInstanceJson(id);
|
||||
String originalJson = Files.readString(manifestFile);
|
||||
Path target = repository.getLayout().getInstanceJarFile(id);
|
||||
Files.writeString(target, "original");
|
||||
Path source = tempDirectory.resolve("replacement.jar");
|
||||
Files.writeString(source, "replacement");
|
||||
repository.failDraftPublish = true;
|
||||
|
||||
DefaultDependencyManager manager = createDependencyManager(repository);
|
||||
String sha1 = DigestUtils.digestToString("SHA-1", source);
|
||||
Path cached = manager.getCacheRepository().cacheFile(source, "SHA-1", sha1);
|
||||
GameInstanceManifest replacement = original.withMainClass("updated.Main")
|
||||
.withDownloads(Map.of(DownloadType.CLIENT,
|
||||
new DownloadInfo("https://example.invalid/client.jar", sha1)));
|
||||
Task<?> update = manager.updateInstanceAsync(id, instance -> Task.completed(replacement));
|
||||
assertFalse(update.executor().test());
|
||||
|
||||
assertEquals(original, repository.getInstance(id).getManifest());
|
||||
assertEquals(originalJson, Files.readString(manifestFile));
|
||||
assertEquals("original", Files.readString(target));
|
||||
assertEquals("replacement", Files.readString(source));
|
||||
assertEquals("replacement", Files.readString(cached));
|
||||
try (DefaultGameRepositoryDraft draft = repository.openDraft()) {
|
||||
assertTrue(draft.isOpen());
|
||||
}
|
||||
}
|
||||
|
||||
/// Rejects an asynchronous update that attempts to create a different instance id.
|
||||
@@ -299,7 +504,7 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
GameInstanceID otherId = new GameInstanceID("other");
|
||||
repository.save(new GameInstanceManifest(id).withMainClass("original.Main"));
|
||||
|
||||
Task<?> update = repository.updateInstanceAsync(id, workingInstance ->
|
||||
Task<?> update = createDependencyManager(repository).updateInstanceAsync(id, workingInstance ->
|
||||
Task.supplyAsync(() -> workingInstance.getManifest().withId(otherId)));
|
||||
assertFalse(update.executor().test());
|
||||
|
||||
@@ -316,7 +521,7 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("missing");
|
||||
|
||||
Task<?> update = repository.updateInstanceAsync(id, workingInstance ->
|
||||
Task<?> update = createDependencyManager(repository).updateInstanceAsync(id, workingInstance ->
|
||||
Task.completed(workingInstance.getManifest()));
|
||||
assertFalse(update.executor().test());
|
||||
|
||||
@@ -332,7 +537,7 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
repository.save(new GameInstanceManifest(id));
|
||||
|
||||
Task<?> update = repository.updateInstanceAsync(id, workingInstance -> {
|
||||
Task<?> update = createDependencyManager(repository).updateInstanceAsync(id, workingInstance -> {
|
||||
throw new IOException("Simulated updater failure");
|
||||
});
|
||||
assertFalse(update.executor().test());
|
||||
@@ -342,6 +547,12 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a dependency manager with a cache isolated inside the test repository.
|
||||
private static DefaultDependencyManager createDependencyManager(TestRepository repository) {
|
||||
return new DefaultDependencyManager(repository, new MojangDownloadProvider(),
|
||||
new DefaultCacheRepository(repository.getBaseDirectory().resolve("cache")));
|
||||
}
|
||||
|
||||
/// Minimal repository implementation for draft tests.
|
||||
@NotNullByDefault
|
||||
private static class TestRepository extends DefaultGameRepository {
|
||||
|
||||
Reference in new issue
Block a user