Add dependency cycle detection, fix tests

This commit is contained in:
JeremyStar™ 2024-09-04 19:55:32 +02:00
parent 48b0126e5c
commit 2b9487f2a9
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
2 changed files with 184 additions and 42 deletions

View file

@ -19,6 +19,7 @@
package de.staropensource.sosengine.base.utility; package de.staropensource.sosengine.base.utility;
import de.staropensource.sosengine.base.exception.dependency.DependencyCycleException;
import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException; import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException;
import de.staropensource.sosengine.base.implementable.VersioningSystem; import de.staropensource.sosengine.base.implementable.VersioningSystem;
import de.staropensource.sosengine.base.logging.LoggerInstance; import de.staropensource.sosengine.base.logging.LoggerInstance;
@ -51,14 +52,7 @@ public final class DependencyResolver {
* *
* @since v1-alpha1 * @since v1-alpha1
*/ */
Set<DependencyVector> vectors = new HashSet<>(); @NotNull Set<DependencyVector> vectors = new HashSet<>();
/**
* List of identifiers of already resolved vectors.
*
* @since v1-alpha4
*/
Set<@NotNull String> vectorsResolved = new HashSet<>();
/** /**
* Contains whether the current {@link DependencyVector} list has been resolved successfully. * Contains whether the current {@link DependencyVector} list has been resolved successfully.
@ -87,7 +81,7 @@ public final class DependencyResolver {
* @return itself * @return itself
* @since v1-alpha1 * @since v1-alpha1
*/ */
public synchronized DependencyResolver addVector(@NotNull DependencyVector vector) { public synchronized @NotNull DependencyResolver addVector(@NotNull DependencyVector vector) {
try { try {
vectors.add(vector); vectors.add(vector);
} catch (IllegalArgumentException ignored) {} } catch (IllegalArgumentException ignored) {}
@ -102,7 +96,7 @@ public final class DependencyResolver {
* @return itself * @return itself
* @since v1-alpha1 * @since v1-alpha1
*/ */
public DependencyResolver addVectors(@NotNull DependencyVector[] vectors) { public @NotNull DependencyResolver addVectors(@NotNull DependencyVector[] vectors) {
return addVectors(Arrays.stream(vectors).toList()); return addVectors(Arrays.stream(vectors).toList());
} }
@ -113,7 +107,7 @@ public final class DependencyResolver {
* @return itself * @return itself
* @since v1-alpha1 * @since v1-alpha1
*/ */
public DependencyResolver addVectors(@NotNull Collection<? extends @NotNull DependencyVector> vectors) { public @NotNull DependencyResolver addVectors(@NotNull Collection<? extends @NotNull DependencyVector> vectors) {
for (DependencyVector vector : vectors) // thread-safety for (DependencyVector vector : vectors) // thread-safety
addVector(vector); addVector(vector);
return this; return this;
@ -128,18 +122,14 @@ public final class DependencyResolver {
* @throws UnmetDependenciesException when dependencies are unmet * @throws UnmetDependenciesException when dependencies are unmet
* @since v1-alpha1 * @since v1-alpha1
*/ */
public synchronized DependencyResolver resolve() throws IllegalStateException, UnmetDependenciesException { public synchronized @NotNull DependencyResolver resolve() throws IllegalStateException, UnmetDependenciesException {
Map<@NotNull DependencyVector, @NotNull String> unmetDependencies = new HashMap<>(); Map<@NotNull DependencyVector, @NotNull String> unmetDependencies = new HashMap<>();
List<@NotNull String> output; List<@NotNull String> output;
for (DependencyVector vector : vectors) { for (DependencyVector vector : vectors) {
if (!vectorsResolved.contains(vector.getIdentifier())) { output = resolveVector(vector, new LinkedHashSet<>());
output = resolveVector(vector);
for (String item : output) for (String item : output)
unmetDependencies.put(vector, item); unmetDependencies.put(vector, item);
vectorsResolved.add(vector.getIdentifier());
}
} }
if (!unmetDependencies.isEmpty()) if (!unmetDependencies.isEmpty())
@ -158,10 +148,10 @@ public final class DependencyResolver {
* @throws Exception when some unknown error occurs * @throws Exception when some unknown error occurs
* @since v1-alpha4 * @since v1-alpha4
*/ */
private @NotNull List<@NotNull String> resolveVector(@NotNull DependencyVector vector) throws IllegalStateException { private @NotNull List<@NotNull String> resolveVector(@NotNull DependencyVector vector, @NotNull LinkedHashSet<@NotNull String> vectorsDependencyStack) throws IllegalStateException {
List<@NotNull String> unmetDependencies = new ArrayList<>(); List<@NotNull String> unmetDependencies = new ArrayList<>();
// provides vectorsDependencyStack.add(vector.getIdentifier());
// 0 = identifier // 0 = identifier
// 1 = version equal // 1 = version equal
@ -250,20 +240,39 @@ public final class DependencyResolver {
} }
} }
// Resolve vector // Get vector with same identifier
DependencyVector dependencyResolved = getMatchingVector(identifier.toString()); DependencyVector vectorDependency = getMatchingVector(identifier.toString());
if (dependencyResolved == null) { if (vectorDependency == null) {
unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Not found"); unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Not found");
continue; continue;
} }
// Resolve vector
if (vectorsDependencyStack.contains(vectorDependency.getIdentifier())) {
StringBuilder cycle = new StringBuilder();
for (String component : vectorsDependencyStack) {
if (!cycle.isEmpty())
cycle.append(" -> ");
cycle.append(component);
}
cycle
.append(" -> ")
.append(identifier);
throw new DependencyCycleException("Dependency cycle detected: " + cycle);
} else
resolveVector(vectorDependency, vectorsDependencyStack);
VersioningSystem versioningSystemResolved; VersioningSystem versioningSystemResolved;
// Get resolved versioning system // Get resolved versioning system
try { try {
versioningSystemResolved = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(dependencyResolved.getVersion()); versioningSystemResolved = vectorDependency.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(vectorDependency.getVersion());
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + vectorDependency.getVersioningSystem().getName(), exception);
break; break;
} }
@ -273,9 +282,9 @@ public final class DependencyResolver {
// Get expected VersioningSystem // Get expected VersioningSystem
try { try {
versioningSystemEquals = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionEqual.toString()); versioningSystemEquals = vectorDependency.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionEqual.toString());
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + vectorDependency.getVersioningSystem().getName(), exception);
break; break;
} }
@ -289,17 +298,17 @@ public final class DependencyResolver {
if (!versionSmaller.isEmpty()) if (!versionSmaller.isEmpty())
// Get expected VersioningSystem // Get expected VersioningSystem
try { try {
versioningSystemSmaller = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionSmaller.toString()); versioningSystemSmaller = vectorDependency.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionSmaller.toString());
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + vectorDependency.getVersioningSystem().getName(), exception);
break; break;
} }
if (!versionBigger.isEmpty()) if (!versionBigger.isEmpty())
// Get expected VersioningSystem // Get expected VersioningSystem
try { try {
versioningSystemBigger = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionBigger.toString()); versioningSystemBigger = vectorDependency.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionBigger.toString());
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + vectorDependency.getVersioningSystem().getName(), exception);
break; break;
} }
@ -318,6 +327,7 @@ public final class DependencyResolver {
} }
} }
vectorsDependencyStack.remove(vector.getIdentifier());
return unmetDependencies; return unmetDependencies;
} }

View file

@ -19,6 +19,7 @@
package de.staropensource.sosengine.base.srctests.utility; package de.staropensource.sosengine.base.srctests.utility;
import de.staropensource.sosengine.base.exception.dependency.DependencyCycleException;
import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException; import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException;
import de.staropensource.sosengine.base.implementation.versioning.OneNumberVersioningSystem; import de.staropensource.sosengine.base.implementation.versioning.OneNumberVersioningSystem;
import de.staropensource.sosengine.base.srctests.TestBase; import de.staropensource.sosengine.base.srctests.TestBase;
@ -26,6 +27,7 @@ import de.staropensource.sosengine.base.type.DependencyVector;
import de.staropensource.sosengine.base.utility.DependencyResolver; import de.staropensource.sosengine.base.utility.DependencyResolver;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
@ -165,6 +167,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 5 // Vector 5
dependencies = new HashSet<>(dependencies);
dependencies.add("test4>998<1000"); dependencies.add("test4>998<1000");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -237,6 +240,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 6p // Vector 6p
dependencies = new HashSet<>(dependencies);
dependencies.add("test5>998<1000"); dependencies.add("test5>998<1000");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -247,6 +251,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 7 // Vector 7
dependencies = new HashSet<>(dependencies);
dependencies.add("test6<7900"); dependencies.add("test6<7900");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -258,8 +263,8 @@ class DependencyResolverTest extends TestBase {
); );
} }
default -> { default -> {
getLogger().error("layers=" + layers + " is unimplemented"); getLogger().error("layers=" + layers + " is unimplemented in testResolve()");
throw new IllegalStateException("layers=" + layers + " is unimplemented"); throw new IllegalStateException("layers=" + layers + " is unimplemented in testResolve()");
} }
} }
@ -267,11 +272,14 @@ class DependencyResolverTest extends TestBase {
try { try {
resolver.resolve(); resolver.resolve();
} catch (UnmetDependenciesException exception) { } catch (UnmetDependenciesException exception) {
getLogger().error("Dependency resolution failed:"); getLogger().error("Dependency resolution failed in testResolve(layers=" + layers + "):");
for (DependencyVector vector : exception.getUnmetDependencies().keySet()) for (DependencyVector vector : exception.getUnmetDependencies().keySet())
getLogger().error("-> " + vector.getIdentifier() + "=" + vector.getVersion() + ": " + exception.getUnmetDependencies().get(vector)); getLogger().error("-> " + vector.getIdentifier() + "=" + vector.getVersion() + ": " + exception.getUnmetDependencies().get(vector));
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution failed (layers=" + layers + "): See logs"); assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution failed in testResolve(layers=" + layers + "): See logs");
} catch (DependencyCycleException exception) {
getLogger().error("Dependency resolution failed because of circular dependencies in testResolve(layers=" + layers + "): " + exception.getMessage());
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution failed in testResolve(layers=" + layers + "): See logs");
} }
} }
@ -321,6 +329,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 3 // Vector 3
dependencies = new HashSet<>(dependencies);
dependencies.add("test2>600<650"); dependencies.add("test2>600<650");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -351,8 +360,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 2 // Vector 2
dependencies = new HashSet<>(); dependencies = new HashSet<>(dependencies);
dependencies.add("testNULL");
dependencies.add("test1>999"); dependencies.add("test1>999");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -383,6 +391,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 5 // Vector 5
dependencies = new HashSet<>(dependencies);
dependencies.add("test4<998>990"); dependencies.add("test4<998>990");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -455,6 +464,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 6 // Vector 6
dependencies = new HashSet<>(dependencies);
dependencies.add("test5>998<100"); dependencies.add("test5>998<100");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -465,6 +475,7 @@ class DependencyResolverTest extends TestBase {
.build() .build()
); );
// Vector 7 // Vector 7
dependencies = new HashSet<>(dependencies);
dependencies.add("test6>7900"); dependencies.add("test6>7900");
vectors.add( vectors.add(
new DependencyVector.Builder() new DependencyVector.Builder()
@ -476,8 +487,8 @@ class DependencyResolverTest extends TestBase {
); );
} }
default -> { default -> {
getLogger().error("layers=" + layers + " is unimplemented"); getLogger().error("layers=" + layers + " is unimplemented in testResolveWithFailure()");
throw new IllegalStateException("layers=" + layers + " is unimplemented"); throw new IllegalStateException("layers=" + layers + " is unimplemented in testResolveWithFailure()");
} }
} }
@ -485,13 +496,134 @@ class DependencyResolverTest extends TestBase {
try { try {
resolver.resolve(); resolver.resolve();
} catch (UnmetDependenciesException exception) { } catch (UnmetDependenciesException exception) {
getLogger().error("Dependency resolution failed (great!):"); getLogger().error("Dependency resolution failed in testResolve(layers=" + layers + ") (great!):");
for (DependencyVector vector : exception.getUnmetDependencies().keySet()) for (DependencyVector vector : exception.getUnmetDependencies().keySet())
getLogger().error("-> " + vector.getIdentifier() + "=" + vector.getVersion() + ": " + exception.getUnmetDependencies().get(vector)); getLogger().error("-> " + vector.getIdentifier() + "=" + vector.getVersion() + ": " + exception.getUnmetDependencies().get(vector));
return; return;
} catch (DependencyCycleException exception) {
getLogger().error("Dependency resolution failed because of circular dependencies in testResolveWithFailure(layers=" + layers + "): " + exception.getMessage());
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution failed testResolveWithFailure(layers=" + layers + "): See logs");
} }
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution succeeded (layers=" + layers + ")"); assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution succeeded testResolveWithFailure(layers=" + layers + ")");
}
@Test
@DisplayName("resolve (dependency cycle)")
void testResolveDependencyCycle() {
if (isRestricted()) return;
getLogger().testCall("testResolveDependencyCycle");
DependencyResolver resolver = new DependencyResolver();
Set<@NotNull DependencyVector> vectors = new HashSet<>();
Set<@NotNull String> dependencies = new HashSet<>();
// Vector 0
dependencies.add("test7=110");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test0")
.setVersion("49")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 1
dependencies = new HashSet<>();
dependencies.add("test0");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test1")
.setVersion("999")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 2
dependencies = new HashSet<>();
dependencies.add("test1");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test2")
.setVersion("666")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 3
dependencies = new HashSet<>();
dependencies.add("test2<700");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test3")
.setVersion("666")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 4
dependencies = new HashSet<>();
dependencies.add("test3=666");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test4")
.setVersion("49")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 5
dependencies = new HashSet<>();
dependencies.add("test4<50");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test5")
.setVersion("999")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 6
dependencies = new HashSet<>();
dependencies.add("test5>990<1000");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test6")
.setVersion("6978")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
// Vector 7
dependencies = new HashSet<>();
dependencies.add("test6<7900");
vectors.add(
new DependencyVector.Builder()
.setIdentifier("test7")
.setVersion("110")
.setVersioningSystem(OneNumberVersioningSystem.class)
.setDependencies(dependencies)
.build()
);
resolver.addVectors(vectors);
try {
resolver.resolve();
} catch (UnmetDependenciesException exception) {
getLogger().error("Dependency resolution failed in testResolveDependencyCycle():");
for (DependencyVector vector : exception.getUnmetDependencies().keySet())
getLogger().error("-> " + vector.getIdentifier() + "=" + vector.getVersion() + ": " + exception.getUnmetDependencies().get(vector));
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution failed in testResolveDependencyCycle(): See logs");
} catch (DependencyCycleException exception) {
getLogger().error("Dependency resolution failed because of circular dependencies in testResolveDependencyCycle() (great!):");
getLogger().error(exception.getMessage());
return;
}
assertEquals("Please ignore this, this just exists to trigger an error", "", "Dependency resolution succeeded in testResolveDependencyCycle()");
} }
} }