Fix and test FabricApiExtension not supporting deprecated modules. (#950)

This commit is contained in:
modmuss
2023-09-09 22:37:02 +01:00
committed by GitHub
parent e924faf44e
commit 0a3779f41d
9 changed files with 231 additions and 32 deletions

View File

@@ -89,7 +89,7 @@ public class LoomGradlePlugin implements BootstrappedPlugin {
// Setup extensions // Setup extensions
project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project)); project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project));
project.getExtensions().create("fabricApi", FabricApiExtension.class, project); project.getExtensions().create("fabricApi", FabricApiExtension.class);
for (Class<? extends Runnable> jobClass : SETUP_JOBS) { for (Class<? extends Runnable> jobClass : SETUP_JOBS) {
project.getObjects().newInstance(jobClass).run(); project.getObjects().newInstance(jobClass).run();

View File

@@ -26,9 +26,11 @@ package net.fabricmc.loom.configuration;
import java.io.File; import java.io.File;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
@@ -41,25 +43,29 @@ import org.w3c.dom.NodeList;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.util.download.DownloadException; import net.fabricmc.loom.util.download.DownloadException;
public class FabricApiExtension { public abstract class FabricApiExtension {
private final Project project; @Inject
public abstract Project getProject();
public FabricApiExtension(Project project) {
this.project = project;
}
private static final HashMap<String, Map<String, String>> moduleVersionCache = new HashMap<>(); private static final HashMap<String, Map<String, String>> moduleVersionCache = new HashMap<>();
private static final HashMap<String, Map<String, String>> deprecatedModuleVersionCache = new HashMap<>();
public Dependency module(String moduleName, String fabricApiVersion) { public Dependency module(String moduleName, String fabricApiVersion) {
return project.getDependencies() return getProject().getDependencies()
.create(getDependencyNotation(moduleName, fabricApiVersion)); .create(getDependencyNotation(moduleName, fabricApiVersion));
} }
public String moduleVersion(String moduleName, String fabricApiVersion) { public String moduleVersion(String moduleName, String fabricApiVersion) {
String moduleVersion = moduleVersionCache String moduleVersion = moduleVersionCache
.computeIfAbsent(fabricApiVersion, this::populateModuleVersionMap) .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions)
.get(moduleName); .get(moduleName);
if (moduleVersion == null) {
moduleVersion = deprecatedModuleVersionCache
.computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions)
.get(moduleName);
}
if (moduleVersion == null) { if (moduleVersion == null) {
throw new RuntimeException("Failed to find module version for module: " + moduleName); throw new RuntimeException("Failed to find module version for module: " + moduleName);
} }
@@ -71,9 +77,24 @@ public class FabricApiExtension {
return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion));
} }
private Map<String, String> populateModuleVersionMap(String fabricApiVersion) { private Map<String, String> getApiModuleVersions(String fabricApiVersion) {
File pomFile = getApiMavenPom(fabricApiVersion); try {
return populateModuleVersionMap(getApiMavenPom(fabricApiVersion));
} catch (PomNotFoundException e) {
throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion);
}
}
private Map<String, String> getDeprecatedApiModuleVersions(String fabricApiVersion) {
try {
return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion));
} catch (PomNotFoundException e) {
// Not all fabric-api versions have deprecated modules, return an empty map to cache this fact.
return Collections.emptyMap();
}
}
private Map<String, String> populateModuleVersionMap(File pomFile) {
try { try {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
@@ -101,27 +122,36 @@ public class FabricApiExtension {
} }
} }
private File getApiMavenPom(String fabricApiVersion) { private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException {
LoomGradleExtension extension = LoomGradleExtension.get(project); return getPom("fabric-api", fabricApiVersion);
}
File mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/" + fabricApiVersion + ".pom"); private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException {
return getPom("fabric-api-deprecated", fabricApiVersion);
}
if (project.getGradle().getStartParameter().isOffline()) { private File getPom(String name, String version) throws PomNotFoundException {
if (!mavenPom.exists()) { final LoomGradleExtension extension = LoomGradleExtension.get(getProject());
throw new RuntimeException("Cannot retrieve fabric-api pom due to being offline"); final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version));
}
return mavenPom;
}
try { try {
extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion)) extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name))
.defaultCache() .defaultCache()
.downloadPath(mavenPom.toPath()); .downloadPath(mavenPom.toPath());
} catch (DownloadException e) { } catch (DownloadException e) {
throw new UncheckedIOException("Failed to download maven info for " + fabricApiVersion, e); if (e.getStatusCode() == 404) {
throw new PomNotFoundException(e);
}
throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e);
} }
return mavenPom; return mavenPom;
} }
private static class PomNotFoundException extends Exception {
PomNotFoundException(Throwable cause) {
super(cause);
}
}
} }

View File

@@ -129,7 +129,7 @@ public final class Download {
if (!successful) { if (!successful) {
progressListener.onEnd(); progressListener.onEnd();
throw error("HTTP request to (%s) returned unsuccessful status (%d)", url, statusCode); throw statusError("HTTP request to (%s) returned unsuccessful status".formatted(url) + "(%d)", statusCode);
} }
try (InputStream inputStream = decodeOutput(response)) { try (InputStream inputStream = decodeOutput(response)) {
@@ -228,7 +228,7 @@ public final class Download {
} }
} }
} else { } else {
throw error("HTTP request returned unsuccessful status (%d)", statusCode); throw statusError("HTTP request returned unsuccessful status (%d)", statusCode);
} }
if (useEtag) { if (useEtag) {
@@ -430,6 +430,10 @@ public final class Download {
} }
} }
private DownloadException statusError(String message, int statusCode) {
return new DownloadException(String.format(Locale.ENGLISH, message, statusCode), statusCode);
}
private DownloadException error(String message, Object... args) { private DownloadException error(String message, Object... args) {
return new DownloadException(String.format(Locale.ENGLISH, message, args)); return new DownloadException(String.format(Locale.ENGLISH, message, args));
} }

View File

@@ -158,6 +158,11 @@ public class DownloadBuilder {
return supplier.get(build(i)); return supplier.get(build(i));
} catch (DownloadException e) { } catch (DownloadException e) {
if (e.getStatusCode() == 404) {
// Don't retry on 404's
throw e;
}
if (i == maxRetries) { if (i == maxRetries) {
throw new DownloadException(String.format(Locale.ENGLISH, "Failed download after %d attempts", maxRetries), e); throw new DownloadException(String.format(Locale.ENGLISH, "Failed download after %d attempts", maxRetries), e);
} }

View File

@@ -27,15 +27,32 @@ package net.fabricmc.loom.util.download;
import java.io.IOException; import java.io.IOException;
public class DownloadException extends IOException { public class DownloadException extends IOException {
private final int statusCode;
public DownloadException(String message) { public DownloadException(String message) {
super(message); super(message);
statusCode = -1;
}
public DownloadException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
} }
public DownloadException(String message, Throwable cause) { public DownloadException(String message, Throwable cause) {
super(message, cause); super(message, cause);
statusCode = cause instanceof DownloadException downloadException ? downloadException.getStatusCode() : -1;
} }
public DownloadException(Throwable cause) { public DownloadException(Throwable cause) {
super(cause); super(cause);
statusCode = cause instanceof DownloadException downloadException ? downloadException.getStatusCode() : -1;
}
/**
* @return -1 when the status code is unknown.
*/
public int getStatusCode() {
return statusCode;
} }
} }

View File

@@ -0,0 +1,69 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2023 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import org.gradle.api.Project
import spock.lang.Specification
import net.fabricmc.loom.configuration.FabricApiExtension
import net.fabricmc.loom.test.util.GradleTestUtil
class FabricApiExtensionTest extends Specification {
def "get module version"() {
when:
def fabricApi = new FabricApiExtension() {
Project project = GradleTestUtil.mockProject()
}
def version = fabricApi.moduleVersion(moduleName, apiVersion)
then:
version == expectedVersion
where:
moduleName | apiVersion | expectedVersion
"fabric-api-base" | "0.88.3+1.20.2" | "0.4.32+fce67b3299" // Normal module, new version
"fabric-api-base" | "0.13.1+build.257-1.14" | "0.1.2+28f8190f42" // Normal module, old version before deprecated modules.
"fabric-networking-v0" | "0.88.0+1.20.1" | "0.3.50+df3654b377" // Deprecated module, opt-out version
"fabric-networking-v0" | "0.85.0+1.20.1" | "0.3.48+df3654b377" // Deprecated module, opt-in version
}
def "unknown module"() {
when:
def fabricApi = new FabricApiExtension() {
Project project = GradleTestUtil.mockProject()
}
fabricApi.moduleVersion("fabric-api-unknown", apiVersion)
then:
def e = thrown RuntimeException
e.getMessage() == "Failed to find module version for module: fabric-api-unknown"
where:
apiVersion | _
"0.88.0+1.20.1" | _ // Deprecated opt-out
"0.85.0+1.20.1" | _ // Deprecated opt-int
"0.13.1+build.257-1.14" | _ // No deprecated modules
}
}

View File

@@ -76,16 +76,33 @@ class DownloadFileTest extends DownloadTest {
def "File: Not found"() { def "File: Not found"() {
setup: setup:
server.get("/fileNotfound") { server.get("/fileNotfound") {
it.status(404) it.status(HttpStatus.NOT_FOUND)
} }
def output = new File(File.createTempDir(), "file.txt").toPath() def output = new File(File.createTempDir(), "file.txt").toPath()
when: when:
def result = Download.create("$PATH/stringNotFound").downloadPath(output) def result = Download.create("$PATH/fileNotfound").downloadPath(output)
then: then:
thrown DownloadException def e = thrown DownloadException
e.statusCode == 404
}
def "File: Server error"() {
setup:
server.get("/fileServerError") {
it.status(HttpStatus.INTERNAL_SERVER_ERROR)
}
def output = new File(File.createTempDir(), "file.txt").toPath()
when:
def result = Download.create("$PATH/fileServerError").downloadPath(output)
then:
def e = thrown DownloadException
e.statusCode == 500
} }
def "Cache: Sha1"() { def "Cache: Sha1"() {

View File

@@ -46,7 +46,7 @@ class DownloadStringTest extends DownloadTest {
def "String: Not found"() { def "String: Not found"() {
setup: setup:
server.get("/stringNotFound") { server.get("/stringNotFound") {
it.status(404) it.status(HttpStatus.NOT_FOUND)
} }
when: when:
@@ -55,7 +55,24 @@ class DownloadStringTest extends DownloadTest {
.downloadString() .downloadString()
then: then:
thrown DownloadException def e = thrown DownloadException
e.statusCode == 404
}
def "String: Server error"() {
setup:
server.get("/stringNotFound") {
it.status(HttpStatus.INTERNAL_SERVER_ERROR)
}
when:
def result = Download.create("$PATH/stringNotFound")
.maxRetries(3) // Ensure we still error as expected when retrying
.downloadString()
then:
def e = thrown DownloadException
e.statusCode == 500
} }
def "String: Redirect"() { def "String: Redirect"() {
@@ -97,6 +114,25 @@ class DownloadStringTest extends DownloadTest {
result == "Hello World 3" result == "Hello World 3"
} }
def "String: Retries 404"() {
setup:
int requests = 0
server.get("/retryString") {
requests ++
it.status(HttpStatus.NOT_FOUND)
}
when:
def result = Download.create("$PATH/retryString")
.maxRetries(3)
.downloadString()
then:
def e = thrown DownloadException
e.statusCode == 404
requests == 1
}
def "String: File cache"() { def "String: File cache"() {
setup: setup:
server.get("/downloadString2") { server.get("/downloadString2") {

View File

@@ -36,12 +36,16 @@ import org.gradle.api.provider.Property
import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.util.PatternFilterable import org.gradle.api.tasks.util.PatternFilterable
import org.jetbrains.annotations.Nullable import org.jetbrains.annotations.Nullable
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import net.fabricmc.loom.LoomGradleExtension import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.loom.extension.LoomFiles
import net.fabricmc.loom.test.LoomTestConstants
import net.fabricmc.loom.util.download.Download
import static org.mockito.ArgumentMatchers.any import static org.mockito.ArgumentMatchers.any
import static org.mockito.Mockito.mock import static org.mockito.Mockito.*
import static org.mockito.Mockito.when
class GradleTestUtil { class GradleTestUtil {
static <T> Property<T> mockProperty(T value) { static <T> Property<T> mockProperty(T value) {
@@ -73,7 +77,18 @@ class GradleTestUtil {
static LoomGradleExtension mockLoomGradleExtension() { static LoomGradleExtension mockLoomGradleExtension() {
def mock = mock(LoomGradleExtension.class) def mock = mock(LoomGradleExtension.class)
def loomFiles = mockLoomFiles()
when(mock.refreshDeps()).thenReturn(false) when(mock.refreshDeps()).thenReturn(false)
when(mock.getFiles()).thenReturn(loomFiles)
when(mock.download(any())).thenAnswer {
Download.create(it.getArgument(0))
}
return mock
}
static LoomFiles mockLoomFiles() {
def mock = mock(LoomFiles.class, new RequiresStubAnswer())
doReturn(LoomTestConstants.TEST_DIR).when(mock).getUserCache()
return mock return mock
} }
@@ -121,4 +136,10 @@ class GradleTestUtil {
def mock = mock(RepositoryHandler.class) def mock = mock(RepositoryHandler.class)
return mock return mock
} }
static class RequiresStubAnswer implements Answer<Object> {
Object answer(InvocationOnMock invocation) throws Throwable {
throw new RuntimeException("${invocation.getMethod().getName()} is not stubbed")
}
}
} }