From 48ebc7d4f9edc914f97aa120180932aaf8febb62 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Thu, 21 Aug 2025 19:21:01 +0530 Subject: [PATCH 1/2] Add purl-spec submodule for test data Signed-off-by: Keshav Priyadarshi --- .gitmodules | 3 +++ src/test/resources/purl-spec | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 src/test/resources/purl-spec diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..947dcc8c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/test/resources/purl-spec"] + path = src/test/resources/purl-spec + url = https://github.com/package-url/purl-spec diff --git a/src/test/resources/purl-spec b/src/test/resources/purl-spec new file mode 160000 index 00000000..414fef48 --- /dev/null +++ b/src/test/resources/purl-spec @@ -0,0 +1 @@ +Subproject commit 414fef487025046691af67f70dfa8677139df92d From eff2bd2bc939f32aec5eb8ce9469d3fb6a9df620 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Thu, 21 Aug 2025 20:16:50 +0530 Subject: [PATCH 2/2] Run tests using reference data from purl-spec Signed-off-by: Keshav Priyadarshi --- .github/workflows/maven.yml | 2 + .gitignore | 4 + pom.xml | 6 + .../github/packageurl/PurlSpecRefTest.java | 216 ++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 src/test/java/com/github/packageurl/PurlSpecRefTest.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index b52e0006..3242e9c2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -17,6 +17,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.2 + with: + submodules: true - name: Set up JDK 8 and ${{ matrix.java-version }} uses: actions/setup-java@v4 diff --git a/.gitignore b/.gitignore index 17635482..04542fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ buildNumber.properties .idea *.iml ########################################################################## + +#### ignore VSCODE config +.vscode/ +########################################################################## \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8923361a..b2c6ce19 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,7 @@ 1.37 20250107 5.13.3 + 2.19.2 1.4.0 @@ -200,6 +201,11 @@ junit-jupiter-params test + + com.fasterxml.jackson.core + jackson-databind + ${lib.jackson-databind.version} + diff --git a/src/test/java/com/github/packageurl/PurlSpecRefTest.java b/src/test/java/com/github/packageurl/PurlSpecRefTest.java new file mode 100644 index 00000000..e27bcb95 --- /dev/null +++ b/src/test/java/com/github/packageurl/PurlSpecRefTest.java @@ -0,0 +1,216 @@ +/* + * MIT License + * + * 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. + * + * SPDX-License-Identifier: MIT + * Copyright (c) AboutCode, and contributors. All Rights Reserved. + */ + +package com.github.packageurl; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.DeserializationContext; + +import java.util.Map; + +public class PurlSpecRefTest { + + public static class TestSuite { + @JsonProperty("$schema") + public String schema; + public List tests; + } + + public static class TestCase { + public String description; + public String test_group; + public String test_type; + + public PurlOrComponent input; + public PurlOrComponent expected_output; + + public boolean expected_failure; + public String expected_failure_reason; + + public TestCase() { + } + } + + @JsonDeserialize(using = PurlOrComponentDeserializer.class) + public static class PurlOrComponent { + public String purl; + public PurlComponents components; + } + + public static class PurlComponents { + public String type; + public String namespace; + public String name; + public String version; + public Map qualifiers; + public String subpath; + } + + public static class PurlOrComponentDeserializer extends JsonDeserializer { + @Override + public PurlOrComponent deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + ObjectCodec codec = p.getCodec(); + JsonNode node = codec.readTree(p); + + PurlOrComponent value = new PurlOrComponent(); + + if (node.isTextual()) { + value.purl = node.asText(); + } else if (node.isObject()) { + value.components = codec.treeToValue(node, PurlComponents.class); + } + + return value; + } + } + + static Stream collectTestCases() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + URL dirURL = PurlSpecRefTest.class.getClassLoader().getResource("purl-spec/tests/types/"); + if (dirURL == null) { + throw new RuntimeException("Resource directory 'purl-spec/tests/types/' not found."); + } + + Path testDataPath = Paths.get(dirURL.toURI()); + List jsonFiles = Files.list(testDataPath) + .filter(p -> p.toString().endsWith(".json")) + .collect(Collectors.toList()); + + Stream.Builder builder = Stream.builder(); + + for (Path jsonFile : jsonFiles) { + try (InputStream is = Files.newInputStream(jsonFile)) { + TestSuite suite = mapper.readValue(is, TestSuite.class); + suite.tests.forEach(builder::add); + } + } + + return builder.build(); + } + + void runRoundtripTest(TestCase testCase) throws Exception { + String result; + try { + result = new PackageURL(testCase.input.purl).canonicalize().toString(); + } catch (Exception e) { + assertTrue(testCase.expected_failure, "Unexpected failure: " + e.getMessage()); + return; + } + assertFalse(testCase.expected_failure, "Expected failure but parsing succeeded"); + + assertEquals(result, testCase.expected_output.purl); + + } + + void runBuildTest(TestCase testCase) throws Exception { + PurlComponents input = testCase.input.components; + String result; + try { + result = new PackageURL(input.type, input.namespace, input.name, input.version, input.qualifiers, + input.subpath).canonicalize().toString(); + } catch (Exception e) { + assertTrue(testCase.expected_failure, "Unexpected failure: " + e.getMessage()); + return; + } + + assertFalse(testCase.expected_failure, "Expected failure but build succeeded"); + assertEquals(result, testCase.expected_output.purl); + } + + void runParseTest(TestCase testCase) throws Exception { + PackageURL result; + try { + result = new PackageURL(testCase.input.purl); + } catch (Exception e) { + assertTrue(testCase.expected_failure, "Unexpected failure: " + e.getMessage()); + return; + } + assertFalse(testCase.expected_failure, "Expected failure but parsing succeeded"); + + PurlComponents expected = testCase.expected_output.components; + result.canonicalize(); + + assertEquals(expected.type, result.getType(), "Type mismatch"); + assertEquals(expected.namespace, result.getNamespace(), "Namespace mismatch"); + assertEquals(expected.name, result.getName(), "Name mismatch"); + assertEquals(expected.version, result.getVersion(), "Version mismatch"); + assertEquals(expected.subpath, result.getSubpath(), "Subpath mismatch"); + + assertEquals( + expected.qualifiers != null ? expected.qualifiers : Collections.emptyMap(), + result.getQualifiers(), + "Qualifiers mismatch"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("collectTestCases") + void runTest(TestCase testCase) throws Exception { + switch (testCase.test_type) { + case "roundtrip": + runRoundtripTest(testCase); + break; + case "build": + runBuildTest(testCase); + break; + case "parse": + runParseTest(testCase); + break; + default: + throw new IllegalArgumentException("Unknown test_type: " + testCase.test_type); + } + + } + +}