diff --git a/plugins/dependency-graph-resolver/src/main/java/eu/f4sten/depgraph/DependencyGraphResolution.java b/plugins/dependency-graph-resolver/src/main/java/eu/f4sten/depgraph/DependencyGraphResolution.java index 6fe7eb25..87325b77 100644 --- a/plugins/dependency-graph-resolver/src/main/java/eu/f4sten/depgraph/DependencyGraphResolution.java +++ b/plugins/dependency-graph-resolver/src/main/java/eu/f4sten/depgraph/DependencyGraphResolution.java @@ -16,14 +16,18 @@ package eu.f4sten.depgraph; import java.util.Set; +import java.util.function.Consumer; import javax.inject.Inject; -import eu.fasten.core.maven.data.ResolvedRevision; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import eu.fasten.core.maven.data.Scope; import eu.fasten.core.maven.resolution.IMavenResolver; +import eu.fasten.core.maven.resolution.MavenResolutionException; import eu.fasten.core.maven.resolution.ResolverConfig; -import eu.fasten.core.maven.resolution.ResolverDepth; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -31,10 +35,13 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; @Path("/depgraph") public class DependencyGraphResolution { + private static final Logger LOG = LoggerFactory.getLogger(DependencyGraphResolution.class); + private IMavenResolver resolver; @Inject @@ -42,48 +49,73 @@ public DependencyGraphResolution(IMavenResolver resolver) { this.resolver = resolver; } - // TODO return a "Response" that allows us to add error information - @GET @Path("/dependents/{groupId}/{artifactId}/{version}") @Produces(MediaType.APPLICATION_JSON) - public Set resolveDependents( // + public Response resolveDependents( // @PathParam("groupId") String groupId, // @PathParam("artifactId") String artifactId, // @PathParam("version") String version, // @QueryParam("resolveAt") Long resolveAt, // - @QueryParam("depth") ResolverDepth depth, // + @QueryParam("depth") String depth, // + @QueryParam("limit") Integer limit, // @QueryParam("scope") Scope scope, // @QueryParam("alwaysIncludeProvided") Boolean alwaysIncludeProvided, // @QueryParam("alwaysIncludeOptional") Boolean alwaysIncludeOptional) { - var config = getConfig(resolveAt, depth, scope, alwaysIncludeProvided, alwaysIncludeOptional); - return resolver.resolveDependents(groupId, artifactId, version, config); + var config = getConfig(resolveAt, depth, limit, scope, alwaysIncludeProvided, alwaysIncludeOptional); + try { + var dpts = resolver.resolveDependents(groupId, artifactId, version, config); + return Response.ok(dpts).build(); + } catch (MavenResolutionException e) { + logError("resolveDependents", Set.of(gav(groupId, artifactId, version)), config, e); + return Response.status(HttpStatus.SC_UNPROCESSABLE_ENTITY, e.getMessage()).build(); + } catch (Exception e) { + logError("resolveDependents", Set.of(gav(groupId, artifactId, version)), config, e); + return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage()).build(); + } + } + + private static String gav(String groupId, String artifactId, String version) { + return String.format("%s:%s:%s", groupId, artifactId, version); } @POST @Path("/dependencies") @Produces(MediaType.APPLICATION_JSON) - public Set resolveDependencies( // + public Response resolveDependencies( // Set gavs, // @QueryParam("resolveAt") Long resolveAt, // - @QueryParam("depth") ResolverDepth depth, // + @QueryParam("depth") String depth, // + @QueryParam("limit") Integer limit, // @QueryParam("scope") Scope scope, // @QueryParam("alwaysIncludeProvided") Boolean alwaysIncludeProvided, // @QueryParam("alwaysIncludeOptional") Boolean alwaysIncludeOptional) { - var config = getConfig(resolveAt, depth, scope, alwaysIncludeProvided, alwaysIncludeOptional); - return resolver.resolveDependencies(gavs, config); + var config = getConfig(resolveAt, depth, limit, scope, alwaysIncludeProvided, alwaysIncludeOptional); + try { + var deps = resolver.resolveDependencies(gavs, config); + return Response.ok(deps).build(); + } catch (MavenResolutionException e) { + logError("resolveDependencies", gavs, config, e); + return Response.status(HttpStatus.SC_UNPROCESSABLE_ENTITY, e.getMessage()).build(); + } catch (Exception e) { + logError("resolveDependencies", gavs, config, e); + return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage()).build(); + } } - private static ResolverConfig getConfig(Long resolveAt, ResolverDepth depth, Scope scope, + private static ResolverConfig getConfig(Long resolveAt, String depth, Integer limit, Scope scope, Boolean alwaysIncludeProvided, Boolean alwaysIncludeOptional) { var cfg = new ResolverConfig(); if (resolveAt != null) { cfg.resolveAt = resolveAt; } if (depth != null) { - cfg.depth = depth; + parseDepth(depth, n -> cfg.depth = n); + } + if (limit != null) { + parseLimit(limit, n -> cfg.limit = n); } if (scope != null) { cfg.scope = scope; @@ -96,4 +128,41 @@ private static ResolverConfig getConfig(Long resolveAt, ResolverDepth depth, Sco } return cfg; } + + private static void parseDepth(String depth, Consumer c) { + depth = depth.toUpperCase().strip(); + + if ("MAX".equals(depth) || "TRANSITIVE".equals(depth)) { + c.accept(Integer.MAX_VALUE); + } else if ("DIRECT".equals(depth)) { + c.accept(1); + } else { + try { + var n = Integer.parseInt(depth); + if (n < 1) { + var msg = "Ignoring invalid depth (%d)"; + LOG.error(String.format(msg, n)); + return; + } + c.accept(n); + } catch (NumberFormatException e) { + var msg = "Ignoring unparseable depth (%s)"; + LOG.error(String.format(msg, depth), e); + } + } + } + + private static void parseLimit(int limit, Consumer c) { + if (limit < 1) { + var msg = "Ignoring invalid limit (%d)"; + LOG.error(String.format(msg, limit)); + return; + } + c.accept(limit); + } + + private static void logError(String endpoint, Set gavs, ResolverConfig cfg, Exception e) { + var msg = "%s in %s(%s, %s):"; + LOG.error(String.format(msg, e.getClass().getSimpleName(), endpoint, gavs, cfg), e); + } } \ No newline at end of file diff --git a/plugins/dependency-graph-resolver/src/test/java/eu/f4sten/depgraph/DependencyGraphResolutionTest.java b/plugins/dependency-graph-resolver/src/test/java/eu/f4sten/depgraph/DependencyGraphResolutionTest.java new file mode 100644 index 00000000..123ef20b --- /dev/null +++ b/plugins/dependency-graph-resolver/src/test/java/eu/f4sten/depgraph/DependencyGraphResolutionTest.java @@ -0,0 +1,311 @@ +/* + * Copyright 2022 Delft University of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.f4sten.depgraph; + +import static eu.f4sten.test.TestLoggerUtils.assertLogsContain; +import static eu.f4sten.test.TestLoggerUtils.clearLog; +import static eu.fasten.core.maven.data.Scope.PROVIDED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.Date; +import java.util.Set; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import eu.f4sten.test.TestLoggerUtils; +import eu.fasten.core.maven.data.ResolvedRevision; +import eu.fasten.core.maven.data.Scope; +import eu.fasten.core.maven.resolution.IMavenResolver; +import eu.fasten.core.maven.resolution.MavenResolutionException; +import eu.fasten.core.maven.resolution.ResolverConfig; + +public class DependencyGraphResolutionTest { + + private static final Set SOME_DEPS = Set.of("a:a:1", "b:b:2"); + private static final Set SOME_RESULTS = Set.of(rr(1), rr(2)); + private static final ResolverConfig DEFAULT_CFG = new ResolverConfig(); + + private DependencyGraphResolution sut; + private IMavenResolver resolver; + private ResolverConfig lastCfg; + + @BeforeEach + public void setup() { + clearLog(); + DEFAULT_CFG.resolveAt = new Date().getTime(); + lastCfg = null; + resolver = mock(IMavenResolver.class); + sut = new DependencyGraphResolution(resolver); + + var cfgCaptor = ArgumentCaptor.forClass(ResolverConfig.class); + var answerWithSomeResults = new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + lastCfg = cfgCaptor.getValue(); + return SOME_RESULTS; + } + }; + when(resolver.resolveDependencies(anySet(), cfgCaptor.capture())).thenAnswer(answerWithSomeResults); + when(resolver.resolveDependents(anyString(), anyString(), anyString(), cfgCaptor.capture())) + .thenAnswer(answerWithSomeResults); + } + + @Test + public void deps_defaultRequest() { + var res = sut.resolveDependencies(SOME_DEPS, null, null, null, null, null, null); + assertResolveAt(); + verify(resolver).resolveDependencies(eq(SOME_DEPS), eq(DEFAULT_CFG)); + + assertEquals(HttpStatus.SC_OK, res.getStatus()); + assertEquals(SOME_RESULTS, res.getEntity()); + } + + @Test + public void deps_resolutionException() { + + when(resolver.resolveDependencies(anySet(), any(ResolverConfig.class))) + .thenThrow(new MavenResolutionException("")); + + var res = sut.resolveDependencies(Set.of("g1:a1:1.1.1"), null, null, null, null, null, null); + var status = res.getStatusInfo(); + assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, status.getStatusCode()); + assertEquals("", status.getReasonPhrase()); + + var logs = TestLoggerUtils.getFormattedLogs(DependencyGraphResolution.class); + assertEquals(1, logs.size()); + var log = String.format("ERROR MavenResolutionException in resolveDependencies([g1:a1:1.1.1], %s", + ResolverConfig.class.getName()); + assertTrue(logs.get(0).startsWith(log)); + } + + @Test + public void deps_generalException() { + + when(resolver.resolveDependencies(anySet(), any(ResolverConfig.class))) + .thenThrow(new RuntimeException("")); + + var res = sut.resolveDependencies(Set.of("g1:a1:1.1.1"), null, null, null, null, null, null); + var status = res.getStatusInfo(); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, status.getStatusCode()); + assertEquals("", status.getReasonPhrase()); + + var logs = TestLoggerUtils.getFormattedLogs(DependencyGraphResolution.class); + assertEquals(1, logs.size()); + var log = String.format("ERROR RuntimeException in resolveDependencies([g1:a1:1.1.1], %s", + ResolverConfig.class.getName()); + assertTrue(logs.get(0).startsWith(log)); + } + + @Test + public void dpts_defaultRequest() { + var actual = sut.resolveDependents("g1", "a1", "1.1.1", null, null, null, null, null, null); + assertResolveAt(); + verify(resolver).resolveDependents(eq("g1"), eq("a1"), eq("1.1.1"), eq(DEFAULT_CFG)); + + assertEquals(HttpStatus.SC_OK, actual.getStatus()); + assertEquals(SOME_RESULTS, actual.getEntity()); + } + + @Test + public void dpts_resolutionException() { + + when(resolver.resolveDependents(anyString(), anyString(), anyString(), any(ResolverConfig.class))) + .thenThrow(new MavenResolutionException("")); + + var res = sut.resolveDependents("g1", "a1", "1.1.1", null, null, null, null, null, null); + var status = res.getStatusInfo(); + assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, status.getStatusCode()); + assertEquals("", status.getReasonPhrase()); + + var logs = TestLoggerUtils.getFormattedLogs(DependencyGraphResolution.class); + assertEquals(1, logs.size()); + var log = String.format("ERROR MavenResolutionException in resolveDependents([g1:a1:1.1.1], %s", + ResolverConfig.class.getName()); + assertTrue(logs.get(0).startsWith(log)); + } + + @Test + public void dpts_generalException() { + + when(resolver.resolveDependents(anyString(), anyString(), anyString(), any(ResolverConfig.class))) + .thenThrow(new RuntimeException("")); + + var res = sut.resolveDependents("g1", "a1", "1.1.1", null, null, null, null, null, null); + var status = res.getStatusInfo(); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, status.getStatusCode()); + assertEquals("", status.getReasonPhrase()); + + var logs = TestLoggerUtils.getFormattedLogs(DependencyGraphResolution.class); + assertEquals(1, logs.size()); + var log = String.format("ERROR RuntimeException in resolveDependents([g1:a1:1.1.1], %s", + ResolverConfig.class.getName()); + assertTrue(logs.get(0).startsWith(log)); + } + + @Test + public void cfgResolveAt() { + testDepsAndDpts(1234L, null, null, null, null, null, () -> { + assertEquals(1234L, lastCfg.resolveAt); + lastCfg.resolveAt = DEFAULT_CFG.resolveAt; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgDepthMax() { + testDepsAndDpts(null, "mAx", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(Integer.MAX_VALUE, lastCfg.depth); + lastCfg.depth = DEFAULT_CFG.depth; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgDepthTrans() { + testDepsAndDpts(null, "TrAnSitive", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(Integer.MAX_VALUE, lastCfg.depth); + lastCfg.depth = DEFAULT_CFG.depth; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgDepthDirect() { + testDepsAndDpts(null, "dIrEcT", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(1, lastCfg.depth); + lastCfg.depth = DEFAULT_CFG.depth; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgDepthNumber() { + testDepsAndDpts(null, "234", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(234, lastCfg.depth); + lastCfg.depth = DEFAULT_CFG.depth; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgDepthInvalid() { + testDepsAndDpts(null, "-1", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(DEFAULT_CFG, lastCfg); + assertLogsContain(DependencyGraphResolution.class, "ERROR Ignoring invalid depth (-1)"); + }); + } + + @Test + public void cfgDepthUnparseable() { + testDepsAndDpts(null, "X", null, null, null, null, () -> { + assertResolveAt(); + assertEquals(DEFAULT_CFG, lastCfg); + assertLogsContain(DependencyGraphResolution.class, "ERROR Ignoring unparseable depth (X)"); + }); + } + + @Test + public void cfgLimit() { + testDepsAndDpts(null, null, 132, null, null, null, () -> { + assertResolveAt(); + assertEquals(132, lastCfg.limit); + lastCfg.limit = DEFAULT_CFG.limit; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgLimitNegative() { + testDepsAndDpts(null, null, -1, null, null, null, () -> { + assertResolveAt(); + assertEquals(DEFAULT_CFG, lastCfg); + assertLogsContain(DependencyGraphResolution.class, "ERROR Ignoring invalid limit (-1)"); + }); + } + + @Test + public void cfgScope() { + testDepsAndDpts(null, null, null, PROVIDED, null, null, () -> { + assertResolveAt(); + assertEquals(PROVIDED, lastCfg.scope); + lastCfg.scope = DEFAULT_CFG.scope; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgInclProvided() { + testDepsAndDpts(null, null, null, null, true, null, () -> { + assertResolveAt(); + assertTrue(lastCfg.alwaysIncludeProvided); + lastCfg.alwaysIncludeProvided = DEFAULT_CFG.alwaysIncludeProvided; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + @Test + public void cfgInclOpt() { + testDepsAndDpts(null, null, null, null, null, true, () -> { + assertResolveAt(); + assertTrue(lastCfg.alwaysIncludeOptional); + lastCfg.alwaysIncludeOptional = DEFAULT_CFG.alwaysIncludeOptional; + assertEquals(DEFAULT_CFG, lastCfg); + }); + } + + private void testDepsAndDpts(Long resolveAt, String depth, Integer limit, Scope scope, + Boolean alwaysIncludeProvided, Boolean alwaysIncludeOptional, Runnable tests) { + + sut.resolveDependents("g1", "a1", "1.1.1", resolveAt, depth, limit, scope, alwaysIncludeProvided, + alwaysIncludeOptional); + tests.run(); + + setup(); + sut.resolveDependencies(Set.of("g1:a1:1.1.1"), resolveAt, depth, limit, scope, alwaysIncludeProvided, + alwaysIncludeOptional); + tests.run(); + } + + private void assertResolveAt() { + assertNotNull(lastCfg); + assertTrue(lastCfg.resolveAt - DEFAULT_CFG.resolveAt < 1000); + lastCfg.resolveAt = DEFAULT_CFG.resolveAt; + } + + private static ResolvedRevision rr(int id) { + return new ResolvedRevision(id, "g" + id, "a" + id, id + ".0.0", new Timestamp(id), Scope.COMPILE); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index ca2db2c6..d3641ef8 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ eu.fasten core - 0.0.13 + 0.0.14 ch.qos.logback