From 865f6306d48ae3e16348d58d2d06d243c390af9d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 14:13:02 +0200 Subject: [PATCH 01/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/discover_2.x_schemas.yml | 42 ++++++++++++++++++++++ .github/workflows/lint_2.x_schemas.yml | 28 +-------------- .github/workflows/test_2.x_js.yml | 17 +++++++-- 3 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/discover_2.x_schemas.yml diff --git a/.github/workflows/discover_2.x_schemas.yml b/.github/workflows/discover_2.x_schemas.yml new file mode 100644 index 00000000..ce1cfb25 --- /dev/null +++ b/.github/workflows/discover_2.x_schemas.yml @@ -0,0 +1,42 @@ + +name: CT CDX-2.x Lint + +on: + workflow_call: + outputs: + versions: + description: 'JSON array of discovered versions' + value: ${{ jobs.discover.outputs.versions }} + +# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token +permissions: {} + +jobs: + discover: + timeout-minutes: 5 + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.discover.outputs.versions }} + steps: + - name: Checkout repository + # see https://github.com/actions/checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Discover schema versions + id: discover + run: | + mapfile -d '' -t dirs < <( + find schema \ + -mindepth 1 -maxdepth 1 \ + -type d \ + -name '2.*' \ + -printf '%f\0' \ + | sort -z + ) + if [ ${#dirs[@]} -eq 0 ]; then + echo 'No schema/2.* directories found' >&2 + exit 1 + fi + printf 'versions=' >> "$GITHUB_OUTPUT" + printf '%s\n' "${dirs[@]}" | jq -R . | jq -c -s . >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/lint_2.x_schemas.yml b/.github/workflows/lint_2.x_schemas.yml index 78082d56..85e22792 100644 --- a/.github/workflows/lint_2.x_schemas.yml +++ b/.github/workflows/lint_2.x_schemas.yml @@ -18,33 +18,7 @@ env: jobs: discover-schema: - timeout-minutes: 5 - runs-on: ubuntu-latest - outputs: - versions: ${{ steps.discover.outputs.versions }} - steps: - - name: Checkout repository - # see https://github.com/actions/checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Discover schema versions - id: discover - run: | - mapfile -d '' -t dirs < <( - find schema \ - -mindepth 1 -maxdepth 1 \ - -type d \ - -name '2.*' \ - -printf '%f\0' \ - | sort -z - ) - if [ ${#dirs[@]} -eq 0 ]; then - echo 'No schema/2.* directories found' >&2 - exit 1 - fi - printf 'versions=' >> "$GITHUB_OUTPUT" - printf '%s\n' "${dirs[@]}" | jq -R . | jq -c -s . >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/discover_2.x_schemas.yml discover-linter: timeout-minutes: 5 runs-on: ubuntu-latest diff --git a/.github/workflows/test_2.x_js.yml b/.github/workflows/test_2.x_js.yml index 8c867af5..9138f7dc 100644 --- a/.github/workflows/test_2.x_js.yml +++ b/.github/workflows/test_2.x_js.yml @@ -19,10 +19,22 @@ defaults: # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token permissions: {} +env: + NODE_VERSION: '24.x' + jobs: + discover-schema: + uses: ./.github/workflows/discover_2.x_schemas.yml test_js: + needs: + - discover-schema timeout-minutes: 30 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + schema_version: ${{ fromJSON(needs.discover-schema.outputs.versions) }} + name: test ${{ matrix.schema_version }} steps: - name: Checkout # see https://github.com/actions/checkout @@ -33,7 +45,7 @@ jobs: # see https://github.com/actions/setup-node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '24.x' + node-version: ${{ env.NODE_VERSION }} package-manager-cache: false - name: Install Dependencies run: npm install @@ -47,4 +59,5 @@ jobs: && github.base_ref == 'master' ) }} - run: npm test + SCHEMA_VERSION: ${{ matrix.schema_version }} + run: npm run "test:v$SCHEMA_VERSION" From bb6f6f744c0f8e12a48382bc491b548d27a3c6e1 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 14:20:53 +0200 Subject: [PATCH 02/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_js.yml | 7 ++++--- .github/workflows/test_2.x_php.yml | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_2.x_js.yml b/.github/workflows/test_2.x_js.yml index 9138f7dc..741192c1 100644 --- a/.github/workflows/test_2.x_js.yml +++ b/.github/workflows/test_2.x_js.yml @@ -12,13 +12,14 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -defaults: - run: - working-directory: tools/src/test/js/schema-v2 # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token permissions: {} +defaults: + run: + working-directory: tools/src/test/js/schema-v2 + env: NODE_VERSION: '24.x' diff --git a/.github/workflows/test_2.x_php.yml b/.github/workflows/test_2.x_php.yml index 5a18f11e..bbcd5806 100644 --- a/.github/workflows/test_2.x_php.yml +++ b/.github/workflows/test_2.x_php.yml @@ -12,17 +12,28 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + +# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token +permissions: {} + defaults: run: working-directory: tools/src/test/php/schema-v2 -# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token -permissions: {} +env: + PHP_VERSION: "8.5" jobs: + discover-schema: + uses: ./.github/workflows/discover_2.x_schemas.yml test_php: timeout-minutes: 30 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + schema_version: ${{ fromJSON(needs.discover-schema.outputs.versions) }} + name: test ${{ matrix.schema_version }} steps: - name: Checkout # see https://github.com/actions/checkout @@ -33,9 +44,11 @@ jobs: # see https://github.com/shivammathur/setup-php uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: - php-version: "8.5" + php-version: ${{ env.PHP_VERSION }} tools: composer:v2 - name: Install Dependencies run: composer install - name: Run test - run: composer run test + env: + SCHEMA_VERSION: ${{ matrix.schema_version }} + run: composer run "test:v$SCHEMA_VERSION" From 4d8c93571af7e3fdbb7e58a2d936e3ff99e4227d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 14:23:30 +0200 Subject: [PATCH 03/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .../{discover_2.x_schemas.yml => discover_2.x_schema.yml} | 0 .github/workflows/lint_2.x_schemas.yml | 2 +- .github/workflows/test_2.x_js.yml | 2 +- .github/workflows/test_2.x_php.yml | 4 +++- 4 files changed, 5 insertions(+), 3 deletions(-) rename .github/workflows/{discover_2.x_schemas.yml => discover_2.x_schema.yml} (100%) diff --git a/.github/workflows/discover_2.x_schemas.yml b/.github/workflows/discover_2.x_schema.yml similarity index 100% rename from .github/workflows/discover_2.x_schemas.yml rename to .github/workflows/discover_2.x_schema.yml diff --git a/.github/workflows/lint_2.x_schemas.yml b/.github/workflows/lint_2.x_schemas.yml index 85e22792..ca59db5a 100644 --- a/.github/workflows/lint_2.x_schemas.yml +++ b/.github/workflows/lint_2.x_schemas.yml @@ -18,7 +18,7 @@ env: jobs: discover-schema: - uses: ./.github/workflows/discover_2.x_schemas.yml + uses: ./.github/workflows/discover_2.x_schema.yml discover-linter: timeout-minutes: 5 runs-on: ubuntu-latest diff --git a/.github/workflows/test_2.x_js.yml b/.github/workflows/test_2.x_js.yml index 741192c1..900cf429 100644 --- a/.github/workflows/test_2.x_js.yml +++ b/.github/workflows/test_2.x_js.yml @@ -25,7 +25,7 @@ env: jobs: discover-schema: - uses: ./.github/workflows/discover_2.x_schemas.yml + uses: ./.github/workflows/discover_2.x_schema.yml test_js: needs: - discover-schema diff --git a/.github/workflows/test_2.x_php.yml b/.github/workflows/test_2.x_php.yml index bbcd5806..2206ce7e 100644 --- a/.github/workflows/test_2.x_php.yml +++ b/.github/workflows/test_2.x_php.yml @@ -25,8 +25,10 @@ env: jobs: discover-schema: - uses: ./.github/workflows/discover_2.x_schemas.yml + uses: ./.github/workflows/discover_2.x_schema.yml test_php: + needs: + - discover-schema timeout-minutes: 30 runs-on: ubuntu-latest strategy: From d866ff4ff491cec178580374db1f7f2d491a7c20 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 14:24:17 +0200 Subject: [PATCH 04/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/discover_2.x_schema.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/discover_2.x_schema.yml b/.github/workflows/discover_2.x_schema.yml index ce1cfb25..c2f14731 100644 --- a/.github/workflows/discover_2.x_schema.yml +++ b/.github/workflows/discover_2.x_schema.yml @@ -1,5 +1,5 @@ -name: CT CDX-2.x Lint +name: Discover CDX-2.x Schema on: workflow_call: From a40412f86680c34c13feb935b535783864712ac2 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 14:32:21 +0200 Subject: [PATCH 05/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 26 +++++++++++++++++++++----- tools/pom.xml | 8 +++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 29d47760..464924fc 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -10,16 +10,30 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token +permissions: {} + defaults: run: working-directory: tools -# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token -permissions: {} +env: + JAVA_VERSION: '8' + JAVA_DISTRIBUTION: 'zulu' jobs: + discover-schema: + uses: ./.github/workflows/discover_2.x_schema.yml test_java: + needs: + - discover-schema runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + schema_version: ${{ fromJSON(needs.discover-schema.outputs.versions) }} + name: test ${{ matrix.schema_version }} steps: - name: Checkout # see https://github.com/actions/checkout @@ -30,8 +44,10 @@ jobs: # see https://github.com/actions/setup-java uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: - java-version: '8' - distribution: 'zulu' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} java-package: jdk - name: test with Maven - run: mvn clean test -Pschema-v2-tests + env: + SCHEMA_VERSION: ${{ matrix.schema_version }} + run: mvn clean test -P"schema-v${SCHEMA_VERSION}-tests" diff --git a/tools/pom.xml b/tools/pom.xml index e33c4e36..e74f3a9b 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -205,5 +205,11 @@ schema-v2 + + schema-v2.0-tests + + schema-v2.0 + + - \ No newline at end of file + From ae7b9ef56706ad585a12fc0cb1adebde05034967 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 15:00:49 +0200 Subject: [PATCH 06/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 2 +- tools/pom.xml | 4 + .../AbstractJsonSchemaVerificationTest.java | 168 ++++++++++++++++++ .../schema/v2/BaseSchemaVerificationTest.java | 40 ----- .../schema/v2/JsonSchemaVerificationTest.java | 150 ---------------- .../v2/V20JsonSchemaVerificationTest.java | 25 +++ 6 files changed, 198 insertions(+), 191 deletions(-) create mode 100644 tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java delete mode 100644 tools/src/test/java/org/cyclonedx/schema/v2/BaseSchemaVerificationTest.java delete mode 100644 tools/src/test/java/org/cyclonedx/schema/v2/JsonSchemaVerificationTest.java create mode 100644 tools/src/test/java/org/cyclonedx/schema/v2/V20JsonSchemaVerificationTest.java diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 464924fc..0f28a5b6 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -50,4 +50,4 @@ jobs: - name: test with Maven env: SCHEMA_VERSION: ${{ matrix.schema_version }} - run: mvn clean test -P"schema-v${SCHEMA_VERSION}-tests" + run: mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" diff --git a/tools/pom.xml b/tools/pom.xml index e74f3a9b..c0d1315d 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -46,6 +46,9 @@ + + false 2.10.4 3.0.1 @@ -179,6 +182,7 @@ 3.5.2 ${surefire.includeTags} + ${surefire.useFile} diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java new file mode 100644 index 00000000..088a5ec8 --- /dev/null +++ b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java @@ -0,0 +1,168 @@ +/* + * 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 org.cyclonedx.schema.v2; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.networknt.schema.DefaultJsonMetaSchemaFactory; +import com.networknt.schema.DisallowUnknownKeywordFactory; +import com.networknt.schema.JsonMetaSchema; +import com.networknt.schema.JsonMetaSchemaFactory; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.NonValidationKeyword; +import com.networknt.schema.SchemaId; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.resource.ClasspathSchemaLoader; +import com.networknt.schema.resource.DisallowSchemaLoader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Verifies a CycloneDX 2.x JSON schema against its JSON test data. + *

+ * Subclass once per schema version, passing the version to the constructor + * and adding the appropriate {@code @Tag} annotations for test filtering. + */ +abstract class AbstractJsonSchemaVerificationTest { + + private static final ObjectMapper MAPPER = new JsonMapper(); + + private static final String SPDX_NAMESPACE = "cyclonedx.org/schema/spdx.schema.json"; + private static final String CRYPTO_DEF_NAMESPACE = "cyclonedx.org/schema/cryptography-defs.schema.json"; + private static final String BEHAVIOR_TAXONOMY_NAMESPACE = "cyclonedx.org/schema/behavior-taxonomy.schema.json"; + + private final String version; + private final JsonSchema schema; + + protected AbstractJsonSchemaVerificationTest(final String version) { + this.version = version; + this.schema = buildSchema(version); + } + + private static JsonSchema buildSchema(final String version) { + JsonMetaSchemaFactory metaSchemaFactory = new DefaultJsonMetaSchemaFactory() { + @Override + public JsonMetaSchema getMetaSchema( + String iri, JsonSchemaFactory schemaFactory, SchemaValidatorsConfig config) { + return addCustomKeywords(super.getMetaSchema(iri, schemaFactory, config)); + } + }; + JsonSchemaFactory factory = JsonSchemaFactory.builder() + // main schema and models are 2020-12 + .defaultMetaSchemaIri(SchemaId.V202012) + .metaSchema(addCustomKeywords(JsonMetaSchema.getV202012())) + // referenced externals may still be draft-07 + .metaSchema(addCustomKeywords(JsonMetaSchema.getV7())) + .metaSchemaFactory(metaSchemaFactory) + .schemaLoaders(b -> b.add(new ClasspathSchemaLoader()).add(DisallowSchemaLoader.getInstance())) + .schemaMappers(b -> b + .mapPrefix("https://cyclonedx.org/schema/" + version + "/model/", + "classpath:" + version + "/model/") + // version-independent externals + .mapPrefix("https://" + SPDX_NAMESPACE, "classpath:spdx.schema.json") + .mapPrefix("http://" + SPDX_NAMESPACE, "classpath:spdx.schema.json") + .mapPrefix("https://" + CRYPTO_DEF_NAMESPACE, "classpath:cryptography-defs.schema.json") + .mapPrefix("http://" + CRYPTO_DEF_NAMESPACE, "classpath:cryptography-defs.schema.json") + .mapPrefix("http://" + BEHAVIOR_TAXONOMY_NAMESPACE, "classpath:behavior-taxonomy.schema.json") + .mapPrefix("https://" + BEHAVIOR_TAXONOMY_NAMESPACE, "classpath:behavior-taxonomy.schema.json") + ).build(); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() + // in 2020-12, "format" is annotation-only unless asserted + .formatAssertionsEnabled(true) + .build(); + return factory.getSchema( + SchemaLocation.of("classpath:" + version + "/cyclonedx-" + version + ".schema.json"), + config); + } + + private static JsonMetaSchema addCustomKeywords(JsonMetaSchema metaSchema) { + return JsonMetaSchema.builder(metaSchema) + // Non-standard keywords in the CycloneDX schema files. + .keyword(new NonValidationKeyword("meta:enum")) + .unknownKeywordFactory(new DisallowUnknownKeywordFactory()) + .build(); + } + + /** Lists all test-data resources for this schema version. */ + private List getVersionResources() throws IOException { + final String resourceDirectory = version + "/"; + final List resources = new ArrayList<>(); + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resourceDirectory)) { + if (in != null) { + IOUtils.readLines(in, StandardCharsets.UTF_8) + .forEach(resource -> resources.add(resourceDirectory + resource)); + } + } + return resources; + } + + @TestFactory + Collection verifyTestData() throws Exception { + final List dynamicTests = new ArrayList<>(); + for (final String resource : getVersionResources()) { + final String resourceName = StringUtils.substringAfterLast(resource, "/"); + if (!resourceName.endsWith(".json")) { + continue; + } + if (resourceName.startsWith("valid")) { + dynamicTests.add(resourceTest(resource, true)); + } else if (resourceName.startsWith("invalid")) { + dynamicTests.add(resourceTest(resource, false)); + } + } + assertFalse(dynamicTests.isEmpty(), "no JSON test data found for schema version " + version); + return dynamicTests; + } + + /** Creates a test that validates the resource and logs which resource is being processed. */ + private DynamicTest resourceTest(final String resource, final boolean expectValid) { + return DynamicTest.dynamicTest(resource, () -> { + System.out.println("[schema " + version + "] validating (expect " + + (expectValid ? "pass" : "fail") + "): " + resource); + if (expectValid) { + assertTrue(isValid(resource), resource); + } else { + assertFalse(isValid(resource), resource); + } + }); + } + + private boolean isValid(final String resource) { + try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource); + JsonParser parser = MAPPER.createParser(input)) { + JsonNode node = parser.readValueAsTree(); + return schema.validate(node).isEmpty(); + } catch (IOException e) { + // broken/unreadable test data must fail loudly, + // not masquerade as schema rejection + throw new UncheckedIOException(e); + } + } +} diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/BaseSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/BaseSchemaVerificationTest.java deleted file mode 100644 index 6b71d371..00000000 --- a/tools/src/test/java/org/cyclonedx/schema/v2/BaseSchemaVerificationTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 org.cyclonedx.schema.v2; - -import org.apache.commons.io.IOUtils; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -public abstract class BaseSchemaVerificationTest { - - List getAllResources() throws Exception { - final List files = new ArrayList<>(); - files.addAll(getResources("2.0/")); - return files; - } - - private List getResources(final String resourceDirectory) throws Exception { - final List resources = new ArrayList<>(); - try (InputStream in = this.getClass().getClassLoader().getResourceAsStream(resourceDirectory)) { - if (in != null) { - IOUtils.readLines(in, StandardCharsets.UTF_8) - .forEach(resource -> resources.add(resourceDirectory + resource)); - } - } - return resources; - } -} diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/JsonSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/JsonSchemaVerificationTest.java deleted file mode 100644 index ed04ca61..00000000 --- a/tools/src/test/java/org/cyclonedx/schema/v2/JsonSchemaVerificationTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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 org.cyclonedx.schema.v2; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.networknt.schema.DefaultJsonMetaSchemaFactory; -import com.networknt.schema.DisallowUnknownKeywordFactory; -import com.networknt.schema.ExecutionContext; -import com.networknt.schema.Format; -import com.networknt.schema.JsonMetaSchema; -import com.networknt.schema.JsonMetaSchemaFactory; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.NonValidationKeyword; -import com.networknt.schema.SchemaId; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.SchemaValidatorsConfig; -import com.networknt.schema.resource.ClasspathSchemaLoader; -import com.networknt.schema.resource.DisallowSchemaLoader; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.TestFactory; - -@Tag("schema-v2") -class JsonSchemaVerificationTest extends BaseSchemaVerificationTest { - - private static final ObjectMapper MAPPER = new JsonMapper(); - - private static final String SPDX_NAMESPACE = "cyclonedx.org/schema/spdx.schema.json"; - private static final String CRYPTO_DEF_NAMESPACE = "cyclonedx.org/schema/cryptography-defs.schema.json"; - private static final String BEHAVIOR_TAXONOMY_NAMESPACE = "cyclonedx.org/schema/behavior-taxonomy.schema.json"; - - /** version -> compiled schema. Add new versions here. */ - private static final Map SCHEMAS = new LinkedHashMap<>(); - - static { - // future: SCHEMAS.put("2.1", buildSchema("2.1")); - SCHEMAS.put("2.0", buildSchema("2.0")); - } - - private static JsonSchema buildSchema(final String version) { - JsonMetaSchemaFactory metaSchemaFactory = new DefaultJsonMetaSchemaFactory() { - @Override - public JsonMetaSchema getMetaSchema( - String iri, JsonSchemaFactory schemaFactory, SchemaValidatorsConfig config) { - return addCustomKeywords(super.getMetaSchema(iri, schemaFactory, config)); - } - }; - JsonSchemaFactory factory = JsonSchemaFactory.builder() - // main schema and models are 2020-12 - .defaultMetaSchemaIri(SchemaId.V202012) - .metaSchema(addCustomKeywords(JsonMetaSchema.getV202012())) - // referenced externals may still be draft-07 - .metaSchema(addCustomKeywords(JsonMetaSchema.getV7())) - .metaSchemaFactory(metaSchemaFactory) - .schemaLoaders(b -> b.add(new ClasspathSchemaLoader()).add(DisallowSchemaLoader.getInstance())) - .schemaMappers(b -> b - .mapPrefix("https://cyclonedx.org/schema/" + version + "/model/", - "classpath:" + version + "/model/") - // version-independent externals - .mapPrefix("https://" + SPDX_NAMESPACE, "classpath:spdx.schema.json") - .mapPrefix("http://" + SPDX_NAMESPACE, "classpath:spdx.schema.json") - .mapPrefix("https://" + CRYPTO_DEF_NAMESPACE, "classpath:cryptography-defs.schema.json") - .mapPrefix("http://" + CRYPTO_DEF_NAMESPACE, "classpath:cryptography-defs.schema.json") - .mapPrefix("http://" + BEHAVIOR_TAXONOMY_NAMESPACE, "classpath:behavior-taxonomy.schema.json") - .mapPrefix("https://" + BEHAVIOR_TAXONOMY_NAMESPACE, "classpath:behavior-taxonomy.schema.json") - ).build(); - SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() - // in 2020-12, "format" is annotation-only unless asserted - .formatAssertionsEnabled(true) - .build(); - return factory.getSchema( - SchemaLocation.of("classpath:" + version + "/cyclonedx-" + version + ".schema.json"), - config); - } - - private static JsonMetaSchema addCustomKeywords(JsonMetaSchema metaSchema) { - return JsonMetaSchema.builder(metaSchema) - // Non-standard keywords in the CycloneDX schema files. - .keyword(new NonValidationKeyword("meta:enum")) - .unknownKeywordFactory(new DisallowUnknownKeywordFactory()) - .build(); - } - - @TestFactory - Collection dynamicTestsWithCollection() throws Exception { - final List resources = getAllResources(); - final List dynamicTests = new ArrayList<>(); - for (final String resource : resources) { - String resourceName = StringUtils.substringAfterLast(resource, "/"); - if (resourceName.endsWith(".json")) { - JsonSchema schema = getSchema(resourceName); - if (schema != null) { - if (resourceName.startsWith("valid")) { - dynamicTests.add(DynamicTest.dynamicTest( - resource, () -> assertTrue(isValid(schema, resource), resource))); - } else if (resourceName.startsWith("invalid")) { - dynamicTests.add(DynamicTest.dynamicTest( - resource, () -> assertFalse(isValid(schema, resource), resource))); - } - } - } - } - return dynamicTests; - } - - private boolean isValid(JsonSchema schema, String resource) { - try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource); - JsonParser parser = MAPPER.createParser(input)) { - JsonNode node = parser.readValueAsTree(); - return schema.validate(node).isEmpty(); - } catch (IOException e) { - return false; - } - } - - private JsonSchema getSchema(String resourceName) { - for (Map.Entry entry : SCHEMAS.entrySet()) { - if (resourceName.endsWith("-" + entry.getKey() + ".json")) { - return entry.getValue(); - } - } - return null; - } -} diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/V20JsonSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/V20JsonSchemaVerificationTest.java new file mode 100644 index 00000000..421a3aeb --- /dev/null +++ b/tools/src/test/java/org/cyclonedx/schema/v2/V20JsonSchemaVerificationTest.java @@ -0,0 +1,25 @@ +/* + * 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 org.cyclonedx.schema.v2; + +import org.junit.jupiter.api.Tag; + +@Tag("schema-v2") +@Tag("schema-v2.0") +class V20JsonSchemaVerificationTest extends AbstractJsonSchemaVerificationTest { + + V20JsonSchemaVerificationTest() { + super("2.0"); + } +} From 42a36cd6e645491e219f80cbfbe93faeacd94755 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 15:11:09 +0200 Subject: [PATCH 07/10] wip Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 6 +++++- .../v2/AbstractJsonSchemaVerificationTest.java | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 0f28a5b6..89a250b4 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -50,4 +50,8 @@ jobs: - name: test with Maven env: SCHEMA_VERSION: ${{ matrix.schema_version }} - run: mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" + run: | + mvn help:all-profiles | grep -q "Profile Id: schema-v${SCHEMA_VERSION}-tests" \ + || { echo "::error::No Maven profile 'schema-v${SCHEMA_VERSION}-tests' in tools/pom.xml"; \ + exit 1; } + mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java index 088a5ec8..7dccb178 100644 --- a/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java +++ b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java @@ -41,8 +41,10 @@ import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; /** * Verifies a CycloneDX 2.x JSON schema against its JSON test data. @@ -50,6 +52,7 @@ * Subclass once per schema version, passing the version to the constructor * and adding the appropriate {@code @Tag} annotations for test filtering. */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) abstract class AbstractJsonSchemaVerificationTest { private static final ObjectMapper MAPPER = new JsonMapper(); @@ -59,10 +62,21 @@ abstract class AbstractJsonSchemaVerificationTest { private static final String BEHAVIOR_TAXONOMY_NAMESPACE = "cyclonedx.org/schema/behavior-taxonomy.schema.json"; private final String version; - private final JsonSchema schema; + + /** Compiled lazily in {@link #compileSchema()} for clear failure attribution. */ + private JsonSchema schema; protected AbstractJsonSchemaVerificationTest(final String version) { this.version = version; + } + + /** + * Compiles the schema before any test runs. A broken schema fails here, + * clearly attributed to this lifecycle step, instead of surfacing as an + * opaque test-instantiation error. + */ + @BeforeAll + void compileSchema() { this.schema = buildSchema(version); } From 3aee71f59c20ad645d0a284064f0ae571dfe5022 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 15:35:09 +0200 Subject: [PATCH 08/10] 2.0-dev_ghworkflows_split_by_schemaversion Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 3 +++ tools/pom.xml | 21 ++++++++++++++++++- .../AbstractJsonSchemaVerificationTest.java | 14 ++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 89a250b4..21247892 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -55,3 +55,6 @@ jobs: || { echo "::error::No Maven profile 'schema-v${SCHEMA_VERSION}-tests' in tools/pom.xml"; \ exit 1; } mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" + - name: Print report + if: ${{ failure() }} + run: cat ./target/surefire-reports/* diff --git a/tools/pom.xml b/tools/pom.xml index c0d1315d..684a297d 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -46,7 +46,7 @@ - false @@ -183,6 +183,19 @@ ${surefire.includeTags} ${surefire.useFile} + + + true + false + true + true + + + false + true + true + @@ -215,5 +228,11 @@ schema-v2.0 + + schema-v2.1-tests + + schema-v2.1 + + diff --git a/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java index 7dccb178..5aa51b9a 100644 --- a/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java +++ b/tools/src/test/java/org/cyclonedx/schema/v2/AbstractJsonSchemaVerificationTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestReporter; /** * Verifies a CycloneDX 2.x JSON schema against its JSON test data. @@ -138,7 +139,7 @@ private List getVersionResources() throws IOException { } @TestFactory - Collection verifyTestData() throws Exception { + Collection verifyTestData(final TestReporter reporter) throws Exception { final List dynamicTests = new ArrayList<>(); for (final String resource : getVersionResources()) { final String resourceName = StringUtils.substringAfterLast(resource, "/"); @@ -146,20 +147,19 @@ Collection verifyTestData() throws Exception { continue; } if (resourceName.startsWith("valid")) { - dynamicTests.add(resourceTest(resource, true)); + dynamicTests.add(resourceTest(reporter, resource, true)); } else if (resourceName.startsWith("invalid")) { - dynamicTests.add(resourceTest(resource, false)); + dynamicTests.add(resourceTest(reporter, resource, false)); } } assertFalse(dynamicTests.isEmpty(), "no JSON test data found for schema version " + version); return dynamicTests; } - /** Creates a test that validates the resource and logs which resource is being processed. */ - private DynamicTest resourceTest(final String resource, final boolean expectValid) { + /** Creates a test that validates the resource, reporting progress via the JUnit platform. */ + private DynamicTest resourceTest(final TestReporter reporter, final String resource, final boolean expectValid) { return DynamicTest.dynamicTest(resource, () -> { - System.out.println("[schema " + version + "] validating (expect " - + (expectValid ? "pass" : "fail") + "): " + resource); + reporter.publishEntry("validating", resource + " (expect " + (expectValid ? "pass" : "fail") + ")"); if (expectValid) { assertTrue(isValid(resource), resource); } else { From 5c44559989e53d7824ba61a7bfb1e12ec2cf64b0 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 15:39:49 +0200 Subject: [PATCH 09/10] wip Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 4 ++-- tools/pom.xml | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 21247892..3138c299 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -52,8 +52,8 @@ jobs: SCHEMA_VERSION: ${{ matrix.schema_version }} run: | mvn help:all-profiles | grep -q "Profile Id: schema-v${SCHEMA_VERSION}-tests" \ - || { echo "::error::No Maven profile 'schema-v${SCHEMA_VERSION}-tests' in tools/pom.xml"; \ - exit 1; } + || { echo "::error::No Maven profile 'schema-v${SCHEMA_VERSION}-tests' in tools/pom.xml"; \ + exit 1; } mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" - name: Print report if: ${{ failure() }} diff --git a/tools/pom.xml b/tools/pom.xml index 684a297d..fd75f93b 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -228,11 +228,5 @@ schema-v2.0 - - schema-v2.1-tests - - schema-v2.1 - - From 7ea3d66ef1b2630c4870f71ea5514c3277e7661d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 1 Sep 2026 15:45:36 +0200 Subject: [PATCH 10/10] wip Signed-off-by: Jan Kowalleck --- .github/workflows/test_2.x_java.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_2.x_java.yml b/.github/workflows/test_2.x_java.yml index 3138c299..8405d3f0 100644 --- a/.github/workflows/test_2.x_java.yml +++ b/.github/workflows/test_2.x_java.yml @@ -50,11 +50,10 @@ jobs: - name: test with Maven env: SCHEMA_VERSION: ${{ matrix.schema_version }} - run: | - mvn help:all-profiles | grep -q "Profile Id: schema-v${SCHEMA_VERSION}-tests" \ - || { echo "::error::No Maven profile 'schema-v${SCHEMA_VERSION}-tests' in tools/pom.xml"; \ - exit 1; } - mvn clean test -P "schema-v${SCHEMA_VERSION}-tests" + run: >- + mvn clean test + -D "surefire.includeTags=schema-v${SCHEMA_VERSION}" + -D failIfNoTests=true - name: Print report if: ${{ failure() }} run: cat ./target/surefire-reports/*