diff --git a/build.gradle b/build.gradle index d2024f1e6..94f2e477e 100644 --- a/build.gradle +++ b/build.gradle @@ -346,6 +346,9 @@ testing { suites { test { useJUnitJupiter() + dependencies { + implementation "org.assertj:assertj-core:3.24.2" + } } integrationTest(JvmTestSuite) { diff --git a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java index 640a184df..4db3ba1cc 100644 --- a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java +++ b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java @@ -406,8 +406,9 @@ private Artifact toArtifact(MavenCoordinate coord) { } private static ArtifactInfo toArtifactInfo(Artifact artifact) { + // TODO: get attributes from artifact descriptor?? MavenCoordinate coord = new MavenCoordinate(artifact.getGroupId(), artifact.getArtifactId(), - artifact.getVersion(), artifact.getClassifier(), artifact.getExtension()); + artifact.getVersion(), artifact.getClassifier(), artifact.getExtension(), DependencyAttributes.DEFAULT); return new ArtifactInfo(coord, artifact.getFile().toPath()); } diff --git a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java new file mode 100644 index 000000000..4dd1a769f --- /dev/null +++ b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java @@ -0,0 +1,66 @@ +package dev.jbang.dependencies; + +import java.util.*; + +import dev.jbang.util.AttributeParser; + +public class DependencyAttributes { + + /** default attributes */ + public static final DependencyAttributes DEFAULT = new DependencyAttributes( + AttributeParser.parseAttributeList("scope=build,run", "scope")); + + private final Map> attributes; + + // Java 8 Set.of() is sad :) + static Set defaultScopes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("build", "run"))); + Set scopes; + + public DependencyAttributes(Map> attributes) { + this.attributes = attributes; + } + + // lazy computed; dont store hashset unless truly needed + public boolean includeInScope(String scope) { + if (scopes == null) { + if (attributes.containsKey("scope")) { + scopes = new HashSet<>(attributes.get("scope")); + } else { + scopes = defaultScopes; + } + } + return scopes.contains(scope); + } + + /** + * is there something configured? even if default ? + * + * @return + */ + public boolean isDefault() { + return !attributes.isEmpty(); + } + + public String toStringFormat() { + return AttributeParser.toStringRep(attributes, "scope"); + } + + @Override + public String toString() { + return toStringFormat(); + } + + @Override + public boolean equals(Object o) { + // TODO: this wont make g:a:v and g:a:v{build,run} equal ...even if they are... + if (o == null || getClass() != o.getClass()) + return false; + DependencyAttributes that = (DependencyAttributes) o; + return Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hashCode(attributes); + } +} diff --git a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java index 4184bf901..dfd0e0853 100644 --- a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java +++ b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java @@ -7,6 +7,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import dev.jbang.util.AttributeParser; + /** * * MavenCoordinate should not implement euqals as it is not meaningfull to use @@ -25,15 +27,18 @@ public class MavenCoordinate { private final String version; private final String classifier; private final String type; + private final DependencyAttributes attributes; public static final String DUMMY_GROUP = "group"; public static final String DEFAULT_VERSION = "999-SNAPSHOT"; + public static final String SCOPE_ATTR = "scope"; private static final Pattern gavPattern = Pattern.compile( - "^(?[^:]*):(?[^:]*)(:(?[^:@]*))?(:(?[^@]*))?(@(?.*))?$"); + "^(?[^:]*):(?[^:]*)(:(?[^:@{]*))?(:(?[^@{]*))?(@(?.[^{]*))?(\\{(?.*)})?$"); private static final Pattern canonicalPattern = Pattern.compile( "^(?[^:]*):(?[^:]*)((:(?.*)(:(?[^@]*))?)?:(?[^:@]*))?$"); + private String managementKey; public String getGroupId() { @@ -73,17 +78,21 @@ public String getManagementKey() { return managementKey; } + /** + * Converts a canonical string to a MavenCoordinate. + */ + @Deprecated public static MavenCoordinate fromCanonicalString(String depId) { return parse(depId, canonicalPattern); } private static MavenCoordinate parse(String depId, Pattern pattern) { Matcher gav = pattern.matcher(depId); - gav.find(); + // gav.find(); if (!gav.matches()) { throw new IllegalStateException(String.format( - "[ERROR] Invalid dependency locator: '%s'. Expected format is groupId:artifactId:version[:classifier][@type]", + "[ERROR] Invalid dependency locator: '%s'. Expected format is groupId:artifactId:version[:classifier][@type][{attrlist}]", depId)); } @@ -92,26 +101,30 @@ private static MavenCoordinate parse(String depId, Pattern pattern) { String version = DependencyUtil.formatVersion(gav.group("version")); String classifier = gav.group("classifier"); String type = Optional.ofNullable(gav.group("type")).orElse("jar"); + String propString = gav.group("properties"); - return new MavenCoordinate(groupId, artifactId, version, classifier, type); + DependencyAttributes da = new DependencyAttributes( + AttributeParser.parseAttributeList(propString, "scope")); + return new MavenCoordinate(groupId, artifactId, version, classifier, type, da); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version) { - this(groupId, artifactId, version, null, null); + this(groupId, artifactId, version, null, null, DependencyAttributes.DEFAULT); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, - @Nullable String classifier, @Nullable String type) { + @Nullable String classifier, @Nullable String type, DependencyAttributes attributes) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; this.classifier = classifier != null && classifier.isEmpty() ? null : classifier; this.type = type; + this.attributes = attributes; } public MavenCoordinate withVersion() { return version != null ? this - : new MavenCoordinate(groupId, artifactId, DEFAULT_VERSION, classifier, type); + : new MavenCoordinate(groupId, artifactId, DEFAULT_VERSION, classifier, type, attributes); } /** @@ -163,6 +176,12 @@ public String toString() { ", version='" + version + '\'' + ", classifier='" + classifier + '\'' + ", type='" + type + '\'' + + ", attributes='" + attributes + '\'' + '}'; } + + public DependencyAttributes getAttributes() { + return attributes; + } + } diff --git a/src/main/java/dev/jbang/util/AttributeParser.java b/src/main/java/dev/jbang/util/AttributeParser.java new file mode 100644 index 000000000..1f86202c6 --- /dev/null +++ b/src/main/java/dev/jbang/util/AttributeParser.java @@ -0,0 +1,146 @@ +package dev.jbang.util; + +import java.util.*; + +//handle {} similar to how Asciidoc(tor) interpret attribute lists: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/ +//Derived from https://github.com/yupiik/tools-maven-plugin/blob/d1e8b97ebcae032684a0fafac426bcc25819089f/asciidoc-java/src/main/java/io/yupiik/asciidoc/parser/Parser.java#L1634 + +public class AttributeParser { + + public static Map> parseAttributeList(String input, String defaultKey) { + Map> result = new LinkedHashMap<>(); + List unnamed = new ArrayList<>(); + + for (String token : tokenize(input)) { + if (token.isEmpty()) + continue; + + if (token.contains("=")) { + String[] kv = token.split("=", 2); + String key = kv[0].trim(); + String value = unquote(kv[1].trim()); + + if (key.equals(defaultKey)) { + unnamed.add(value); + } else { + result.put(key, Collections.singletonList(value)); + } + } else if (token.contains("%")) { + String[] parts = token.split("%"); + int startIdx = 0; + + if (!parts[0].isEmpty()) { + unnamed.add(unquote(parts[0])); + startIdx = 1; + } + + for (int i = startIdx; i < parts.length; i++) { + if (!parts[i].isEmpty()) { + result.put(parts[i], Collections.singletonList("true")); + } + } + } else { + unnamed.add(unquote(token)); + } + } + + if (!unnamed.isEmpty()) { + result.put(defaultKey, unnamed); + } + + return result; + } + + private static List tokenize(String input) { + if (input == null) + return Collections.emptyList(); + List tokens = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inSingleQuote = false, inDoubleQuote = false; + + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + if (c == ',' && !inSingleQuote && !inDoubleQuote) { + tokens.add(current.toString().trim()); + current.setLength(0); + } else { + if (c == '"' && !inSingleQuote) { + if (i == 0 || input.charAt(i - 1) != '\\') { + inDoubleQuote = !inDoubleQuote; + } + } else if (c == '\'' && !inDoubleQuote) { + if (i == 0 || input.charAt(i - 1) != '\\') { + inSingleQuote = !inSingleQuote; + } + } + current.append(c); + } + } + + if (current.length() > 0) { + tokens.add(current.toString().trim()); + } + + return tokens; + } + + private static String unquote(String s) { + if (s.length() < 2) + return s; + + char first = s.charAt(0); + char last = s.charAt(s.length() - 1); + + if ((first == '"' || first == '\'') && last != first) { + return s; + } + + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + String inner = s.substring(1, s.length() - 1); + return first == '"' ? inner.replace("\\\"", "\"") : inner.replace("\\'", "'"); + } + + return s; + } + + private static String quoteValue(String value) { + boolean needsQuote = value.contains(",") || value.contains(" ") || value.contains("\"") || value.contains("'"); + if (!needsQuote) + return value; + + boolean useDouble = !value.contains("\"") || value.contains("'"); + String escaped = value.replace(useDouble ? "\"" : "'", useDouble ? "\\\"" : "\\'"); + return useDouble ? "\"" + escaped + "\"" : "'" + escaped + "'"; + } + + public static String toStringRep(Map> attributes, String defaultKey) { + List parts = new ArrayList<>(); + + // Positional values first + List positional = attributes.get(defaultKey); + if (positional != null) { + for (String value : positional) { + parts.add(quoteValue(value)); + } + } + + // Other keys + for (Map.Entry> entry : attributes.entrySet()) { + String key = entry.getKey(); + if (key.equals(defaultKey)) + continue; + + List values = entry.getValue(); + for (String value : values) { + if ("true".equals(value)) { + parts.add("%" + key); + } else { + parts.add(key + "=" + quoteValue(value)); + } + } + } + + return String.join(",", parts); + } +} diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index 5709b3976..e70689dee 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -2,16 +2,7 @@ import static dev.jbang.dependencies.DependencyUtil.toMavenRepo; import static dev.jbang.util.JavaUtil.defaultJdkManager; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayWithSize; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; import java.io.File; import java.io.IOException; @@ -36,7 +27,7 @@ class DependencyResolverTest extends BaseTest { @Test void testFormatVersion() { - assertEquals("[1.0,)", DependencyUtil.formatVersion("1.0+")); + assertThat(DependencyUtil.formatVersion("1.0+")).isEqualTo("[1.0,)"); } @BeforeEach @@ -47,37 +38,121 @@ void clearCache() { @Test void testdepIdToArtifact() { MavenCoordinate artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("0.6.0.20150202", artifact.getVersion()); - assertEquals("redhat", artifact.getClassifier()); - assertEquals("doc", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isEqualTo("redhat"); + assertThat(artifact.getType()).isEqualTo("doc"); artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("0.6.0.20150202", artifact.getVersion()); - assertNull(artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isNull(); + assertThat(artifact.getType()).isEqualTo("jar"); artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6+"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("[0.6,)", artifact.getVersion()); - assertNull(artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("[0.6,)"); + assertThat(artifact.getClassifier()).isNull(); + assertThat(artifact.getType()).isEqualTo("jar"); - assertThrows(IllegalStateException.class, () -> MavenCoordinate.fromString("bla?f")); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> MavenCoordinate.fromString("bla?f")); + } + + @Test() + void testEqualsEmptyAttributes() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + assertThat(a1).as("mavencoordinate bad comparison of empty attribtues").isNotEqualTo(a2); + } + + @Test + void testEqualsGAVBehavior() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); + assertThat(a1).isNotEqualTo(a2); // these are equal in behavior - but not in "format" + } + + @Test + void testEqualsGAVFlippedBehavior() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{run,build}"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); + assertThat(a1).isNotEqualTo(a2); // these are equal in behavior - but not in "format" + } + + @Test + void testVariants() { + MavenCoordinate artifact = MavenCoordinate + .fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc{build,boot,run}"); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isEqualTo("redhat"); + assertThat(artifact.getType()).isEqualTo("doc"); + assertThat(artifact.getAttributes().includeInScope("boot")).isTrue(); + } + + @Test + void testScopes() { + + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + + } + + @Test + void testMultiScopes() { + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact.getAttributes().includeInScope("compile")).isFalse(); + assertThat(artifact.getAttributes().includeInScope("doesnotexist")).isFalse(); + } + + @Test + void testBadDependencyRequests() { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> MavenCoordinate.fromString("a.b:c:1.2{")); + } + + @Test + void testOtherProperties() { + + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + MavenCoordinate artifact2 = MavenCoordinate.fromString("a.b:c:1.2"); + + assertThat(artifact2) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact2.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact2.getAttributes().includeInScope("run")).isTrue(); } @Test void testdecodeEnv() { - assertThrows(IllegalStateException.class, () -> DependencyUtil.decodeEnv("{{wonka}}")); - assertEquals("wonka", DependencyUtil.decodeEnv("wonka")); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> DependencyUtil.decodeEnv("{{wonka}}")); + assertThat(DependencyUtil.decodeEnv("wonka")).isEqualTo("wonka"); environmentVariables.set("test.value", "wonka"); - assertEquals("wonka", DependencyUtil.decodeEnv("{{test.value}}")); + assertThat(DependencyUtil.decodeEnv("{{test.value}}")).isEqualTo("wonka"); } @Test @@ -90,11 +165,11 @@ void testdepIdWithPlaceHoldersToArtifact() { "com.example:my-native-library:1.0.0:${os.detected.jfxname}", p); MavenCoordinate artifact = MavenCoordinate.fromString(gav); - assertEquals("com.example", artifact.getGroupId()); - assertEquals("my-native-library", artifact.getArtifactId()); - assertEquals("1.0.0", artifact.getVersion()); - assertEquals(p.getProperty("os.detected.jfxname"), artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.example"); + assertThat(artifact.getArtifactId()).isEqualTo("my-native-library"); + assertThat(artifact.getVersion()).isEqualTo("1.0.0"); + assertThat(artifact.getClassifier()).isEqualTo(p.getProperty("os.detected.jfxname")); + assertThat(artifact.getType()).isEqualTo("jar"); } @Test @@ -106,14 +181,14 @@ void testResolveJavaFXWithAether() { List deps = Collections.singletonList( PropertiesValueResolver.replaceProperties("org.openjfx:javafx-base:18.0.2:${os.detected.jfxname}", p)); List artifacts = ArtifactResolver.Builder.create().build().resolve(deps); - assertEquals(1, artifacts.size()); + assertThat(artifacts.size()).isEqualTo(1); } @Test void testResolveDependenciesWithAether() { List deps = Arrays.asList("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2+"); List artifacts = ArtifactResolver.Builder.create().build().resolve(deps); - assertEquals(2, artifacts.size()); + assertThat(artifacts.size()).isEqualTo(2); } @Test @@ -124,14 +199,15 @@ void testResolveDependenciesAltRepo(@TempDir File altrepo) { .localFolder(altrepo.toPath()) .build() .resolve(deps); - assertEquals(2, artifacts.size()); - assertThat(altrepo.listFiles(), arrayWithSize(4)); + assertThat(artifacts.size()).isEqualTo(2); + assertThat(altrepo.listFiles()).hasSize(4); } @Test void testRedHatJBossRepos() { - assertEquals(toMavenRepo("jbossorg").getUrl(), "https://repository.jboss.org/nexus/content/groups/public/"); - assertEquals(toMavenRepo("redhat").getUrl(), "https://maven.repository.redhat.com/ga/"); + assertThat("https://repository.jboss.org/nexus/content/groups/public/") + .isEqualTo(toMavenRepo("jbossorg").getUrl()); + assertThat("https://maven.repository.redhat.com/ga/").isEqualTo(toMavenRepo("redhat").getUrl()); } @Test @@ -143,7 +219,7 @@ void testResolveDependencies() { true, false); // if returns 5 its because optional deps are included which they shouldn't - assertEquals(2, classpath.getClassPaths().size()); + assertThat(classpath.getClassPaths().size()).isEqualTo(2); } @Test @@ -162,7 +238,7 @@ void testResolveDependenciesNoDuplicates() { HashSet othercps = new HashSet<>(cps); - assertThat(cps, containsInAnyOrder(othercps.toArray())); + assertThat(cps).containsAll(othercps); } @Test @@ -177,7 +253,7 @@ void testResolveNativeDependencies() { false, true, false); - assertEquals(46, classpath.getClassPaths().size()); + assertThat(classpath.getClassPaths().size()).isEqualTo(46); } @Test @@ -196,11 +272,11 @@ protected boolean supportsModules(Jdk jdk) { List ma = cp.getAutoDectectedModuleArguments(defaultJdkManager().getOrInstallJdk(null)); - assertThat(ma, hasItem("--module-path")); + assertThat(ma).contains("--module-path"); - assertThat(ma, not(hasItem("docopt"))); + assertThat(ma).doesNotContain("docopt"); - assertThat(cp.getClassPath(), containsString("docopt")); + assertThat(cp.getClassPath()).contains("docopt"); } @Test @@ -211,7 +287,7 @@ void testImportPOM() { false, true, false); - assertEquals(62, classpath.getArtifacts().size()); + assertThat(classpath.getArtifacts().size()).isEqualTo(62); } @Test @@ -235,14 +311,14 @@ void testImportMultipleBoms() { .startsWith("io.vertx:vertx-core")) .findFirst(); - assertEquals("4.2.3", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("4.2.3"); coord = classpath.getArtifacts() .stream() .filter(ai -> ai.getCoordinate().toCanonicalForm().startsWith("org.slf4j:slf4j-simple:")) .findFirst(); - assertEquals("1.7.30", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("1.7.30"); coord = classpath.getArtifacts() .stream() @@ -251,7 +327,7 @@ void testImportMultipleBoms() { .startsWith("org.apache.camel:camel-vertx")) .findFirst(); - assertEquals(coord.get().getCoordinate().getVersion(), "3.9.0"); + assertThat("3.9.0").isEqualTo(coord.get().getCoordinate().getVersion()); deps = Arrays.asList( "org.apache.camel:camel-bom:3.9.0@pom", @@ -265,7 +341,7 @@ void testImportMultipleBoms() { .filter(ai -> ai.getCoordinate().toCanonicalForm().startsWith("io.vertx:vertx-core")) .findFirst(); - assertEquals("3.9.5", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("3.9.5"); } @Test @@ -276,12 +352,11 @@ void testResolveTestJar() { false, true, false); - assertThat(classpath.getArtifacts(), hasSize(7)); + assertThat(classpath.getArtifacts()).hasSize(7); ArtifactInfo ai = classpath.getArtifacts().get(0); - assertThat(ai.getCoordinate().toCanonicalForm(), - equalTo("org.infinispan:infinispan-commons:tests:jar:13.0.5.Final")); - assertThat(ai.getFile().toString(), - endsWith("infinispan-commons-13.0.5.Final-tests.jar")); + assertThat(ai.getCoordinate().toCanonicalForm()) + .isEqualTo("org.infinispan:infinispan-commons:tests:jar:13.0.5.Final"); + assertThat(ai.getFile().toString()).endsWith("infinispan-commons-13.0.5.Final-tests.jar"); } } diff --git a/src/test/java/dev/jbang/util/AttributeParserTest.java b/src/test/java/dev/jbang/util/AttributeParserTest.java new file mode 100644 index 000000000..567c38ade --- /dev/null +++ b/src/test/java/dev/jbang/util/AttributeParserTest.java @@ -0,0 +1,86 @@ +package dev.jbang.util; + +import static dev.jbang.util.AttributeParser.parseAttributeList; +import static java.util.List.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class AttributeParserTest { + + Map> parse(String str) { + return parse(str, ""); + } + + Map> parse(String str, String def) { + return parseAttributeList(str, def); + } + + @Test + public void testEmpty() { + assertThat(parse("")).isEmpty(); + } + + @Test + public void testParseBasic() { + assertThat(parse("basic")) + .isNotEmpty() + .contains(entry("", of("basic"))); + } + + @Test + public void testParseBoolean() { + assertThat(parse("%transitive")) + .isNotEmpty() + .contains(entry("transitive", of("true"))); + } + + @Test + public void testList() { + assertThat(parse("basic,run")) + .isNotEmpty() + .contains(entry("", of("basic", "run"))); + } + + @Test + public void testMultipleBooleans() { + assertThat(parse("%transitive%import")) + .isNotEmpty() + .contains(entry("transitive", of("true")), entry("import", of("true"))); + } + + @Test + public void testNamedAttributes() { + assertThat(parse("my=basic,run")) + .isNotEmpty() + .hasSize(2) + .contains(entry("my", of("basic")), entry("", of("run"))); + } + + @Test + public void testCombinedMultipleBooleans() { + assertThat(parse("basic,run,scope=herewego,%transitive%import", "scope")) + .isNotEmpty() + .hasSize(3) + .contains( + entry("transitive", of("true")), + entry("import", of("true")), + entry("scope", of("basic", "run", "herewego"))); + } + + @Test + public void testQuotes() { + assertThat(parse("'basic,run' ,scope=\"here 'we' \\\"go\\\" \",%transitive%import", "scope")) + .isNotEmpty() + .hasSize(3) + .contains( + entry("transitive", of("true")), + entry("import", of("true")), + entry("scope", of("basic,run", "here 'we' \"go\" "))); + } + +}