diff --git a/.github/workflows/index-file-validation.yml b/.github/workflows/index-file-validation.yml index 1e88f50caa2..afdb451d714 100644 --- a/.github/workflows/index-file-validation.yml +++ b/.github/workflows/index-file-validation.yml @@ -37,7 +37,7 @@ jobs: run: | ./gradlew generateChangedIndexFileCoordinatesList -PbaseCommit=${{ github.event.pull_request.base.sha }} -PnewCommit=${{ github.event.pull_request.head.sha }} - - name: "Check that the changed index.json files conform to their schemas" + - name: "Validate index file schema and version consistency" if: steps.filter.outputs.changed == 'true' run: | ./gradlew validateIndexFiles -Pcoordinates="${{ steps.get-coords.outputs.changed-coordinates }}" diff --git a/.github/workflows/scripts/run-consecutive-tests.sh b/.github/workflows/scripts/run-consecutive-tests.sh index 15ea47b6cdd..1e7574a54c9 100644 --- a/.github/workflows/scripts/run-consecutive-tests.sh +++ b/.github/workflows/scripts/run-consecutive-tests.sh @@ -6,6 +6,8 @@ set -u +TIMEOUT="5m" + if [ $# -ne 2 ]; then echo "Usage: $0 " exit 1 @@ -41,17 +43,23 @@ run_multiple_attempts() { echo "Re-running stage '$stage' (attempt $((attempt + 1))/$max_attempts)" fi - eval "$cmd_str" + timeout --signal=QUIT --kill-after=20s "$TIMEOUT" bash -c "$cmd_str" result=$? if [ "$result" -eq 0 ]; then return 0 fi + if [ "$result" -eq 124 ] || [ "$result" -eq 131 ] || [ "$result" -eq 137 ]; then + echo "⚠️ TIMEOUT: '$stage' for $VERSION took longer than $TIMEOUT (exit code $result)." + else + echo "❌ ERROR: '$stage' for $VERSION failed with exit code $result." + fi + attempt=$((attempt + 1)) done - echo "FAILED [$stage][$VERSION][$cmd_str]" + echo "FAILED[$stage][$VERSION][$gradle_command]" return $result } diff --git a/.github/workflows/verify-new-library-version-compatibility.yml b/.github/workflows/verify-new-library-version-compatibility.yml index 32626b9ac34..d89f71bbe84 100644 --- a/.github/workflows/verify-new-library-version-compatibility.yml +++ b/.github/workflows/verify-new-library-version-compatibility.yml @@ -117,15 +117,7 @@ jobs: - name: "Extract test path and library version" if: steps.check_existing_issue.outputs.skip != 'true' id: extract-params - run: | - LIBRARY_PATH=$(echo "${{ matrix.item.name }}" | sed 's/:/\//g') - LATEST_VERSION=$(find tests/src/$LIBRARY_PATH/* -maxdepth 1 -type d | sort -V | tail -1 | cut -d '/' -f5) - TEST_PATH="$LIBRARY_PATH/$LATEST_VERSION" - TEST_COORDINATES=$(echo "$TEST_PATH" | tr / :) - - echo "LATEST_VERSION=$LATEST_VERSION" >> ${GITHUB_ENV} - echo "TEST_PATH=$TEST_PATH" >> ${GITHUB_ENV} - echo "TEST_COORDINATES=$TEST_COORDINATES" >> ${GITHUB_ENV} + run: ./gradlew extractLibraryTestParams -Pcoordinates="${{ matrix.item.name }}" - name: "Pull allowed docker images" if: steps.check_existing_issue.outputs.skip != 'true' diff --git a/AGENTS.md b/AGENTS.md index 333a4e99aea..dfeb813a78f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,8 @@ - Write type annotations in all functions and most variables. - Document code without being too verbose. - In Java and Groovy, always import classes and use them without qualified names. +- In Java use multi-line strings where possible. +- In Java use the markdown style for comments. ## Testing individual components diff --git a/docs/DEVELOPING.md b/docs/DEVELOPING.md index f0e1f232435..669425d55ae 100644 --- a/docs/DEVELOPING.md +++ b/docs/DEVELOPING.md @@ -51,7 +51,7 @@ For a single coordinate, CI runs three steps in this order: ```console ./gradlew pullAllowedDockerImages -Pcoordinates=org.postgresql:postgresql:42.7.3 ``` -2. Validate index.json schemas: +2. Validate index.json file integrity and schemas: ```console ./gradlew validateIndexFiles -Pcoordinates=org.postgresql:postgresql:42.7.3 ``` diff --git a/metadata/ch.qos.logback/logback-classic/index.json b/metadata/ch.qos.logback/logback-classic/index.json index 3c370b5866d..6c40f094f34 100644 --- a/metadata/ch.qos.logback/logback-classic/index.json +++ b/metadata/ch.qos.logback/logback-classic/index.json @@ -5,15 +5,7 @@ "metadata-version" : "1.4.9", "test-version": "1.4.1", "tested-versions" : [ - "1.4.9" - ] - }, - { - "module" : "ch.qos.logback:logback-classic", - "default-for" : "1\\.4\\.[0-8]", - "metadata-version" : "1.4.1", - "tested-versions" : [ - "1.4.1", + "1.4.9", "1.4.10", "1.4.11", "1.4.12", @@ -28,6 +20,14 @@ "1.5.6" ] }, + { + "module" : "ch.qos.logback:logback-classic", + "default-for" : "1\\.4\\.[0-8]", + "metadata-version" : "1.4.1", + "tested-versions" : [ + "1.4.1" + ] + }, { "module" : "ch.qos.logback:logback-classic", "default-for" : "1\\.[0-3]\\..*", diff --git a/metadata/index.json b/metadata/index.json index fc2f310a9a7..97ae7a58ab3 100644 --- a/metadata/index.json +++ b/metadata/index.json @@ -1,396 +1,400 @@ [ { - "allowed-packages" : [ "ch.qos.logback" ], "directory" : "ch.qos.logback.contrib/logback-jackson", - "module" : "ch.qos.logback.contrib:logback-jackson" + "module" : "ch.qos.logback.contrib:logback-jackson", + "allowed-packages" : [ "ch.qos.logback" ] }, { - "allowed-packages" : [ "ch.qos.logback" ], "directory" : "ch.qos.logback.contrib/logback-json-classic", - "module" : "ch.qos.logback.contrib:logback-json-classic" + "module" : "ch.qos.logback.contrib:logback-json-classic", + "allowed-packages" : [ "ch.qos.logback" ] }, { - "allowed-packages" : [ "ch.qos.logback" ], "directory" : "ch.qos.logback/logback-classic", - "module" : "ch.qos.logback:logback-classic" + "module" : "ch.qos.logback:logback-classic", + "allowed-packages" : [ "ch.qos.logback" ] }, { - "allowed-packages" : [ "com.ecwid" ], "directory" : "com.ecwid.consul/consul-api", - "module" : "com.ecwid.consul:consul-api" + "module" : "com.ecwid.consul:consul-api", + "allowed-packages" : [ "com.ecwid" ] }, { - "allowed-packages" : [ "com.fasterxml.jackson", "java.sql", "org.w3c.dom", "javax.xml" ], "directory" : "com.fasterxml.jackson.core/jackson-databind", - "module" : "com.fasterxml.jackson.core:jackson-databind" + "module" : "com.fasterxml.jackson.core:jackson-databind", + "allowed-packages" : [ "com.fasterxml.jackson", "java.sql", "org.w3c.dom", "javax.xml" ] }, { - "allowed-packages" : [ "com.github.benmanes.caffeine" ], "directory" : "com.github.ben-manes.caffeine/caffeine", - "module" : "com.github.ben-manes.caffeine:caffeine" + "module" : "com.github.ben-manes.caffeine:caffeine", + "allowed-packages" : [ "com.github.benmanes.caffeine" ] }, { - "allowed-packages" : [ "com.github.luben" ], "directory" : "com.github.luben/zstd-jni", - "module" : "com.github.luben:zstd-jni" + "module" : "com.github.luben:zstd-jni", + "allowed-packages" : [ "com.github.luben" ] }, { - "allowed-packages" : [ "com.google.protobuf" ], "directory" : "com.google.protobuf/protobuf-java-util", - "module" : "com.google.protobuf:protobuf-java-util" + "module" : "com.google.protobuf:protobuf-java-util", + "allowed-packages" : [ "com.google.protobuf" ] }, { - "allowed-packages" : [ "graphql" ], "directory" : "com.graphql-java/graphql-java", - "module" : "com.graphql-java:graphql-java" + "module" : "com.graphql-java:graphql-java", + "allowed-packages" : [ "graphql" ] }, { - "allowed-packages" : [ "graphql" ], "directory" : "com.graphql-java/graphql-java-extended-validation", "module" : "com.graphql-java:graphql-java-extended-validation", - "requires" : [ "com.graphql-java:graphql-java", "org.hibernate.validator:hibernate-validator" ] + "requires" : [ "com.graphql-java:graphql-java", "org.hibernate.validator:hibernate-validator" ], + "allowed-packages" : [ "graphql" ] }, { - "allowed-packages" : [ "org.h2" ], "directory" : "com.h2database/h2", - "module" : "com.h2database:h2" + "module" : "com.h2database:h2", + "allowed-packages" : [ "org.h2" ] }, { - "allowed-packages" : [ "com.hazelcast" ], "directory" : "com.hazelcast/hazelcast", - "module" : "com.hazelcast:hazelcast" + "module" : "com.hazelcast:hazelcast", + "allowed-packages" : [ "com.hazelcast" ] }, { - "allowed-packages" : [ "com.itextpdf" ], "directory" : "com.itextpdf/forms", - "module" : "com.itextpdf:forms" + "module" : "com.itextpdf:forms", + "allowed-packages" : [ "com.itextpdf" ] }, { - "allowed-packages" : [ "com.itextpdf" ], "directory" : "com.itextpdf/io", - "module" : "com.itextpdf:io" + "module" : "com.itextpdf:io", + "allowed-packages" : [ "com.itextpdf" ] }, { - "allowed-packages" : [ "com.itextpdf" ], "directory" : "com.itextpdf/kernel", - "module" : "com.itextpdf:kernel" + "module" : "com.itextpdf:kernel", + "allowed-packages" : [ "com.itextpdf" ] }, { - "allowed-packages" : [ "com.itextpdf" ], "directory" : "com.itextpdf/layout", - "module" : "com.itextpdf:layout" + "module" : "com.itextpdf:layout", + "allowed-packages" : [ "com.itextpdf" ] }, { - "allowed-packages" : [ "com.itextpdf" ], "directory" : "com.itextpdf/svg", - "module" : "com.itextpdf:svg" + "module" : "com.itextpdf:svg", + "allowed-packages" : [ "com.itextpdf" ] }, { - "allowed-packages" : [ "com.microsoft.sqlserver" ], "directory" : "com.microsoft.sqlserver/mssql-jdbc", - "module" : "com.microsoft.sqlserver:mssql-jdbc" + "module" : "com.microsoft.sqlserver:mssql-jdbc", + "allowed-packages" : [ "com.microsoft.sqlserver" ] }, { - "allowed-packages" : [ "com.mysql" ], "directory" : "com.mysql/mysql-connector-j", - "module" : "com.mysql:mysql-connector-j" + "module" : "com.mysql:mysql-connector-j", + "allowed-packages" : [ "com.mysql" ] }, { - "allowed-packages" : [ "com.sun.mail", "jakarta" ], "directory" : "com.sun.mail/jakarta.mail", - "module" : "com.sun.mail:jakarta.mail" + "module" : "com.sun.mail:jakarta.mail", + "allowed-packages" : [ "com.sun.mail", "jakarta" ] }, { - "allowed-packages" : [ "com.zaxxer", "jdk.internal" ], "directory" : "com.zaxxer/HikariCP", - "module" : "com.zaxxer:HikariCP" + "module" : "com.zaxxer:HikariCP", + "allowed-packages" : [ "com.zaxxer", "jdk.internal" ] }, { - "allowed-packages" : [ "org.apache.commons.logging" ], "directory" : "commons-logging/commons-logging", - "module" : "commons-logging:commons-logging" + "module" : "commons-logging:commons-logging", + "allowed-packages" : [ "org.apache.commons.logging" ] }, { - "allowed-packages" : [ "io.grpc" ], "directory" : "io.grpc/grpc-core", - "module" : "io.grpc:grpc-core" + "module" : "io.grpc:grpc-core", + "allowed-packages" : [ "io.grpc" ] }, { - "allowed-packages" : [ "io.grpc" ], "directory" : "io.grpc/grpc-netty", - "module" : "io.grpc:grpc-netty" + "module" : "io.grpc:grpc-netty", + "allowed-packages" : [ "io.grpc" ] }, { - "allowed-packages" : [ "io.jsonwebtoken" ], "directory" : "io.jsonwebtoken/jjwt-gson", - "module" : "io.jsonwebtoken:jjwt-gson" + "module" : "io.jsonwebtoken:jjwt-gson", + "allowed-packages" : [ "io.jsonwebtoken" ] }, { - "allowed-packages" : [ "io.jsonwebtoken" ], "directory" : "io.jsonwebtoken/jjwt-jackson", - "module" : "io.jsonwebtoken:jjwt-jackson" + "module" : "io.jsonwebtoken:jjwt-jackson", + "allowed-packages" : [ "io.jsonwebtoken" ] }, { - "allowed-packages" : [ "io.jsonwebtoken" ], "directory" : "io.jsonwebtoken/jjwt-orgjson", - "module" : "io.jsonwebtoken:jjwt-orgjson" + "module" : "io.jsonwebtoken:jjwt-orgjson", + "allowed-packages" : [ "io.jsonwebtoken" ] }, { - "allowed-packages" : [ "io.nats" ], "directory" : "io.nats/jnats", - "module" : "io.nats:jnats" + "module" : "io.nats:jnats", + "allowed-packages" : [ "io.nats" ] }, { - "allowed-packages" : [ "io.netty" ], "directory" : "io.netty/netty-common", - "module" : "io.netty:netty-common" + "module" : "io.netty:netty-common", + "allowed-packages" : [ "io.netty" ] }, { - "allowed-packages" : [ "io.netty" ], "directory" : "io.netty/netty-transport", - "module" : "io.netty:netty-transport" + "module" : "io.netty:netty-transport", + "allowed-packages" : [ "io.netty" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-exporter-jaeger", - "module" : "io.opentelemetry:opentelemetry-exporter-jaeger" + "module" : "io.opentelemetry:opentelemetry-exporter-jaeger", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-exporter-logging", - "module" : "io.opentelemetry:opentelemetry-exporter-logging" + "module" : "io.opentelemetry:opentelemetry-exporter-logging", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-exporter-otlp", - "module" : "io.opentelemetry:opentelemetry-exporter-otlp" + "module" : "io.opentelemetry:opentelemetry-exporter-otlp", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-exporter-zipkin", - "module" : "io.opentelemetry:opentelemetry-exporter-zipkin" + "module" : "io.opentelemetry:opentelemetry-exporter-zipkin", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-sdk-metrics", - "module" : "io.opentelemetry:opentelemetry-sdk-metrics" + "module" : "io.opentelemetry:opentelemetry-sdk-metrics", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.opentelemetry" ], "directory" : "io.opentelemetry/opentelemetry-sdk-trace", - "module" : "io.opentelemetry:opentelemetry-sdk-trace" + "module" : "io.opentelemetry:opentelemetry-sdk-trace", + "allowed-packages" : [ "io.opentelemetry" ] }, { - "allowed-packages" : [ "io.undertow" ], "directory" : "io.undertow/undertow-core", - "module" : "io.undertow:undertow-core" + "module" : "io.undertow:undertow-core", + "allowed-packages" : [ "io.undertow" ] }, { - "allowed-packages" : [ "jakarta.servlet" ], "directory" : "jakarta.servlet/jakarta.servlet-api", - "module" : "jakarta.servlet:jakarta.servlet-api" + "module" : "jakarta.servlet:jakarta.servlet-api", + "allowed-packages" : [ "jakarta.servlet" ] }, { - "allowed-packages" : [ "javax.cache" ], "directory" : "javax.cache/cache-api", - "module" : "javax.cache:cache-api" + "module" : "javax.cache:cache-api", + "allowed-packages" : [ "javax.cache" ] }, { - "allowed-packages" : [ "org.apache.log4j" ], "directory" : "log4j/log4j", - "module" : "log4j:log4j" + "module" : "log4j:log4j", + "allowed-packages" : [ "org.apache.log4j" ] }, { - "allowed-packages" : [ "mysql" ], "directory" : "mysql/mysql-connector-java", - "module" : "mysql:mysql-connector-java" + "module" : "mysql:mysql-connector-java", + "allowed-packages" : [ "mysql" ] }, { - "allowed-packages" : [ "com.sun.jna" ], "directory" : "net.java.dev.jna/jna", - "module" : "net.java.dev.jna:jna" + "module" : "net.java.dev.jna:jna", + "allowed-packages" : [ "com.sun.jna" ] }, { - "allowed-packages" : [ "org.apache.activemq" ], "directory" : "org.apache.activemq/activemq-broker", - "module" : "org.apache.activemq:activemq-broker" + "module" : "org.apache.activemq:activemq-broker", + "allowed-packages" : [ "org.apache.activemq" ] }, { - "allowed-packages" : [ "org.apache.activemq" ], "directory" : "org.apache.activemq/activemq-client", - "module" : "org.apache.activemq:activemq-client" + "module" : "org.apache.activemq:activemq-client", + "allowed-packages" : [ "org.apache.activemq" ] }, { - "allowed-packages" : [ "org.apache.activemq" ], "directory" : "org.apache.activemq/artemis-jms-client", - "module" : "org.apache.activemq:artemis-jms-client" + "module" : "org.apache.activemq:artemis-jms-client", + "allowed-packages" : [ "org.apache.activemq" ] }, { - "allowed-packages" : [ "org.apache.commons" ], "directory" : "org.apache.commons/commons-compress", - "module" : "org.apache.commons:commons-compress" + "module" : "org.apache.commons:commons-compress", + "allowed-packages" : [ "org.apache.commons" ] }, { - "allowed-packages" : [ "org.apache.commons" ], "directory" : "org.apache.commons/commons-dbcp2", - "module" : "org.apache.commons:commons-dbcp2" + "module" : "org.apache.commons:commons-dbcp2", + "allowed-packages" : [ "org.apache.commons" ] }, { - "allowed-packages" : [ "org.apache.commons" ], "directory" : "org.apache.commons/commons-pool2", - "module" : "org.apache.commons:commons-pool2" + "module" : "org.apache.commons:commons-pool2", + "allowed-packages" : [ "org.apache.commons" ] }, { - "allowed-packages" : [ "org.apache.http" ], "directory" : "org.apache.httpcomponents/httpclient", - "module" : "org.apache.httpcomponents:httpclient" + "module" : "org.apache.httpcomponents:httpclient", + "allowed-packages" : [ "org.apache.http" ] }, { - "allowed-packages" : [ "org.apache.kafka" ], "directory" : "org.apache.kafka/kafka-clients", - "module" : "org.apache.kafka:kafka-clients" + "module" : "org.apache.kafka:kafka-clients", + "allowed-packages" : [ "org.apache.kafka" ] }, { - "allowed-packages" : [ "org.apache.kafka", "org.rocksdb" ], "directory" : "org.apache.kafka/kafka-streams", - "module" : "org.apache.kafka:kafka-streams" + "module" : "org.apache.kafka:kafka-streams", + "allowed-packages" : [ "org.apache.kafka", "org.rocksdb" ] +}, { + "directory" : "org.apache.santuario/xmlsec", + "module" : "org.apache.santuario:xmlsec", + "allowed-packages" : [ "org.apache.santuario" ] }, { - "allowed-packages" : [ "org.apache.tomcat", "org.apache.catalina" ], "directory" : "org.apache.tomcat.embed/tomcat-embed-core", - "module" : "org.apache.tomcat.embed:tomcat-embed-core" + "module" : "org.apache.tomcat.embed:tomcat-embed-core", + "allowed-packages" : [ "org.apache.tomcat", "org.apache.catalina" ] }, { - "allowed-packages" : [ "org.apache.tomcat.jdbc" ], "directory" : "org.apache.tomcat/tomcat-jdbc", - "module" : "org.apache.tomcat:tomcat-jdbc" + "module" : "org.apache.tomcat:tomcat-jdbc", + "allowed-packages" : [ "org.apache.tomcat.jdbc" ] }, { - "allowed-packages" : [ "org.bouncycastle" ], "directory" : "org.bouncycastle/bcpkix-jdk15on", - "module" : "org.bouncycastle:bcpkix-jdk15on" + "module" : "org.bouncycastle:bcpkix-jdk15on", + "allowed-packages" : [ "org.bouncycastle" ] }, { - "allowed-packages" : [ "org.bouncycastle" ], "directory" : "org.bouncycastle/bcpkix-jdk15to18", - "module" : "org.bouncycastle:bcpkix-jdk15to18" + "module" : "org.bouncycastle:bcpkix-jdk15to18", + "allowed-packages" : [ "org.bouncycastle" ] }, { - "allowed-packages" : [ "org.bouncycastle" ], "directory" : "org.bouncycastle/bcpkix-jdk18on", - "module" : "org.bouncycastle:bcpkix-jdk18on" -}, { - "allowed-packages" : [ "jakarta" ], + "module" : "org.bouncycastle:bcpkix-jdk18on", + "allowed-packages" : [ "org.bouncycastle" ] +}, { "directory" : "org.eclipse.angus/jakarta.mail", - "module" : "org.eclipse.angus:jakarta.mail" + "module" : "org.eclipse.angus:jakarta.mail", + "allowed-packages" : [ "jakarta" ] }, { - "allowed-packages" : [ "org.eclipse.jetty" ], "directory" : "org.eclipse.jetty/jetty-client", - "module" : "org.eclipse.jetty:jetty-client" + "module" : "org.eclipse.jetty:jetty-client", + "allowed-packages" : [ "org.eclipse.jetty" ] }, { - "allowed-packages" : [ "org.eclipse" ], "directory" : "org.eclipse.jetty/jetty-server", - "module" : "org.eclipse.jetty:jetty-server" + "module" : "org.eclipse.jetty:jetty-server", + "allowed-packages" : [ "org.eclipse" ] }, { - "allowed-packages" : [ "org.eclipse.jetty" ], "directory" : "org.eclipse.jetty/jetty-util", - "module" : "org.eclipse.jetty:jetty-util" + "module" : "org.eclipse.jetty:jetty-util", + "allowed-packages" : [ "org.eclipse.jetty" ] }, { - "allowed-packages" : [ "org.eclipse.jgit" ], "directory" : "org.eclipse.jgit/org.eclipse.jgit", - "module" : "org.eclipse.jgit:org.eclipse.jgit" + "module" : "org.eclipse.jgit:org.eclipse.jgit", + "allowed-packages" : [ "org.eclipse.jgit" ] }, { - "allowed-packages" : [ "org.eclipse.paho.client.mqttv3" ], "directory" : "org.eclipse.paho/org.eclipse.paho.client.mqttv3", - "module" : "org.eclipse.paho:org.eclipse.paho.client.mqttv3" + "module" : "org.eclipse.paho:org.eclipse.paho.client.mqttv3", + "allowed-packages" : [ "org.eclipse.paho.client.mqttv3" ] }, { - "allowed-packages" : [ "org.eclipse.paho.mqttv5" ], "directory" : "org.eclipse.paho/org.eclipse.paho.mqttv5.client", - "module" : "org.eclipse.paho:org.eclipse.paho.mqttv5.client" + "module" : "org.eclipse.paho:org.eclipse.paho.mqttv5.client", + "allowed-packages" : [ "org.eclipse.paho.mqttv5" ] }, { - "allowed-packages" : [ "org.ehcache" ], "directory" : "org.ehcache/ehcache", - "module" : "org.ehcache:ehcache" + "module" : "org.ehcache:ehcache", + "allowed-packages" : [ "org.ehcache" ] }, { - "allowed-packages" : [ "org.example" ], "directory" : "org.example/library", - "module" : "org.example:library" + "module" : "org.example:library", + "allowed-packages" : [ "org.example" ] }, { - "allowed-packages" : [ "org.flywaydb", "org.apache", "org.slf4j" ], "directory" : "org.flywaydb/flyway-core", - "module" : "org.flywaydb:flyway-core" + "module" : "org.flywaydb:flyway-core", + "allowed-packages" : [ "org.flywaydb", "org.apache", "org.slf4j" ] }, { - "allowed-packages" : [ "org.flywaydb" ], "directory" : "org.flywaydb/flyway-database-postgresql", - "module" : "org.flywaydb:flyway-database-postgresql" + "module" : "org.flywaydb:flyway-database-postgresql", + "allowed-packages" : [ "org.flywaydb" ] }, { - "allowed-packages" : [ "org.flywaydb" ], "directory" : "org.flywaydb/flyway-sqlserver", - "module" : "org.flywaydb:flyway-sqlserver" + "module" : "org.flywaydb:flyway-sqlserver", + "allowed-packages" : [ "org.flywaydb" ] }, { - "allowed-packages" : [ "org.freemarker" ], "directory" : "org.freemarker/freemarker", - "module" : "org.freemarker:freemarker" + "module" : "org.freemarker:freemarker", + "allowed-packages" : [ "org.freemarker" ] }, { - "allowed-packages" : [ "org.glassfish.jaxb", "jakarta.xml" ], "directory" : "org.glassfish.jaxb/jaxb-runtime", - "module" : "org.glassfish.jaxb:jaxb-runtime" + "module" : "org.glassfish.jaxb:jaxb-runtime", + "allowed-packages" : [ "org.glassfish.jaxb", "jakarta.xml" ] }, { - "allowed-packages" : [ "org.HdrHistogram" ], "directory" : "org.hdrhistogram/HdrHistogram", - "module" : "org.hdrhistogram:HdrHistogram" + "module" : "org.hdrhistogram:HdrHistogram", + "allowed-packages" : [ "org.HdrHistogram" ] }, { - "allowed-packages" : [ "org.hibernate", "jakarta", "org.h2.Driver", "org.postgresql.Driver", "com.mysql.cj.jdbc.Driver", "org.hsqldb.jdbc.JDBCDriver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "oracle.jdbc.driver.OracleDriver", "org.mariadb.jdbc.Driver" ], "directory" : "org.hibernate.orm/hibernate-core", - "module" : "org.hibernate.orm:hibernate-core" + "module" : "org.hibernate.orm:hibernate-core", + "allowed-packages" : [ "org.hibernate", "jakarta", "org.h2.Driver", "org.postgresql.Driver", "com.mysql.cj.jdbc.Driver", "org.hsqldb.jdbc.JDBCDriver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "oracle.jdbc.driver.OracleDriver", "org.mariadb.jdbc.Driver" ] }, { - "allowed-packages" : [ "org.hibernate", "jakarta" ], "directory" : "org.hibernate.orm/hibernate-envers", "module" : "org.hibernate.orm:hibernate-envers", - "requires" : [ "org.hibernate.orm:hibernate-core" ] + "requires" : [ "org.hibernate.orm:hibernate-core" ], + "allowed-packages" : [ "org.hibernate", "jakarta" ] }, { - "allowed-packages" : [ "org.hibernate.reactive" ], "directory" : "org.hibernate.reactive/hibernate-reactive-core", - "module" : "org.hibernate.reactive:hibernate-reactive-core" + "module" : "org.hibernate.reactive:hibernate-reactive-core", + "allowed-packages" : [ "org.hibernate.reactive" ] }, { - "allowed-packages" : [ "org.hibernate", "jakarta" ], "directory" : "org.hibernate.validator/hibernate-validator", "module" : "org.hibernate.validator:hibernate-validator", - "requires" : [ "org.jboss.logging:jboss-logging" ] + "requires" : [ "org.jboss.logging:jboss-logging" ], + "allowed-packages" : [ "org.hibernate", "jakarta" ] }, { - "allowed-packages" : [ "org.hibernate", "jakarta", "org.h2.Driver", "org.postgresql.Driver", "com.mysql.cj.jdbc.Driver", "org.hsqldb.jdbc.JDBCDriver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "oracle.jdbc.driver.OracleDriver", "org.mariadb.jdbc.Driver" ], "directory" : "org.hibernate/hibernate-core", - "module" : "org.hibernate:hibernate-core" + "module" : "org.hibernate:hibernate-core", + "allowed-packages" : [ "org.hibernate", "jakarta", "org.h2.Driver", "org.postgresql.Driver", "com.mysql.cj.jdbc.Driver", "org.hsqldb.jdbc.JDBCDriver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "oracle.jdbc.driver.OracleDriver", "org.mariadb.jdbc.Driver" ] }, { - "allowed-packages" : [ "org.hibernate" ], "directory" : "org.hibernate/hibernate-spatial", - "module" : "org.hibernate:hibernate-spatial" + "module" : "org.hibernate:hibernate-spatial", + "allowed-packages" : [ "org.hibernate" ] }, { - "allowed-packages" : [ "org.jboss" ], "directory" : "org.jboss.logging/jboss-logging", - "module" : "org.jboss.logging:jboss-logging" + "module" : "org.jboss.logging:jboss-logging", + "allowed-packages" : [ "org.jboss" ] }, { - "allowed-packages" : [ "org.jboss" ], "directory" : "org.jboss.spec.javax.servlet/jboss-servlet-api_4.0_spec", - "module" : "org.jboss.spec.javax.servlet:jboss-servlet-api_4.0_spec" + "module" : "org.jboss.spec.javax.servlet:jboss-servlet-api_4.0_spec", + "allowed-packages" : [ "org.jboss" ] }, { - "allowed-packages" : [ "org.jctools" ], "directory" : "org.jctools/jctools-core", - "module" : "org.jctools:jctools-core" + "module" : "org.jctools:jctools-core", + "allowed-packages" : [ "org.jctools" ] }, { - "allowed-packages" : [ "kotlin" ], "directory" : "org.jetbrains.kotlin/kotlin-reflect", - "module" : "org.jetbrains.kotlin:kotlin-reflect" + "module" : "org.jetbrains.kotlin:kotlin-reflect", + "allowed-packages" : [ "kotlin" ] }, { - "allowed-packages" : [ "kotlin" ], "directory" : "org.jetbrains.kotlin/kotlin-stdlib", - "module" : "org.jetbrains.kotlin:kotlin-stdlib" + "module" : "org.jetbrains.kotlin:kotlin-stdlib", + "allowed-packages" : [ "kotlin" ] }, { - "allowed-packages" : [ "org.jline", "org.fusesource" ], "directory" : "org.jline/jline", - "module" : "org.jline:jline" + "module" : "org.jline:jline", + "allowed-packages" : [ "org.jline", "org.fusesource" ] }, { - "allowed-packages" : [ "org.jline", "org.fusesource" ], "module" : "org.jline:jline-console", - "requires" : [ "org.jline:jline" ] + "requires" : [ "org.jline:jline" ], + "allowed-packages" : [ "org.jline", "org.fusesource" ] }, { - "allowed-packages" : [ "org.jline", "org.fusesource" ], "module" : "org.jline:jline-terminal", - "requires" : [ "org.jline:jline" ] + "requires" : [ "org.jline:jline" ], + "allowed-packages" : [ "org.jline", "org.fusesource" ] }, { - "allowed-packages" : [ "org.jooq", "org_jooq" ], "directory" : "org.jooq/jooq", - "module" : "org.jooq:jooq" + "module" : "org.jooq:jooq", + "allowed-packages" : [ "org.jooq", "org_jooq" ] }, { - "allowed-packages" : [ "org.liquibase", "liquibase" ], "directory" : "org.liquibase/liquibase-core", - "module" : "org.liquibase:liquibase-core" + "module" : "org.liquibase:liquibase-core", + "allowed-packages" : [ "org.liquibase", "liquibase" ] }, { - "allowed-packages" : [ "org.mariadb" ], "directory" : "org.mariadb.jdbc/mariadb-java-client", - "module" : "org.mariadb.jdbc:mariadb-java-client" + "module" : "org.mariadb.jdbc:mariadb-java-client", + "allowed-packages" : [ "org.mariadb" ] }, { - "allowed-packages" : [ "org.mariadb" ], "directory" : "org.mariadb/r2dbc-mariadb", - "module" : "org.mariadb:r2dbc-mariadb" + "module" : "org.mariadb:r2dbc-mariadb", + "allowed-packages" : [ "org.mariadb" ] }, { - "allowed-packages" : [ "org.mockito" ], "directory" : "org.mockito/mockito-core", - "module" : "org.mockito:mockito-core" + "module" : "org.mockito:mockito-core", + "allowed-packages" : [ "org.mockito" ] }, { - "allowed-packages" : [ "org.opengauss" ], "directory" : "org.opengauss/opengauss-jdbc", - "module" : "org.opengauss:opengauss-jdbc" + "module" : "org.opengauss:opengauss-jdbc", + "allowed-packages" : [ "org.opengauss" ] }, { - "allowed-packages" : [ "org.postgresql" ], "directory" : "org.postgresql/postgresql", - "module" : "org.postgresql:postgresql" + "module" : "org.postgresql:postgresql", + "allowed-packages" : [ "org.postgresql" ] }, { - "allowed-packages" : [ "org.quartz" ], "directory" : "org.quartz-scheduler/quartz", - "module" : "org.quartz-scheduler:quartz" + "module" : "org.quartz-scheduler:quartz", + "allowed-packages" : [ "org.quartz" ] }, { - "allowed-packages" : [ "org.testcontainers" ], "directory" : "org.testcontainers/testcontainers", - "module" : "org.testcontainers:testcontainers" + "module" : "org.testcontainers:testcontainers", + "allowed-packages" : [ "org.testcontainers" ] }, { - "allowed-packages" : [ "org.thymeleaf", "org.springframework" ], "directory" : "org.thymeleaf.extras/thymeleaf-extras-springsecurity6", - "module" : "org.thymeleaf.extras:thymeleaf-extras-springsecurity6" + "module" : "org.thymeleaf.extras:thymeleaf-extras-springsecurity6", + "allowed-packages" : [ "org.thymeleaf", "org.springframework" ] }, { - "allowed-packages" : [ "org.thymeleaf" ], "directory" : "org.thymeleaf/thymeleaf", - "module" : "org.thymeleaf:thymeleaf" + "module" : "org.thymeleaf:thymeleaf", + "allowed-packages" : [ "org.thymeleaf" ] }, { - "allowed-packages" : [ "org.thymeleaf" ], "directory" : "org.thymeleaf/thymeleaf-spring6", - "module" : "org.thymeleaf:thymeleaf-spring6" + "module" : "org.thymeleaf:thymeleaf-spring6", + "allowed-packages" : [ "org.thymeleaf" ] }, { - "allowed-packages" : [ "samples" ], "directory" : "samples/docker", - "module" : "samples:docker" -} ] + "module" : "samples:docker", + "allowed-packages" : [ "samples" ] +} ] \ No newline at end of file diff --git a/metadata/org.apache.santuario/xmlsec/4.0.4/reflect-config.json b/metadata/org.apache.santuario/xmlsec/4.0.4/reflect-config.json new file mode 100644 index 00000000000..b2f1fda9bf7 --- /dev/null +++ b/metadata/org.apache.santuario/xmlsec/4.0.4/reflect-config.json @@ -0,0 +1,254 @@ +[ + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "com.sun.crypto.provider.AESCipher$General", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.implementations.IntegrityHmac" + }, + "name": "com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "com.sun.crypto.provider.RSACipher", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.parser.XMLParserImpl" + }, + "name": "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.stax.ext.XMLSecurityConstants" + }, + "name": "com.sun.org.apache.xerces.internal.jaxp.datatype.DatatypeFactoryImpl", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "java.security.SecureRandomParameters" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.stax.ext.XMLSecurityConstants" + }, + "name": "java.security.SecureRandomParameters" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithmSpi" + }, + "name": "java.security.interfaces.RSAPrivateKey" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "java.security.interfaces.RSAPrivateKey" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithmSpi" + }, + "name": "java.security.interfaces.RSAPublicKey" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "java.security.interfaces.RSAPublicKey" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithm" + }, + "name": "org.apache.xml.security.algorithms.implementations.IntegrityHmac$IntegrityHmacSHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithm" + }, + "name": "org.apache.xml.security.algorithms.implementations.SignatureBaseRSA$SignatureRSASHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.signature.SignedInfo" + }, + "name": "org.apache.xml.security.c14n.implementations.Canonicalizer20010315ExclOmitComments", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.AbstractSerializer" + }, + "name": "org.apache.xml.security.c14n.implementations.CanonicalizerPhysical", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "sun.security.provider.NativePRNG", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.security.SecureRandomParameters" + ] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.stax.ext.XMLSecurityConstants" + }, + "name": "sun.security.provider.NativePRNG", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.security.SecureRandomParameters" + ] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "name": "sun.security.provider.SHA", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.MessageDigestAlgorithm" + }, + "name": "sun.security.provider.SHA2$SHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithmSpi" + }, + "name": "sun.security.provider.SHA2$SHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.implementations.IntegrityHmac" + }, + "name": "sun.security.provider.SHA2$SHA256", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.algorithms.SignatureAlgorithmSpi" + }, + "name": "sun.security.rsa.RSASignature$SHA256withRSA", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + } +] diff --git a/metadata/org.apache.santuario/xmlsec/4.0.4/resource-config.json b/metadata/org.apache.santuario/xmlsec/4.0.4/resource-config.json new file mode 100644 index 00000000000..47023c2e22e --- /dev/null +++ b/metadata/org.apache.santuario/xmlsec/4.0.4/resource-config.json @@ -0,0 +1,38 @@ +{ + "resources": { + "includes": [ + { + "condition": { + "typeReachable": "org.apache.xml.security.stax.ext.XMLSecurityConstants" + }, + "pattern": "\\QMETA-INF/services/javax.xml.datatype.DatatypeFactory\\E" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.parser.XMLParserImpl" + }, + "pattern": "\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.stax.ext.XMLSecurityConstants" + }, + "pattern": "\\QMETA-INF/services/javax.xml.stream.XMLOutputFactory\\E" + }, + { + "condition": { + "typeReachable": "org.apache.xml.security.encryption.XMLCipher" + }, + "pattern": "\\QMETA-INF/services/javax.xml.transform.TransformerFactory\\E" + } + ] + }, + "bundles": [ + { + "name": "org/apache/xml/security/resource/xmlsecurity", + "locales": [ + "en" + ] + } + ] +} diff --git a/metadata/org.apache.santuario/xmlsec/index.json b/metadata/org.apache.santuario/xmlsec/index.json new file mode 100644 index 00000000000..20d921c9be9 --- /dev/null +++ b/metadata/org.apache.santuario/xmlsec/index.json @@ -0,0 +1,10 @@ +[ + { + "latest": true, + "metadata-version": "4.0.4", + "module": "org.apache.santuario:xmlsec", + "tested-versions": [ + "4.0.4" + ] + } +] diff --git a/metadata/org.liquibase/liquibase-core/5.0.1/reflect-config.json b/metadata/org.liquibase/liquibase-core/5.0.1/reflect-config.json index 0f823530837..0d9674c95ea 100644 --- a/metadata/org.liquibase/liquibase-core/5.0.1/reflect-config.json +++ b/metadata/org.liquibase/liquibase-core/5.0.1/reflect-config.json @@ -8160,5 +8160,19 @@ }, "name": "liquibase.ui.LoggerUIService", "allPublicConstructors": true + }, + { + "condition": { + "typeReachable": "liquibase.configuration.LiquibaseConfiguration" + }, + "name": "liquibase.change.core.SQLFileChange", + "methods": [ + { + "name": "setPath", + "parameterTypes": [ + "java.lang.String" + ] + } + ] } ] diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/.gitignore b/tests/src/org.apache.santuario/xmlsec/4.0.4/.gitignore new file mode 100644 index 00000000000..c98c7875b0e --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/.gitignore @@ -0,0 +1,4 @@ +gradlew.bat +gradlew +gradle/ +build/ diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/build.gradle b/tests/src/org.apache.santuario/xmlsec/4.0.4/build.gradle new file mode 100644 index 00000000000..89856474cad --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +plugins { + id "org.graalvm.internal.tck" +} + +String libraryVersion = tck.testedLibraryVersion.get() + +dependencies { + testImplementation "org.apache.santuario:xmlsec:$libraryVersion" + testImplementation 'org.assertj:assertj-core:3.22.0' +} + +graalvmNative { + agent { + defaultMode = "conditional" + modes { + conditional { + userCodeFilterPath = "user-code-filter.json" + } + } + } +} diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/gradle.properties b/tests/src/org.apache.santuario/xmlsec/4.0.4/gradle.properties new file mode 100644 index 00000000000..5fa2caac9dc --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/gradle.properties @@ -0,0 +1,2 @@ +library.version = 4.0.4 +metadata.dir = org.apache.santuario/xmlsec/4.0.4/ diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/settings.gradle b/tests/src/org.apache.santuario/xmlsec/4.0.4/settings.gradle new file mode 100644 index 00000000000..2556658e444 --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/settings.gradle @@ -0,0 +1,19 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +pluginManagement { + def tckPath = Objects.requireNonNullElse( + System.getenv("GVM_TCK_TCKDIR"), + "../../../../tck-build-logic" + ) + includeBuild(tckPath) +} + +plugins { + id "org.graalvm.internal.tck-settings" version "1.0.0-SNAPSHOT" +} + +rootProject.name = 'org.apache.santuario.xmlsec_tests' diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/src/test/java/org_apache_santuario/xmlsec/XmlsecTest.java b/tests/src/org.apache.santuario/xmlsec/4.0.4/src/test/java/org_apache_santuario/xmlsec/XmlsecTest.java new file mode 100644 index 00000000000..a793ea70333 --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/src/test/java/org_apache_santuario/xmlsec/XmlsecTest.java @@ -0,0 +1,324 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org_apache_santuario.xmlsec; + +import org.apache.xml.security.Init; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; +import org.apache.xml.security.c14n.Canonicalizer; +import org.apache.xml.security.encryption.EncryptedKey; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.keys.KeyInfo; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.transforms.Transforms; +import org.apache.xml.security.utils.Constants; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +class XmlsecTest { + + @BeforeAll + static void initXmlSec() { + // Initialize the XML Security library once for all tests. + Init.init(); + } + + @Test + void signAndVerifyEnvelopedSignatureRsaSha256() throws Exception { + // Create a simple namespaced document + Document doc = newDocument(); + Element root = doc.createElementNS("urn:test", "t:root"); + // Explicitly declare the namespace prefix used in this subtree + root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + root.setAttributeNS("urn:test", "t:attr", "value"); + Element child = doc.createElementNS("urn:test", "t:child"); + child.setTextContent("payload"); + root.appendChild(child); + doc.appendChild(root); + + // Generate RSA key pair for signing + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048, new SecureRandom()); + KeyPair kp = kpg.generateKeyPair(); + + // Create XML Signature with Exclusive C14N (omit comments) + XMLSignature signature = new XMLSignature( + doc, + "", + XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256, + Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS + ); + + // Append the Signature element into the document + root.appendChild(signature.getElement()); + + // Add reference to the whole document with the Enveloped Signature and Exclusive C14N transforms + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + transforms.addTransform(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + signature.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + + // Sign using the private key + signature.sign(kp.getPrivate()); + + // Ensure a Signature element is present + assertThat(root.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature").getLength()).isEqualTo(1); + + // Verify signature using the public key + XMLSignature parsedSignature = new XMLSignature(signature.getElement(), ""); + boolean valid = parsedSignature.checkSignatureValue(kp.getPublic()); + assertThat(valid).isTrue(); + + // Re-serialize and reparse to ensure the signature remains valid across parsing cycles + byte[] xmlBytes = toBytes(doc); + Document reparsed = parseBytes(xmlBytes); + Element sigElem = (Element) reparsed.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature").item(0); + XMLSignature reparsedSig = new XMLSignature(sigElem, ""); + assertThat(reparsedSig.checkSignatureValue(kp.getPublic())).isTrue(); + } + + @Test + void exclusiveCanonicalizationProducesStableOutput() throws Exception { + // Build two semantically equivalent documents with same prefix and different attribute ordering + Document d1 = newDocument(); + Element r1 = d1.createElementNS("urn:test", "t:root"); + r1.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + r1.setAttributeNS("urn:test", "t:z", "3"); + r1.setAttributeNS("urn:test", "t:a", "1"); + Element c1 = d1.createElementNS("urn:test", "t:child"); + c1.setTextContent("text"); + r1.appendChild(c1); + d1.appendChild(r1); + + Document d2 = newDocument(); + Element r2 = d2.createElementNS("urn:test", "t:root"); + r2.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + r2.setAttributeNS("urn:test", "t:a", "1"); + r2.setAttributeNS("urn:test", "t:z", "3"); + Element c2 = d2.createElementNS("urn:test", "t:child"); + c2.setTextContent("text"); + r2.appendChild(c2); + d2.appendChild(r2); + + Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + + ByteArrayOutputStream bos1 = new ByteArrayOutputStream(); + canon.canonicalizeSubtree(r1, bos1); + byte[] out1 = bos1.toByteArray(); + + ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); + canon.canonicalizeSubtree(r2, bos2); + byte[] out2 = bos2.toByteArray(); + + // The canonicalized outputs must match despite different attribute insertion order + assertThat(new String(out1, StandardCharsets.UTF_8)).isEqualTo(new String(out2, StandardCharsets.UTF_8)); + } + + @Test + void encryptAndDecryptElementWithAes128() throws Exception { + // Build a document with a secret element + Document doc = newDocument(); + Element root = doc.createElementNS("urn:test", "t:root"); + // Ensure the 't' prefix is declared so subtree serialization during encryption/decryption is valid + root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + Element secret = doc.createElementNS("urn:test", "t:Secret"); + secret.setTextContent("super-secret"); + Element other = doc.createElementNS("urn:test", "t:Other"); + other.setTextContent("public"); + root.appendChild(secret); + root.appendChild(other); + doc.appendChild(root); + + // Generate an AES-128 key + KeyGenerator kg = KeyGenerator.getInstance("AES"); + kg.init(128, new SecureRandom()); + SecretKey key = kg.generateKey(); + + // Encrypt the Secret element (element encryption) + XMLCipher encCipher = XMLCipher.getInstance(XMLCipher.AES_128); + encCipher.init(XMLCipher.ENCRYPT_MODE, key); + encCipher.doFinal(doc, secret, false); + + // After encryption, there should be no "Secret" element under root + assertThat(root.getElementsByTagNameNS("urn:test", "Secret").getLength()).isEqualTo(0); + // And there should be exactly one EncryptedData element + assertThat(root.getElementsByTagNameNS("http://www.w3.org/2001/04/xmlenc#", "EncryptedData").getLength()).isEqualTo(1); + + // Decrypt back + Element encryptedData = (Element) root.getElementsByTagNameNS("http://www.w3.org/2001/04/xmlenc#", "EncryptedData").item(0); + XMLCipher decCipher = XMLCipher.getInstance(); + decCipher.init(XMLCipher.DECRYPT_MODE, key); + decCipher.doFinal(doc, encryptedData); + + // The Secret element should be restored with original content + Element restored = (Element) root.getElementsByTagNameNS("urn:test", "Secret").item(0); + assertThat(restored).isNotNull(); + assertThat(restored.getTextContent()).isEqualTo("super-secret"); + // The 'Other' element remains unchanged + assertThat(((Element) root.getElementsByTagNameNS("urn:test", "Other").item(0)).getTextContent()).isEqualTo("public"); + } + + @Test + void hmacSha256SignatureOnElementById_isValidAndDetectsTampering() throws Exception { + // Build a document with a target element identified by ID + Document doc = newDocument(); + Element root = doc.createElementNS("urn:test", "t:root"); + root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + + Element data = doc.createElementNS("urn:test", "t:Data"); + data.setTextContent("important"); + data.setAttribute("Id", "elem-1"); + // Mark the attribute as type ID so same-document URI can resolve + data.setIdAttribute("Id", true); + + Element other = doc.createElementNS("urn:test", "t:Other"); + other.setTextContent("not-signed"); + + root.appendChild(data); + root.appendChild(other); + doc.appendChild(root); + + // Create an HMAC-SHA256 XML Signature (detached same-document reference) + XMLSignature signature = new XMLSignature( + doc, + "", + XMLSignature.ALGO_ID_MAC_HMAC_SHA256, + Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS + ); + root.appendChild(signature.getElement()); + + // Reference only the Data element via its ID and canonicalize it + Transforms transforms = new Transforms(doc); + transforms.addTransform(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + signature.addDocument("#elem-1", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + + // Generate an HMAC key and sign + SecretKey hmacKey = KeyGenerator.getInstance("HmacSHA256").generateKey(); + signature.sign(hmacKey); + + // Verify signature is valid with the same HMAC key + XMLSignature parsedSignature = new XMLSignature(signature.getElement(), ""); + assertThat(parsedSignature.checkSignatureValue(hmacKey)).isTrue(); + + // Tamper with the signed element content; verification must fail now + data.setTextContent("tampered"); + XMLSignature tamperedSig = new XMLSignature(signature.getElement(), ""); + assertThat(tamperedSig.checkSignatureValue(hmacKey)).isFalse(); + } + + @Test + void encryptElementWithAes128AndWrapKeyWithRsaOaep() throws Exception { + // Build a document that contains data to encrypt + Document doc = newDocument(); + Element root = doc.createElementNS("urn:test", "t:root"); + root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:t", "urn:test"); + Element sensitive = doc.createElementNS("urn:test", "t:Sensitive"); + sensitive.setTextContent("classified"); + Element publicElem = doc.createElementNS("urn:test", "t:Public"); + publicElem.setTextContent("public-info"); + root.appendChild(sensitive); + root.appendChild(publicElem); + doc.appendChild(root); + + // Generate RSA key pair for key transport (wrapping AES key) + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048, new SecureRandom()); + KeyPair rsaKp = kpg.generateKeyPair(); + + // Generate a random AES-128 content encryption key + SecretKey aesKey = KeyGenerator.getInstance("AES").generateKey(); + + // Prepare data cipher for element encryption + XMLCipher dataCipher = XMLCipher.getInstance(XMLCipher.AES_128); + dataCipher.init(XMLCipher.ENCRYPT_MODE, aesKey); + + // Wrap AES key using RSA-OAEP and place it into EncryptedData/KeyInfo + XMLCipher keyCipher = XMLCipher.getInstance(XMLCipher.RSA_OAEP); + keyCipher.init(XMLCipher.WRAP_MODE, rsaKp.getPublic()); + EncryptedKey encryptedKey = keyCipher.encryptKey(doc, aesKey); + + KeyInfo ki = new KeyInfo(doc); + ki.add(encryptedKey); + dataCipher.getEncryptedData().setKeyInfo(ki); + + // Encrypt the element (element encryption, not content-only) + dataCipher.doFinal(doc, sensitive, false); + + // Assertions after encryption + assertThat(root.getElementsByTagNameNS("urn:test", "Sensitive").getLength()).isEqualTo(0); + Element encryptedData = (Element) root.getElementsByTagNameNS("http://www.w3.org/2001/04/xmlenc#", "EncryptedData").item(0); + assertThat(encryptedData).isNotNull(); + // Ensure an EncryptedKey is embedded via KeyInfo + assertThat(encryptedData.getElementsByTagNameNS("http://www.w3.org/2001/04/xmlenc#", "EncryptedKey").getLength()).isEqualTo(1); + + // Decrypt using the private RSA key to unwrap the AES key + XMLCipher decryptCipher = XMLCipher.getInstance(); + decryptCipher.init(XMLCipher.DECRYPT_MODE, null); + decryptCipher.setKEK(rsaKp.getPrivate()); + decryptCipher.doFinal(doc, encryptedData); + + // After decryption, original element and content must be restored + Element restored = (Element) root.getElementsByTagNameNS("urn:test", "Sensitive").item(0); + assertThat(restored).isNotNull(); + assertThat(restored.getTextContent()).isEqualTo("classified"); + // Unrelated element remains unchanged + assertThat(((Element) root.getElementsByTagNameNS("urn:test", "Public").item(0)).getTextContent()).isEqualTo("public-info"); + } + + // Utility methods + + private static Document newDocument() throws Exception { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + // Harden the parser a bit for safety in tests + try { + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (Exception ignored) { + // If not supported by the JAXP implementation, ignore for tests + } + DocumentBuilder db = dbf.newDocumentBuilder(); + return db.newDocument(); + } + + private static byte[] toBytes(Document doc) throws Exception { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer t = tf.newTransformer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.transform(new DOMSource(doc), new StreamResult(baos)); + return baos.toByteArray(); + } + + private static Document parseBytes(byte[] data) throws Exception { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + try { + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (Exception ignored) { + } + DocumentBuilder db = dbf.newDocumentBuilder(); + return db.parse(new ByteArrayInputStream(data)); + } +} diff --git a/tests/src/org.apache.santuario/xmlsec/4.0.4/user-code-filter.json b/tests/src/org.apache.santuario/xmlsec/4.0.4/user-code-filter.json new file mode 100644 index 00000000000..588fa5ff149 --- /dev/null +++ b/tests/src/org.apache.santuario/xmlsec/4.0.4/user-code-filter.json @@ -0,0 +1,10 @@ +{ + "rules" : [ + { + "excludeClasses" : "**" + }, + { + "includeClasses" : "org.apache.**" + } + ] +} \ No newline at end of file diff --git a/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle b/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle index 35e7512003c..0237c9d675d 100644 --- a/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle +++ b/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle @@ -50,6 +50,15 @@ def writeGithubOutput(String key, String value) { } } +def writeGithubEnv(String key, String value) { + def path = System.getenv("GITHUB_ENV") + if (path == null || path.trim().isEmpty()) { + println "${key}=${value}" + } else { + new File(path).append("${key}=${value}\n") + } +} + String coordinateFilter = Objects.requireNonNullElse(project.findProperty("coordinates"), "") @@ -64,31 +73,31 @@ if (CoordinateUtils.isFractionalBatch(coordinateFilter)) { matchingCoordinates = matchingCoordinates.findAll { !it.startsWith("samples:") } - // gradle checkstyle -Pcoordinates= +// gradle checkstyle -Pcoordinates= tasks.register("checkstyle", CheckstyleInvocationTask.class) { task -> task.setDescription("Runs checkstyle on all subprojects") task.setGroup(JavaBasePlugin.VERIFICATION_GROUP) } - // gradle compileTestJava -Pcoordinates= +// gradle compileTestJava -Pcoordinates= tasks.register("compileTestJava", CompileTestJavaInvocationTask.class) { task -> task.setDescription("Compiles sources (javac) for all subprojects") task.setGroup(LifecycleBasePlugin.BUILD_GROUP) } - // gradle javaTest -Pcoordinates= +// gradle javaTest -Pcoordinates= tasks.register("javaTest", JavaTestInvocationTask.class) { task -> task.setDescription("Runs JVM tests (Gradle 'test') for all subprojects") task.setGroup(JavaBasePlugin.VERIFICATION_GROUP) } - // gradle nativeTestCompile -Pcoordinates= +// gradle nativeTestCompile -Pcoordinates= tasks.register("nativeTestCompile", NativeTestCompileInvocationTask.class) { task -> task.setDescription("Compiles native tests (nativeTestCompile) for all subprojects") task.setGroup(LifecycleBasePlugin.BUILD_GROUP) } - // gradle clean -Pcoordinates= +// gradle clean -Pcoordinates= if (tasks.findByName("clean") == null) { tasks.register("clean", CleanInvocationTask.class) { t -> t.setDescription("Cleans all matching subprojects") @@ -102,7 +111,7 @@ if (tasks.findByName("clean") == null) { tasks.named("clean").configure { it.dependsOn("tckClean") } } - // gradle test -Pcoordinates= +// gradle test -Pcoordinates= if (tasks.findByName("test") == null) { tasks.register("test", TestInvocationTask.class) { t -> t.setDescription("Runs Native Image tests for all matching coordinates") @@ -169,8 +178,8 @@ Map> matrixDefaultsFor(String section) { def sec = ci[section] if (sec == null) throw new GradleException("Missing '${section}' in ci.json") [ - "version": (sec["java"] as List), - "os": (sec["os"] as List) + "version": (sec["java"] as List), + "os": (sec["os"] as List) ] } @@ -187,7 +196,7 @@ Provider generateMatrixMatchingCoordinates = tasks.register("generateMatri } } - // gradle generateMatrixBatchedCoordinates [-Pbatches=] +// gradle generateMatrixBatchedCoordinates [-Pbatches=] Provider generateMatrixBatchedCoordinates = tasks.register("generateMatrixBatchedCoordinates", DefaultTask) { task -> task.setDescription("Returns matrix definition populated with fractional batch coordinates (k/n)") task.setGroup(METADATA_GROUP) @@ -290,7 +299,7 @@ Provider generateInfrastructureChangedCoordinatesMatrix = tasks.register(" } def matrix = [ - "coordinates": selected + "coordinates": selected ] matrix.putAll(matrixDefaultsFor("generateInfrastructureChangedCoordinatesMatrix")) @@ -362,3 +371,71 @@ tasks.register("addLibraryAsLatestMetadataIndexJson", DefaultTask) { task -> MetadataGenerationUtils.makeVersionLatestInIndexJson(project.layout, coords, testVersion) } } + +// gradle extractLibraryTestParams -Pcoordinates= +tasks.register("extractLibraryTestParams", DefaultTask) { task -> + task.setDescription("Extracts TEST_PATH, LATEST_VERSION and TEST_COORDINATES for given coordinates (group:artifact[:version]). Writes them into GITHUB_ENV.") + task.setGroup(METADATA_GROUP) + task.doFirst { + if (!project.hasProperty("coordinates")) { + throw new GradleException("Missing 'coordinates' property! Rerun Gradle with -Pcoordinates=") + } + def coordsStr = project.findProperty("coordinates").toString() + def parts = coordsStr.split(":") + if (parts.length < 2) { + throw new GradleException("Invalid 'coordinates' value; expected group:artifact or group:artifact:version, got: ${coordsStr}") + } + String lib = parts[0] + ":" + parts[1] + + String libPath = lib.replace(":", "/") + File indexFile = project.file("metadata/${libPath}/index.json") + + Map latestEntry = null + if (indexFile.exists()) { + def entries = new JsonSlurper().parse(indexFile) as List + latestEntry = entries.find { e -> Boolean.TRUE.equals(e["latest"]) } + if (latestEntry == null && !entries.isEmpty()) { + latestEntry = entries.max { e -> VersionNumber.parse((e["metadata-version"] ?: "").toString()) } + } + } + + String latestVersion = null + String testVersionFolder = null + + if (latestEntry != null) { + latestVersion = (latestEntry["metadata-version"] ?: "").toString() + def tv = latestEntry["test-version"] + testVersionFolder = (tv != null && tv.toString().trim()) ? tv.toString() : latestVersion + } else { + if (!indexFile.exists()) { + throw new GradleException("Missing index.json at ${indexFile}. Cannot determine latest metadata version.") + } + def entries = new JsonSlurper().parse(indexFile) as List + if (entries.isEmpty()) { + throw new GradleException("Empty index.json at ${indexFile}.") + } + latestVersion = entries.collect { it["metadata-version"].toString() } + .max { v -> VersionNumber.parse(v) } + } + + if (testVersionFolder == null || testVersionFolder.trim().isEmpty()) { + File testsLibDir = project.file("tests/src/${libPath}") + if (!testsLibDir.exists()) { + throw new GradleException("No tests directory found for ${lib} at ${testsLibDir}") + } + File[] dirs = testsLibDir.listFiles() + List versionDirs = dirs == null ? [] : dirs.findAll { it.isDirectory() } + if (versionDirs.isEmpty()) { + throw new GradleException("No version subdirectories found in ${testsLibDir}") + } + testVersionFolder = versionDirs.max { d -> VersionNumber.parse(d.name) }.name + } + + String testPath = "${libPath}/${testVersionFolder}" + String testCoordinates = testPath.replace("/", ":") + + writeGithubEnv("LATEST_VERSION", latestVersion) + writeGithubEnv("TEST_PATH", testPath) + writeGithubEnv("TEST_COORDINATES", testCoordinates) + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java index 0ac5c2fe9b6..57d5abd0064 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java @@ -139,11 +139,13 @@ public Path getTestDir(String coordinates) { } catch (Exception ignored) { // Fall through to conventional lookup } + // Fallback: conventional layout tests/src/// Path conventional = testRoot().resolve(groupId).resolve(artifactId).resolve(version); if (Files.isDirectory(conventional)) { return conventional; } + throw new RuntimeException("Missing test-directory for coordinates `" + coordinates + "`"); } diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.groovy deleted file mode 100644 index 89c6015baa7..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.groovy +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import groovy.transform.Internal -import org.graalvm.internal.tck.harness.TckExtension -import org.gradle.api.DefaultTask -import org.gradle.api.GradleException -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction -import org.gradle.process.ExecOperations -import org.gradle.process.ExecSpec - -import javax.inject.Inject -import java.nio.charset.StandardCharsets -import java.nio.file.Path -import java.util.stream.Collectors -import static org.graalvm.internal.tck.Utils.splitCoordinates; -import static org.graalvm.internal.tck.Utils.readIndexFile; -import static org.graalvm.internal.tck.Utils.coordinatesMatch; - -import static groovy.io.FileType.FILES - -/** - * Abstract task that is used to invoke test subprojects. - */ -@SuppressWarnings("unused") -abstract class AbstractSubprojectTask extends DefaultTask { - - protected final TckExtension tckExtension - private final String coordinates - - @Inject - abstract ExecOperations getExecOperations() - - @Input - abstract List getCommand(); - - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - final Set getInputFiles() { - def inputFiles = project.objects.fileCollection() - def metadataDir = tckExtension.getMetadataDir(coordinates) - Path testDir = tckExtension.getTestDir(coordinates) - inputFiles.from(project.files(tckExtension.getMetadataFileList(metadataDir))) - def io = inputsFor(testDir) - def result = inputFiles.from(io).files - result - } - - @OutputFile - final File getOutputFile() { - String hash = command.join(",").md5() - def file = project.layout.buildDirectory.file("tests/${coordinates}/${hash}.out").get().asFile - file - } - - @Inject - AbstractSubprojectTask(String coordinates) { - this.tckExtension = project.extensions.findByType(TckExtension) - this.coordinates = coordinates - } - - - protected final configureSpec(ExecSpec spec) { - - def (String groupId, String artifactId, String version) = splitCoordinates(coordinates) - Path metadataDir = tckExtension.getMetadataDir(coordinates) - boolean override = false - - def metadataIndex = readIndexFile(metadataDir.parent) - for (def entry in metadataIndex) { - if (coordinatesMatch((String) entry["module"], groupId, artifactId) && ((List) entry["tested-versions"]).contains(version)) { - if (entry.containsKey("override")) { - override |= entry["override"] as boolean - } - break - } - } - - Path testDir = tckExtension.getTestDir(coordinates) - - Map env = new HashMap<>(System.getenv()) - // Environment variables for setting up TCK - env.put("GVM_TCK_LC", coordinates) - env.put("GVM_TCK_EXCLUDE", override.toString()) - if (System.getenv("GVM_TCK_LV") == null) { - // we only set this env variable if user didn't specify it manually - env.put("GVM_TCK_LV", version) - } - env.put("GVM_TCK_MD", metadataDir.toAbsolutePath().toString()) - env.put("GVM_TCK_TCKDIR", tckExtension.getTckRoot().get().getAsFile().toPath().toAbsolutePath().toString()) - spec.environment(env) - spec.commandLine(getCommand()) - spec.workingDir(testDir.toAbsolutePath().toFile()) - - spec.setIgnoreExitValue(true) - spec.standardOutput = System.out - spec.errorOutput = System.err - } - - /** - * Given project dir returns a tuple that contains a list of inputs. - * @return lists of input files - */ - @Internal - def inputsFor(Path projectDir) { - File dir = projectDir.toFile() - def excludedSubdirNames = [".gradle", ".mvn"] - - List excludedSubdirs = excludedSubdirNames.stream() - .map(name -> projectDir.resolve(name).toFile().getCanonicalPath() + File.separator) - .collect(Collectors.toList()) - - - def inputFiles = [] - - dir.traverse(type: FILES) { File file -> - if (excludedSubdirs.stream().noneMatch(curr -> file.getCanonicalPath().startsWith(curr))) { - inputFiles.add(file.toPath()) - } - } - - return Collections.unmodifiableList(inputFiles) - } - - protected void beforeExecute() { - // do nothing - } - - protected void afterExecute() { - // do nothing - } - - protected String getErrorMessage(int exitCode) { - "Execution of " + getCommand() + " failed." - } - - @TaskAction - final void execute() { - beforeExecute() - println "Command: $command" - def out = new ByteArrayOutputStream() - def err = new ByteArrayOutputStream() - def execResult = execOperations.exec { spec -> - configureSpec(spec) - spec.standardOutput = new TeeOutputStream(out, System.out) - spec.errorOutput = new TeeOutputStream(err, System.err) - } - outputFile.parentFile.mkdirs() - outputFile.text = """Standard out ------ -${out.toString(StandardCharsets.UTF_8)} ------ -Standard err ----- -${err.toString(StandardCharsets.UTF_8)} ----- -""" - def exitCode = execResult.exitValue - if (exitCode != 0) { - throw new GradleException(getErrorMessage(exitCode)) - } - afterExecute() - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.java new file mode 100644 index 00000000000..a234291ce13 --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AbstractSubprojectTask.java @@ -0,0 +1,230 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecOperations; +import org.gradle.process.ExecSpec; +import org.graalvm.internal.tck.harness.TckExtension; + +import javax.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.*; +import java.util.stream.Collectors; + +import static org.graalvm.internal.tck.Utils.coordinatesMatch; +import static org.graalvm.internal.tck.Utils.readIndexFile; +import static org.graalvm.internal.tck.Utils.splitCoordinates; + +/** + * Abstract task that is used to invoke test subprojects. + */ +@SuppressWarnings("unused") +public abstract class AbstractSubprojectTask extends DefaultTask { + + protected final TckExtension tckExtension; + private final String coordinates; + + @Inject + public abstract ExecOperations getExecOperations(); + + @Input + public abstract List getCommand(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public final Set getInputFiles() { + var inputFiles = getProject().getObjects().fileCollection(); + Path metadataDir = tckExtension.getMetadataDir(coordinates); + Path testDir = tckExtension.getTestDir(coordinates); + try { + inputFiles.from(getProject().files(tckExtension.getMetadataFileList(metadataDir))); + } catch (IOException e) { + throw new GradleException("Failed to list metadata files for " + metadataDir, e); + } + List io = inputsFor(testDir); + inputFiles.from(getProject().files(io)); + return inputFiles.getFiles(); + } + + @OutputFile + public final File getOutputFile() { + String hash = md5(String.join(",", getCommand())); + return getProject().getLayout().getBuildDirectory() + .file("tests/" + coordinates + "/" + hash + ".out") + .get().getAsFile(); + } + + @Inject + public AbstractSubprojectTask(String coordinates) { + this.tckExtension = getProject().getExtensions().findByType(TckExtension.class); + this.coordinates = coordinates; + } + + protected final void configureSpec(ExecSpec spec) { + List parts = splitCoordinates(coordinates); + String groupId = parts.get(0); + String artifactId = parts.get(1); + String version = parts.get(2); + Path metadataDir = tckExtension.getMetadataDir(coordinates); + boolean override = false; + + var metadataIndex = readIndexFile(metadataDir.getParent()); + for (Object entryObj : (Iterable) metadataIndex) { + @SuppressWarnings("unchecked") + Map entry = (Map) entryObj; + if (coordinatesMatch((String) entry.get("module"), groupId, artifactId) && + ((List) entry.get("tested-versions")).contains(version)) { + if (entry.containsKey("override")) { + Object ov = entry.get("override"); + if (ov instanceof Boolean b) { + override |= b; + } else if (ov != null) { + override |= Boolean.parseBoolean(ov.toString()); + } + } + break; + } + } + + Path testDir = tckExtension.getTestDir(coordinates); + + Map env = new HashMap<>(System.getenv()); + // Environment variables for setting up TCK + env.put("GVM_TCK_LC", coordinates); + env.put("GVM_TCK_EXCLUDE", Boolean.toString(override)); + if (System.getenv("GVM_TCK_LV") == null) { + // we only set this env variable if user didn't specify it manually + env.put("GVM_TCK_LV", version); + } + env.put("GVM_TCK_MD", metadataDir.toAbsolutePath().toString()); + env.put("GVM_TCK_TCKDIR", tckExtension.getTckRoot().get().getAsFile().toPath().toAbsolutePath().toString()); + spec.environment(env); + spec.commandLine(getCommand()); + spec.workingDir(testDir.toAbsolutePath().toFile()); + + spec.setIgnoreExitValue(true); + spec.setStandardOutput(System.out); + spec.setErrorOutput(System.err); + } + + /** + * Given project dir returns a list of inputs. + * @return list of input files + */ + @Internal + protected List inputsFor(Path projectDir) { + List excludedSubdirNames = List.of(".gradle", ".mvn"); + List excludedSubdirs = excludedSubdirNames.stream() + .map(name -> { + try { + return projectDir.resolve(name).toFile().getCanonicalPath() + File.separator; + } catch (IOException e) { + return projectDir.resolve(name).toFile().getAbsolutePath() + File.separator; + } + }) + .collect(Collectors.toList()); + + List inputFiles = new ArrayList<>(); + try { + Files.walk(projectDir) + .filter(Files::isRegularFile) + .filter(p -> { + try { + String canon = p.toFile().getCanonicalPath(); + return excludedSubdirs.stream().noneMatch(canon::startsWith); + } catch (IOException e) { + return false; + } + }) + .forEach(inputFiles::add); + } catch (IOException e) { + // If traversal fails treat as no inputs to avoid breaking configuration phase + return Collections.emptyList(); + } + + return Collections.unmodifiableList(inputFiles); + } + + protected void beforeExecute() { + // do nothing + } + + protected void afterExecute() { + // do nothing + } + + protected String getErrorMessage(int exitCode) { + return "Execution of " + getCommand() + " failed."; + } + + @TaskAction + public final void executeTask() { + beforeExecute(); + getLogger().lifecycle("Command: " + getCommand()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + var execResult = getExecOperations().exec(spec -> { + configureSpec(spec); + spec.setStandardOutput(new TeeOutputStream(out, System.out)); + spec.setErrorOutput(new TeeOutputStream(err, System.err)); + }); + File of = getOutputFile(); + File parent = of.getParentFile(); + if (parent != null && !parent.exists()) { + //noinspection ResultOfMethodCallIgnored + parent.mkdirs(); + } + String content = "Standard out\n" + + "-----\n" + + out.toString(StandardCharsets.UTF_8) + + "\n-----\n" + + "Standard err\n" + + "----\n" + + err.toString(StandardCharsets.UTF_8) + + "\n----\n"; + try { + Files.writeString(of.toPath(), content, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new GradleException("Failed to write test output to " + of, e); + } + int exitCode = execResult.getExitValue(); + if (exitCode != 0) { + throw new GradleException(getErrorMessage(exitCode)); + } + afterExecute(); + } + + private static String md5(String s) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] dig = md.digest(s.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(dig.length * 2); + for (byte b : dig) { + sb.append(Character.forDigit((b & 0xF0) >> 4, 16)); + sb.append(Character.forDigit(b & 0x0F, 16)); + } + return sb.toString(); + } catch (Exception e) { + return Integer.toHexString(s.hashCode()); + } + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.groovy deleted file mode 100644 index dd5079bd1ca..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.groovy +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.GradleException -import org.gradle.api.tasks.TaskAction -import org.gradle.process.ExecOperations -import org.gradle.process.ExecSpec -import org.graalvm.internal.tck.harness.TckExtension - -import javax.inject.Inject -import java.nio.charset.StandardCharsets -import java.nio.file.Path - -import static org.graalvm.internal.tck.Utils.coordinatesMatch -import static org.graalvm.internal.tck.Utils.readIndexFile -import static org.graalvm.internal.tck.Utils.splitCoordinates - -/** - * Base task that resolves coordinates (via CoordinatesAwareTask) and executes a command for each coordinate. - * Subclasses implement commandFor(String coordinates) and may override hooks for logging. - */ -abstract class AllCoordinatesExecTask extends CoordinatesAwareTask { - - @Inject - abstract ExecOperations getExecOperations() - - - /** - * Subclasses must return the command line to run for the given coordinates. - */ - abstract List commandFor(String coordinates) - - /** - * Customize error message. - */ - protected String errorMessageFor(String coordinates, int exitCode) { - return "Execution failed for ${coordinates} with exit code ${exitCode}" - } - - /** - * Hook invoked before executing each coordinate. - */ - protected void beforeEach(String coordinates, List command) { - // no-op - } - - /** - * Hook invoked after executing each coordinate on success. - */ - protected void afterEach(String coordinates) { - // no-op - } - - @TaskAction - final void runAll() { - List coords = resolveCoordinates() - if (coords.isEmpty()) { - getLogger().lifecycle("No matching coordinates found. Nothing to do.") - return - } - for (String c : coords) { - runSingle(c) - } - } - - private void runSingle(String coordinates) { - List command = commandFor(coordinates) - beforeEach(coordinates, command) - - def out = new ByteArrayOutputStream() - def err = new ByteArrayOutputStream() - - def execResult = getExecOperations().exec { ExecSpec spec -> - this.configureSpec(spec, coordinates, command) - spec.standardOutput = new TeeOutputStream(out, System.out) - spec.errorOutput = new TeeOutputStream(err, System.err) - } - - // write output file like AbstractSubprojectTask - String hash = command.join(",").md5() - File outputFile = project.layout.buildDirectory.file("tests/${coordinates}/${hash}.out").get().asFile - outputFile.parentFile.mkdirs() - outputFile.text = """Standard out ------ -${out.toString(StandardCharsets.UTF_8)} ------ -Standard err ----- -${err.toString(StandardCharsets.UTF_8)} ----- -""" - - int exitCode = execResult.exitValue - if (exitCode != 0) { - throw new GradleException(errorMessageFor(coordinates, exitCode)) - } - afterEach(coordinates) - } - - protected void configureSpec(ExecSpec spec, String coordinates, List command) { - def (String groupId, String artifactId, String version) = splitCoordinates(coordinates) - Path metadataDir = tckExtension.getMetadataDir(coordinates) - boolean override = false - - def metadataIndex = readIndexFile(metadataDir.parent) - for (def entry in metadataIndex) { - if (coordinatesMatch((String) entry["module"], groupId, artifactId) && ((List) entry["tested-versions"]).contains(version)) { - if (entry.containsKey("override")) { - override |= entry["override"] as boolean - } - break - } - } - - Path testDir = tckExtension.getTestDir(coordinates) - - Map env = new HashMap<>(System.getenv()) - env.put("GVM_TCK_LC", coordinates) - env.put("GVM_TCK_EXCLUDE", override.toString()) - if (System.getenv("GVM_TCK_LV") == null) { - env.put("GVM_TCK_LV", version) - } - env.put("GVM_TCK_MD", metadataDir.toAbsolutePath().toString()) - env.put("GVM_TCK_TCKDIR", tckExtension.getTckRoot().get().getAsFile().toPath().toAbsolutePath().toString()) - - spec.environment(env) - spec.commandLine(command) - spec.workingDir(testDir.toAbsolutePath().toFile()) - spec.setIgnoreExitValue(true) - spec.standardOutput = System.out - spec.errorOutput = System.err - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.java new file mode 100644 index 00000000000..458c3db3caf --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/AllCoordinatesExecTask.java @@ -0,0 +1,179 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecOperations; +import org.gradle.process.ExecSpec; + +import javax.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.graalvm.internal.tck.Utils.coordinatesMatch; +import static org.graalvm.internal.tck.Utils.readIndexFile; +import static org.graalvm.internal.tck.Utils.splitCoordinates; + +/** + * Base task that resolves coordinates (via CoordinatesAwareTask) and executes a command for each coordinate. + * Subclasses implement commandFor(String coordinates) and may override hooks for logging. + */ +@SuppressWarnings("unused") +public abstract class AllCoordinatesExecTask extends CoordinatesAwareTask { + + @Inject + public abstract ExecOperations getExecOperations(); + + /** + * Subclasses must return the command line to run for the given coordinates. + */ + public abstract List commandFor(String coordinates); + + /** + * Customize error message. + */ + protected String errorMessageFor(String coordinates, int exitCode) { + return "Execution failed for " + coordinates + " with exit code " + exitCode; + } + + /** + * Hook invoked before executing each coordinate. + */ + protected void beforeEach(String coordinates, List command) { + // no-op + } + + /** + * Hook invoked after executing each coordinate on success. + */ + protected void afterEach(String coordinates) { + // no-op + } + + @TaskAction + public final void runAll() { + List coords = resolveCoordinates(); + if (coords.isEmpty()) { + getLogger().lifecycle("No matching coordinates found. Nothing to do."); + return; + } + for (String c : coords) { + runSingle(c); + } + } + + private void runSingle(String coordinates) { + List command = commandFor(coordinates); + beforeEach(coordinates, command); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + + var execResult = getExecOperations().exec((ExecSpec spec) -> { + this.configureSpec(spec, coordinates, command); + spec.setStandardOutput(new TeeOutputStream(out, System.out)); + spec.setErrorOutput(new TeeOutputStream(err, System.err)); + }); + + // write output file like AbstractSubprojectTask + String hash = md5(String.join(",", command)); + File outputFile = getProject().getLayout().getBuildDirectory().file("tests/" + coordinates + "/" + hash + ".out").get().getAsFile(); + File parent = outputFile.getParentFile(); + if (parent != null && !parent.exists()) { + //noinspection ResultOfMethodCallIgnored + parent.mkdirs(); + } + String content = "Standard out\n" + + "-----\n" + + out.toString(StandardCharsets.UTF_8) + + "\n-----\n" + + "Standard err\n" + + "----\n" + + err.toString(StandardCharsets.UTF_8) + + "\n----\n"; + try { + Files.writeString(outputFile.toPath(), content, StandardCharsets.UTF_8); + } catch (java.io.IOException e) { + throw new GradleException("Failed to write test output to " + outputFile, e); + } + + int exitCode = execResult.getExitValue(); + if (exitCode != 0) { + throw new GradleException(errorMessageFor(coordinates, exitCode)); + } + afterEach(coordinates); + } + + protected void configureSpec(ExecSpec spec, String coordinates, List command) { + List parts = splitCoordinates(coordinates); + String groupId = parts.get(0); + String artifactId = parts.get(1); + String version = parts.get(2); + Path metadataDir = tckExtension.getMetadataDir(coordinates); + boolean override = false; + + var metadataIndex = readIndexFile(metadataDir.getParent()); + for (Object entryObj : (Iterable) metadataIndex) { + @SuppressWarnings("unchecked") + Map entry = (Map) entryObj; + if (coordinatesMatch((String) entry.get("module"), groupId, artifactId) && + ((List) entry.get("tested-versions")).contains(version)) { + if (entry.containsKey("override")) { + Object ov = entry.get("override"); + if (ov instanceof Boolean b) { + override |= b; + } else if (ov != null) { + override |= Boolean.parseBoolean(ov.toString()); + } + } + break; + } + } + + Path testDir = tckExtension.getTestDir(coordinates); + + Map env = new HashMap<>(System.getenv()); + env.put("GVM_TCK_LC", coordinates); + env.put("GVM_TCK_EXCLUDE", Boolean.toString(override)); + if (System.getenv("GVM_TCK_LV") == null) { + env.put("GVM_TCK_LV", version); + } + env.put("GVM_TCK_MD", metadataDir.toAbsolutePath().toString()); + env.put("GVM_TCK_TCKDIR", tckExtension.getTckRoot().get().getAsFile().toPath().toAbsolutePath().toString()); + + spec.environment(env); + spec.commandLine(command); + spec.workingDir(testDir.toAbsolutePath().toFile()); + spec.setIgnoreExitValue(true); + spec.setStandardOutput(System.out); + spec.setErrorOutput(System.err); + } + + private static String md5(String s) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] dig = md.digest(s.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(dig.length * 2); + for (byte b : dig) { + sb.append(Character.forDigit((b & 0xF0) >> 4, 16)); + sb.append(Character.forDigit(b & 0x0F, 16)); + } + return sb.toString(); + } catch (Exception e) { + // Fallback to hex of hashCode to avoid failing the build because of MD5 unavailability + return Integer.toHexString(s.hashCode()); + } + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.groovy deleted file mode 100644 index 3e4d13a67d2..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.GradleException -import org.gradle.api.tasks.TaskAction -import org.graalvm.internal.tck.MetadataFilesCheckerTask - -/** - * Executes MetadataFilesCheckerTask for all matching coordinates resolved via -Pcoordinates. - * This unifies handling so the task itself performs coordinate resolution and iteration. - */ -@SuppressWarnings('unused') -abstract class CheckMetadataFilesAllTask extends CoordinatesAwareTask { - - @TaskAction - void runAll() { - List coords = resolveCoordinates() - if (coords.isEmpty()) { - getLogger().lifecycle("No matching coordinates found for metadata checks. Nothing to do.") - return - } - - List failures = [] - coords.each { c -> - if (c.startsWith("samples:") || c.startsWith("org.example:")) { - return // skip samples/infrastructure - } - String tmpName = ("checkMetadataFiles_" + c.replace(":", "_") + "_" + System.nanoTime()) - def t = project.tasks.create(tmpName, MetadataFilesCheckerTask.class) - t.setCoordinates(c) - try { - t.run() - getLogger().lifecycle("Metadata files check passed for {}", c) - } catch (Throwable ex) { - failures.add("${c}: ${ex.message}") - getLogger().error("Metadata files check failed for {}: {}", c, ex.message) - } finally { - // Best effort cleanup to avoid cluttering the task graph - t.enabled = false - } - } - - if (!failures.isEmpty()) { - String msg = "Metadata files check failed for the following coordinates:\n - " + failures.join("\n - ") - throw new GradleException(msg) - } - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.java new file mode 100644 index 00000000000..94d1550793e --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckMetadataFilesAllTask.java @@ -0,0 +1,56 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; +import org.graalvm.internal.tck.MetadataFilesCheckerTask; + +import java.util.ArrayList; +import java.util.List; + +/** + * Executes MetadataFilesCheckerTask for all matching coordinates resolved via -Pcoordinates. + * This unifies handling so the task itself performs coordinate resolution and iteration. + */ +@SuppressWarnings("unused") +public abstract class CheckMetadataFilesAllTask extends CoordinatesAwareTask { + + @TaskAction + public void runAll() { + List coords = resolveCoordinates(); + if (coords.isEmpty()) { + getLogger().lifecycle("No matching coordinates found for metadata checks. Nothing to do."); + return; + } + + List failures = new ArrayList<>(); + for (String c : coords) { + if (c.startsWith("samples:") || c.startsWith("org.example:")) { + continue; // skip samples/infrastructure + } + String tmpName = "checkMetadataFiles_" + c.replace(":", "_") + "_" + System.nanoTime(); + MetadataFilesCheckerTask t = getProject().getTasks().create(tmpName, MetadataFilesCheckerTask.class); + t.setCoordinates(c); + try { + t.run(); + getLogger().lifecycle("Metadata files check passed for {}", c); + } catch (Throwable ex) { + failures.add(c + ": " + ex.getMessage()); + getLogger().error("Metadata files check failed for {}: {}", c, ex.getMessage()); + } finally { + // Best effort cleanup to avoid cluttering the task graph + t.setEnabled(false); + } + } + + if (!failures.isEmpty()) { + String msg = "Metadata files check failed for the following coordinates:\n - " + String.join("\n - ", failures); + throw new GradleException(msg); + } + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.groovy deleted file mode 100644 index 729bb85216e..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.tasks.Input - -import javax.inject.Inject -/** - * Task that is used to run checkstyle task on subprojects. - */ -@SuppressWarnings("unused") -abstract class CheckstyleInvocationTask extends AllCoordinatesExecTask { - - - List commandFor(String coordinates) { - return [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "checkstyle"] - } - - @Override - protected String errorMessageFor(String coordinates, int exitCode) { - "Checkstyle failed" - } - -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.java new file mode 100644 index 00000000000..ae47bb80205 --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CheckstyleInvocationTask.java @@ -0,0 +1,29 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import java.util.List; + +/** + * Task that is used to run checkstyle task on subprojects. + */ +@SuppressWarnings("unused") +public abstract class CheckstyleInvocationTask extends AllCoordinatesExecTask { + + @Override + public List commandFor(String coordinates) { + return List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "checkstyle" + ); + } + + @Override + protected String errorMessageFor(String coordinates, int exitCode) { + return "Checkstyle failed"; + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.groovy deleted file mode 100644 index 398a107f12a..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.tasks.Input - -import javax.inject.Inject - -@SuppressWarnings('unused') -abstract class CleanInvocationTask extends AllCoordinatesExecTask { - - - @Override - List commandFor(String coordinates) { - return [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "clean"] - } - - @Override - protected String errorMessageFor(String coordinates, int exitCode) { - "Clean task failed" - } - -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.java new file mode 100644 index 00000000000..dfac65cd1d9 --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CleanInvocationTask.java @@ -0,0 +1,26 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import java.util.List; + +@SuppressWarnings("unused") +public abstract class CleanInvocationTask extends AllCoordinatesExecTask { + + @Override + public List commandFor(String coordinates) { + return List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "clean" + ); + } + + @Override + protected String errorMessageFor(String coordinates, int exitCode) { + return "Clean task failed"; + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.groovy deleted file mode 100644 index 4f6c04a554e..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.groovy +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -/** - * Task that is used to compile subprojects with javac. - */ -@SuppressWarnings("unused") -abstract class CompileTestJavaInvocationTask extends AllCoordinatesExecTask { - - @Override - List commandFor(String coordinates) { - return [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "compileTestJava"] - } - - @Override - protected String errorMessageFor(String coordinates, int exitCode) { - "Compilation failed" - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.java new file mode 100644 index 00000000000..f7edbd9e0e2 --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CompileTestJavaInvocationTask.java @@ -0,0 +1,29 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import java.util.List; + +/** + * Task that is used to compile subprojects with javac. + */ +@SuppressWarnings("unused") +public abstract class CompileTestJavaInvocationTask extends AllCoordinatesExecTask { + + @Override + public List commandFor(String coordinates) { + return List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "compileTestJava" + ); + } + + @Override + protected String errorMessageFor(String coordinates, int exitCode) { + return "Compilation failed"; + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.groovy deleted file mode 100644 index a4f3a6c4d6a..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.groovy +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.DefaultTask -import org.gradle.api.provider.ListProperty -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Optional -import org.graalvm.internal.tck.harness.TckExtension -import org.graalvm.internal.tck.utils.CoordinateUtils - -import javax.inject.Inject - -/** - * Base task providing unified coordinate resolution from -Pcoordinates and optional overrides. - * Supports: - * - single coordinate: group:artifact:version - * - filter by group/artifact - * - "all" - * - fractional batches "k/n" - * Always filters out "samples:". - */ -abstract class CoordinatesAwareTask extends DefaultTask { - - protected final TckExtension tckExtension - - @Inject - CoordinatesAwareTask() { - this.tckExtension = project.extensions.findByType(TckExtension) - // Default to no override - getCoordinatesOverride().convention(Collections.emptyList()) - } - - /** - * Allows tasks (e.g., diff) to override the set of coordinates to run on. - */ - @Input - @Optional - final ListProperty coordinatesOverride = project.objects.listProperty(String) - - ListProperty getCoordinatesOverride() { - return coordinatesOverride - } - - void setCoordinatesOverride(List coords) { - getCoordinatesOverride().set(coords) - } - - protected List resolveCoordinates() { - List override = getCoordinatesOverride().orNull - List coords - if (override != null && !override.isEmpty()) { - coords = override - } else { - String coordinateFilter = Objects.requireNonNullElse(project.findProperty("coordinates"), "") as String - coords = computeMatchingCoordinates(coordinateFilter) - } - return coords.findAll { !it.startsWith("samples:") } - } - - protected List computeMatchingCoordinates(String filter) { - if (CoordinateUtils.isFractionalBatch(filter)) { - int[] frac = CoordinateUtils.parseFraction(filter) - List all = tckExtension.getMatchingCoordinates("all") - return CoordinateUtils.computeBatchedCoordinates(all, frac[0], frac[1]) - } else { - return tckExtension.getMatchingCoordinates(filter) - } - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.java new file mode 100644 index 00000000000..bbd6080460a --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/CoordinatesAwareTask.java @@ -0,0 +1,77 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.graalvm.internal.tck.harness.TckExtension; +import org.graalvm.internal.tck.utils.CoordinateUtils; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Base task providing unified coordinate resolution from -Pcoordinates and optional overrides. + * Supports: + * - single coordinate: group:artifact:version + * - filter by group/artifact + * - "all" + * - fractional batches "k/n" + * Always filters out "samples:". + */ +public abstract class CoordinatesAwareTask extends DefaultTask { + + protected final TckExtension tckExtension; + + @Input + @Optional + private final ListProperty coordinatesOverride; + + @Inject + public CoordinatesAwareTask() { + this.tckExtension = getProject().getExtensions().findByType(TckExtension.class); + this.coordinatesOverride = getProject().getObjects().listProperty(String.class); + this.coordinatesOverride.convention(Collections.emptyList()); + } + + public ListProperty getCoordinatesOverride() { + return coordinatesOverride; + } + + public void setCoordinatesOverride(List coords) { + getCoordinatesOverride().set(coords); + } + + protected List resolveCoordinates() { + List override = getCoordinatesOverride().getOrNull(); + List coords; + if (override != null && !override.isEmpty()) { + coords = override; + } else { + String coordinateFilter = Objects.toString(getProject().findProperty("coordinates"), ""); + coords = computeMatchingCoordinates(coordinateFilter); + } + return coords.stream() + .filter(c -> !c.startsWith("samples:")) + .collect(Collectors.toList()); + } + + protected List computeMatchingCoordinates(String filter) { + if (CoordinateUtils.isFractionalBatch(filter)) { + int[] frac = CoordinateUtils.parseFraction(filter); + List all = tckExtension.getMatchingCoordinates("all"); + return CoordinateUtils.computeBatchedCoordinates(all, frac[0], frac[1]); + } else { + return tckExtension.getMatchingCoordinates(filter); + } + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.groovy deleted file mode 100644 index f851246f4f5..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.groovy +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import groovy.json.JsonOutput -import org.graalvm.internal.tck.model.MetadataVersionsIndexEntry -import org.graalvm.internal.tck.model.SkippedVersionEntry -import org.gradle.api.DefaultTask -import org.gradle.api.provider.ListProperty -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.TaskAction -import org.gradle.util.internal.VersionNumber - -import java.util.regex.Matcher -import java.util.regex.Pattern - -@SuppressWarnings("unused") -abstract class FetchExistingLibrariesWithNewerVersionsTask extends DefaultTask { - - @Input - abstract ListProperty getAllLibraryCoordinates() - - private static final List INFRASTRUCTURE_TESTS = List.of("samples", "org.example") - - /** - * Identifies library versions, including optional pre-release, ".Final" and ".RELEASE" suffixes. - *

- * A version is considered a pre-release if it has a suffix (following the last '.' or '-') matching - * one of these case-insensitive patterns: - *

    - *
  • {@code alpha} followed by optional numbers (e.g., "alpha", "Alpha1", "alpha123")
  • - *
  • {@code beta} followed by optional numbers (e.g., "beta", "Beta2", "BETA45")
  • - *
  • {@code rc} followed by optional numbers (e.g., "rc", "RC1", "rc99")
  • - *
  • {@code cr} followed by optional numbers (e.g., "cr", "CR3", "cr10")
  • - *
  • {@code m} followed by REQUIRED numbers (e.g., "M1", "m23")
  • - *
  • {@code ea} followed by optional numbers (e.g., "ea", "ea2", "ea15")
  • - *
  • {@code b} followed by REQUIRED numbers (e.g., "b0244", "b5")
  • - *
  • {@code preview} followed by optional numbers (e.g., "preview", "preview1", "preview42")
  • - *
  • Numeric suffixes separated by '-' (e.g., "-1", "-123")
  • - *
- *

- * Versions ending with ".Final" or ".RELEASE" are treated as full releases of the base version. - */ - private static final Pattern VERSION_PATTERN = ~/(?i)^(\d+(?:\.\d+)*)(?:\.Final|\.RELEASE)?(?:[-.](alpha\d*|beta\d*|rc\d*|cr\d*|m\d+|ea\d*|b\d+|\d+|preview)(?:[-.].*)?)?$/ - - @TaskAction - void action() { - // get all existing libraries - Set libraries = [] - getAllLibraryCoordinates().get().forEach { - libraries.add(it.substring(0, it.lastIndexOf(":"))) - } - - // foreach existing library find newer versions than the latest one tested except for infrastructure tests - List newerVersions = new ArrayList<>() - libraries.forEach { - String libraryName = it - if (INFRASTRUCTURE_TESTS.stream().noneMatch(testName -> libraryName.startsWith(testName))) { - List versions = getNewerVersionsFor(libraryName, getLatestLibraryVersion(libraryName)) - List skipped = getSkippedVersions(libraryName) - - // filter out skipped versions - versions = versions.findAll { !skipped.contains(it) } - - versions.forEach { - newerVersions.add(libraryName.concat(":").concat(it)) - } - } - } - - def map = [:] - newerVersions.each { coord -> - def (group, artifact, version) = coord.tokenize(':') - def key = "${group}:${artifact}" - map[key] = (map[key] ?: []) + version - } - def pairs = map.collect { k, v -> [name: k, versions: v] } - - println JsonOutput.toJson(pairs) - } - - static List getNewerVersionsFor(String library, String startingVersion) { - def baseUrl = "https://repo1.maven.org/maven2" - String[] libraryParts = library.split(":") - String group = libraryParts[0].replace(".", "/") - String artifact = libraryParts[1] - def data = new URL(baseUrl + "/" + group + "/" + artifact + "/" + "maven-metadata.xml").getText() - - List newerVersions = getNewerVersionsFromLibraryIndex(data, startingVersion, library) - - // filter out already tested versions - List testedVersions = getTestedVersions(library); - newerVersions.removeAll(testedVersions); - - // filter pre-release versions if full release exists - return filterPreReleases(newerVersions) - } - - static List getNewerVersionsFromLibraryIndex(String index, String startingVersion, String libraryName) { - Pattern pattern = Pattern.compile("(.*)"); - Matcher matcher = pattern.matcher(index); - List allVersions = new ArrayList<>(); - - if (matcher.groupCount() < 1) { - throw new RuntimeException("Cannot find versions in the given index file: " + libraryName); - } - - while (matcher.find()) { - allVersions.add(matcher.group(1)); - } - - int indexOfStartingVersion = allVersions.indexOf(startingVersion); - if (indexOfStartingVersion < 0) { - return new ArrayList<>(); - } - - allVersions = allVersions.subList(indexOfStartingVersion, allVersions.size()); - - return allVersions.subList(1, allVersions.size()); - } - - static List filterPreReleases(List versions) { - // identify full releases - Set releases = versions.collect { v -> - def matcher = VERSION_PATTERN.matcher(v) - if (matcher.matches() && matcher.group(2) == null) { - return matcher.group(1) - } - return null - }.findAll { it != null } as Set - - // filter pre-releases if full release exists - return versions.findAll { v -> - def matcher = VERSION_PATTERN.matcher(v) - if (matcher.matches()) { - String base = matcher.group(1) - String preSuffix = matcher.groupCount() > 1 ? matcher.group(2) : null - return preSuffix == null || !releases.contains(base) - } - true - } - } - - static String getLatestLibraryVersion(String libraryModule) { - try { - List testedVersions = getTestedVersions(libraryModule); - if (testedVersions.isEmpty()) { - throw new IllegalStateException("Cannot find any tested version for: " + libraryModule); - } - - testedVersions.sort(Comparator.comparing(VersionNumber::parse)); - return testedVersions.get(testedVersions.size() - 1); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Reads the tested versions of a given library from its metadata index file. - */ - static List getTestedVersions(String libraryModule) { - try { - String[] coordinates = libraryModule.split(":"); - String group = coordinates[0]; - String artifact = coordinates[1]; - - File indexFile = new File("metadata/" + group + "/" + artifact + "/index.json"); - if (!indexFile.exists()) { - return Collections.emptyList(); - } - - ObjectMapper objectMapper = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); - - List entries = objectMapper.readValue(indexFile, - new TypeReference>() {}); - - List testedVersions = new ArrayList<>(); - for (MetadataVersionsIndexEntry entry : entries) { - testedVersions.addAll(entry.testedVersions()); - } - return testedVersions; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Returns all versions of a given library that are marked as skipped in the - * metadata index. - *

- * For the provided Maven coordinates (in the format {@code :}), - * this method reads the corresponding {@code index.json} file located under: - * {@code metadata///index.json} - * and collects all version entries listed under {@code skipped-versions}. - */ - static List getSkippedVersions(String libraryModule) { - try { - String[] coordinates = libraryModule.split(":"); - String group = coordinates[0]; - String artifact = coordinates[1]; - - File coordinatesMetadataIndex = new File("metadata/" + group + "/" + artifact + "/index.json"); - ObjectMapper objectMapper = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); - - List entries = objectMapper.readValue( - coordinatesMetadataIndex, - new TypeReference>() {} - ); - - List skipped = new ArrayList<>(); - for (MetadataVersionsIndexEntry entry : entries) { - if (entry.skippedVersions() != null) { - skipped.addAll( - entry.skippedVersions().stream() - .map(SkippedVersionEntry::version) - .toList() - ); - } - } - - return skipped; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.java similarity index 51% rename from tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.groovy rename to tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.java index 5aff54dda74..817cf8223ba 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.groovy +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/JavaTestInvocationTask.java @@ -4,21 +4,26 @@ * You should have received a copy of the CC0 legalcode along with this * work. If not, see . */ -package org.graalvm.internal.tck.harness.tasks +package org.graalvm.internal.tck.harness.tasks; + +import java.util.List; /** * Task that is used to run JVM tests (Gradle 'test') on subprojects. */ @SuppressWarnings("unused") -abstract class JavaTestInvocationTask extends AllCoordinatesExecTask { +public abstract class JavaTestInvocationTask extends AllCoordinatesExecTask { @Override - List commandFor(String coordinates) { - return [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "test"] + public List commandFor(String coordinates) { + return List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "test" + ); } @Override protected String errorMessageFor(String coordinates, int exitCode) { - "Java tests failed" + return "Java tests failed"; } } diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.java similarity index 50% rename from tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.groovy rename to tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.java index 80c3e5f9187..12a518c8ae8 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.groovy +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/NativeTestCompileInvocationTask.java @@ -4,21 +4,26 @@ * You should have received a copy of the CC0 legalcode along with this * work. If not, see . */ -package org.graalvm.internal.tck.harness.tasks +package org.graalvm.internal.tck.harness.tasks; + +import java.util.List; /** * Task that is used to compile native tests (Gradle 'nativeTestCompile') on subprojects. */ @SuppressWarnings("unused") -abstract class NativeTestCompileInvocationTask extends AllCoordinatesExecTask { +public abstract class NativeTestCompileInvocationTask extends AllCoordinatesExecTask { @Override - List commandFor(String coordinates) { - return [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "nativeTestCompile"] + public List commandFor(String coordinates) { + return List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "nativeTestCompile" + ); } @Override protected String errorMessageFor(String coordinates, int exitCode) { - "Native test compilation failed" + return "Native test compilation failed"; } } diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.java similarity index 69% rename from tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.groovy rename to tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.java index ddf1b9090e7..26d5f565003 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.groovy +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/SingleCoordinateTask.java @@ -4,28 +4,30 @@ * You should have received a copy of the CC0 legalcode along with this * work. If not, see . */ -package org.graalvm.internal.tck.harness.tasks +package org.graalvm.internal.tck.harness.tasks; -import org.gradle.api.GradleException +import org.gradle.api.GradleException; + +import java.util.List; /** * Base task for actions that must operate on exactly one coordinate. * Reuses the same unified resolution as CoordinatesAwareTask and enforces a single result. */ -abstract class SingleCoordinateTask extends CoordinatesAwareTask { +public abstract class SingleCoordinateTask extends CoordinatesAwareTask { /** * Resolves to exactly one coordinate or fails with a helpful error. */ protected String resolveSingleCoordinate() { - List coords = resolveCoordinates() + List coords = resolveCoordinates(); if (coords.isEmpty()) { - throw new GradleException("No matching coordinates found. Provide a concrete coordinate via -Pcoordinates=group:artifact:version") + throw new GradleException("No matching coordinates found. Provide a concrete coordinate via -Pcoordinates=group:artifact:version"); } if (coords.size() > 1) { - throw new GradleException("Multiple coordinates matched: ${coords}. This task requires a single concrete coordinate. " + - "Please specify an exact 'group:artifact:version' using -Pcoordinates.") + throw new GradleException("Multiple coordinates matched: " + coords + ". This task requires a single concrete coordinate. " + + "Please specify an exact 'group:artifact:version' using -Pcoordinates."); } - return coords.get(0) + return coords.get(0); } } diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.groovy b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.groovy deleted file mode 100644 index 8865fce114a..00000000000 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.groovy +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright and related rights waived via CC0 - * - * You should have received a copy of the CC0 legalcode along with this - * work. If not, see . - */ -package org.graalvm.internal.tck.harness.tasks - -import org.gradle.api.provider.ProviderFactory - -import javax.inject.Inject -import java.nio.file.Path -import java.util.stream.Collectors - -import static org.graalvm.internal.tck.Utils.readIndexFile -import static org.graalvm.internal.tck.Utils.splitCoordinates - -/** - * Task that is used to start subproject tests for matching coordinates. - * Coordinate resolution is unified and handled by the base class. - */ -@SuppressWarnings("unused") -abstract class TestInvocationTask extends AllCoordinatesExecTask { - - @Inject - abstract ProviderFactory getProviders() - - @Override - List commandFor(String coordinates) { - def defaultArgs = [tckExtension.repoRoot.get().asFile.toPath().resolve("gradlew").toString(), "nativeTest"] - def installPathsProperty = providers.environmentVariable("TCK_JDK_INSTALLATION_PATHS") - if (installPathsProperty.isPresent()) { - defaultArgs.addAll( - [ - "-Porg.gradle.java.installations.auto-detect=false", - "-Porg.gradle.java.installations.paths=${installPathsProperty.get()}" - ] - ) - } - try { - Map> testIndex = readIndexFile(tckExtension.getTestDir(coordinates)) as Map> - if (!testIndex.containsKey("test-command")) { - return defaultArgs - } - - Path metadataDir = tckExtension.getMetadataDir(coordinates) - return testIndex.get("test-command").stream() - .map(c -> processCommand(c, metadataDir, coordinates)) - .collect(Collectors.toList()) - } catch (FileNotFoundException ignored) { - return defaultArgs - } - } - - /** - * Fills in template parameters in the command invocation. - * Parameters are defined as in cmd. - * - * @param cmd command line with parameters - * @param metadataDir metadata directory location - * @param coordinates - * @return final command - */ - static String processCommand(String cmd, Path metadataDir, String coordinates) { - def (String groupId, String artifactId, String version) = splitCoordinates(coordinates) - return cmd.replace("", metadataDir.toAbsolutePath().toString()) - .replace("", groupId) - .replace("", artifactId) - .replace("", version) - } - - @Override - protected String errorMessageFor(String coordinates, int exitCode) { - "Test for ${coordinates} failed with exit code ${exitCode}." - } - - @Override - protected void beforeEach(String coordinates, List command) { - getLogger().lifecycle("====================") - getLogger().lifecycle("Testing library: {}", coordinates) - getLogger().lifecycle("Command: `{}`", String.join(" ", command)) - getLogger().lifecycle("Executing test...") - getLogger().lifecycle("-------") - } - - @Override - protected void afterEach(String coordinates) { - getLogger().lifecycle("-------") - getLogger().lifecycle("Test for {} passed.", coordinates) - getLogger().lifecycle("====================") - } -} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.java new file mode 100644 index 00000000000..f9c44e35e12 --- /dev/null +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/TestInvocationTask.java @@ -0,0 +1,99 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import org.gradle.api.provider.ProviderFactory; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.graalvm.internal.tck.Utils.readIndexFile; +import static org.graalvm.internal.tck.Utils.splitCoordinates; + +/** + * Task that is used to start subproject tests for matching coordinates. + * Coordinate resolution is unified and handled by the base class. + */ +@SuppressWarnings("unused") +public abstract class TestInvocationTask extends AllCoordinatesExecTask { + + @Inject + public abstract ProviderFactory getProviders(); + + @Override + public List commandFor(String coordinates) { + List defaultArgs = new ArrayList<>(List.of( + tckExtension.getRepoRoot().get().getAsFile().toPath().resolve("gradlew").toString(), + "nativeTest" + )); + var installPathsProperty = getProviders().environmentVariable("TCK_JDK_INSTALLATION_PATHS"); + if (installPathsProperty.isPresent()) { + defaultArgs.add("-Porg.gradle.java.installations.auto-detect=false"); + defaultArgs.add("-Porg.gradle.java.installations.paths=" + installPathsProperty.get()); + } + try { + @SuppressWarnings("unchecked") + Map> testIndex = + (Map>) readIndexFile(tckExtension.getTestDir(coordinates)); + if (!testIndex.containsKey("test-command")) { + return defaultArgs; + } + + Path metadataDir = tckExtension.getMetadataDir(coordinates); + return testIndex.get("test-command").stream() + .map(c -> processCommand(c, metadataDir, coordinates)) + .collect(Collectors.toList()); + } catch (Exception ignored) { + return defaultArgs; + } + } + + /** + * Fills in template parameters in the command invocation. + * Parameters are defined as in cmd. + * + * @param cmd command line with parameters + * @param metadataDir metadata directory location + * @param coordinates coordinates in form group:artifact:version + * @return final command + */ + public static String processCommand(String cmd, Path metadataDir, String coordinates) { + List parts = splitCoordinates(coordinates); + String groupId = parts.get(0); + String artifactId = parts.get(1); + String version = parts.get(2); + return cmd.replace("", metadataDir.toAbsolutePath().toString()) + .replace("", groupId) + .replace("", artifactId) + .replace("", version); + } + + @Override + protected String errorMessageFor(String coordinates, int exitCode) { + return "Test for " + coordinates + " failed with exit code " + exitCode + "."; + } + + @Override + protected void beforeEach(String coordinates, List command) { + getLogger().lifecycle("===================="); + getLogger().lifecycle("Testing library: {}", coordinates); + getLogger().lifecycle("Command: `{}`", String.join(" ", command)); + getLogger().lifecycle("Executing test..."); + getLogger().lifecycle("-------"); + } + + @Override + protected void afterEach(String coordinates) { + getLogger().lifecycle("-------"); + getLogger().lifecycle("Test for {} passed.", coordinates); + getLogger().lifecycle("===================="); + } +} diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/ValidateIndexFilesTask.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/ValidateIndexFilesTask.java index b55a5770473..08c60be2cff 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/ValidateIndexFilesTask.java +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/tasks/ValidateIndexFilesTask.java @@ -19,6 +19,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; import org.jetbrains.annotations.NotNull; +import org.gradle.util.internal.VersionNumber; import java.io.File; import java.util.ArrayList; @@ -137,15 +138,25 @@ private void executeValidation(Set targetFiles) { ); JsonNode json = mapper.readTree(jsonFile); - Set errors = schema.validate(json); + int beforeFailures = failures.size(); - if (errors.isEmpty()) { - getLogger().lifecycle("✅ " + filePath + ": Valid"); - } else { + // Schema validation + Set errors = schema.validate(json); + if (!errors.isEmpty()) { for (ValidationMessage err : errors) { failures.add("❌ " + filePath + ": " + err.getMessage()); } } + + // Additional semantic validations for library metadata index files + if (METADATA_PATTERN.matcher(filePath).matches()) { + checkLibraryIndexTestedVersions(json, filePath, failures); + } + + // Print success only if no new failures were added by schema or semantic checks + if (failures.size() == beforeFailures) { + getLogger().lifecycle("✅ " + filePath + ": Valid"); + } } catch (Exception e) { failures.add("💥 " + filePath + ": Parse Error (" + e.getMessage() + ")"); } @@ -157,6 +168,95 @@ private void executeValidation(Set targetFiles) { } } + /** + * Ensures "tested-versions" are mapped to the most appropriate metadata entry. + *

+ * Rule: A version in {@code tested-versions} must be strictly less than the + * next higher {@code metadata-version} available in the file. + *

+ * This prevents "stray" versions from being associated with obsolete metadata + * when a more recent metadata entry exists. + * + * @param json The JSON array of index entries. + * @param filePath Path for error reporting. + * @param failures Accumulator for validation errors. + */ + private static void checkLibraryIndexTestedVersions(JsonNode json, String filePath, List failures) { + if (json == null || !json.isArray()) { + return; + } + + // Collect unique metadata-version strings + java.util.Set metaStrings = new java.util.LinkedHashSet<>(); + for (JsonNode entry : json) { + JsonNode mv = entry.get("metadata-version"); + if (mv != null && mv.isTextual()) { + metaStrings.add(mv.asText()); + } + } + if (metaStrings.isEmpty()) { + return; + } + + // Parse and sort metadata versions + List metasSorted = new ArrayList<>(); + for (String s : metaStrings) { + try { + metasSorted.add(VersionNumber.parse(s)); + } catch (Exception ignore) { + // Ignore unparsable versions; schema should handle invalid shapes + } + } + if (metasSorted.isEmpty()) { + return; + } + metasSorted.sort(java.util.Comparator.naturalOrder()); + + // For each entry, enforce: tested-version < next(metadata-version), if next exists + for (JsonNode entry : json) { + String underMetaStr = entry.path("metadata-version").isTextual() ? entry.get("metadata-version").asText() : null; + if (underMetaStr == null) continue; + + VersionNumber underMeta; + try { + underMeta = VersionNumber.parse(underMetaStr); + } catch (Exception ignore) { + continue; + } + + // Locate index of current metadata-version in the sorted list + int idx = -1; + for (int i = 0; i < metasSorted.size(); i++) { + if (metasSorted.get(i).compareTo(underMeta) == 0) { + idx = i; + break; + } + } + if (idx == -1) continue; + + VersionNumber nextMeta = (idx < metasSorted.size() - 1) ? metasSorted.get(idx + 1) : null; + + JsonNode tvs = entry.get("tested-versions"); + if (tvs != null && tvs.isArray() && nextMeta != null) { + for (JsonNode tvNode : tvs) { + if (tvNode != null && tvNode.isTextual()) { + try { + VersionNumber tv = VersionNumber.parse(tvNode.asText()); + // Must be strictly less than the next metadata-version + if (tv.compareTo(nextMeta) >= 0) { + failures.add("❌ " + filePath + ": tested-versions contains version " + tv + + " not less than next metadata-version " + nextMeta + + " (under metadata-version " + underMetaStr + ")"); + } + } catch (Exception ignore) { + // ignore unparsable tested versions; schema should catch most cases + } + } + } + } + } + } + public static String mapToSchemaPath(String filePath) { if ("metadata/index.json".equals(filePath)) { return "schemas/metadata-root-index-schema-v1.0.0.json"; diff --git a/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/MetadataFilesCheckerTask.java b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/MetadataFilesCheckerTask.java index 7811f271dd5..2ee4a13d0ae 100644 --- a/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/MetadataFilesCheckerTask.java +++ b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/MetadataFilesCheckerTask.java @@ -48,7 +48,7 @@ public abstract class MetadataFilesCheckerTask extends DefaultTask { List allowedPackages; @Option(option = "coordinates", description = "Coordinates in the form of group:artifact:version") - void setCoordinates(String coords) { + public void setCoordinates(String coords) { extractCoordinates(coords); } @@ -78,11 +78,7 @@ private void extractCoordinates(String c) { } @TaskAction - void run() throws IllegalArgumentException, FileNotFoundException { - if (coordinates.group().equalsIgnoreCase("org.example") || coordinates.group().equalsIgnoreCase("samples")) { - return; - } - + public void run() throws IllegalArgumentException, FileNotFoundException { File coordinatesMetadataRoot = getMetadataRoot().get().getAsFile(); if (!coordinatesMetadataRoot.exists()) { throw new IllegalArgumentException("ERROR: Cannot find metadata directory for given coordinates: " + this.coordinates); diff --git a/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/TestedVersionUpdaterTask.java b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/TestedVersionUpdaterTask.java index dfbd37d661f..0fce76f2dac 100644 --- a/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/TestedVersionUpdaterTask.java +++ b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/TestedVersionUpdaterTask.java @@ -55,7 +55,7 @@ public abstract class TestedVersionUpdaterTask extends DefaultTask { *

* Versions ending with ".Final" or `.RELEASE` are treated as full releases of the base version. */ - private static final Pattern VERSION_PATTERN = Pattern.compile( + public static final Pattern VERSION_PATTERN = Pattern.compile( "(?i)^(\\d+(?:\\.\\d+)*)(?:\\.Final|\\.RELEASE)?(?:[-.](alpha\\d*|beta\\d*|rc\\d*|cr\\d*|m\\d+|ea\\d*|b\\d+|\\d+|preview)(?:[-.].*)?)?$" ); diff --git a/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.java b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.java new file mode 100644 index 00000000000..2a50c002c09 --- /dev/null +++ b/tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/harness/tasks/FetchExistingLibrariesWithNewerVersionsTask.java @@ -0,0 +1,213 @@ +/* + * Copyright and related rights waived via CC0 + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ +package org.graalvm.internal.tck.harness.tasks; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.graalvm.internal.tck.TestedVersionUpdaterTask; +import org.graalvm.internal.tck.model.MetadataVersionsIndexEntry; +import org.graalvm.internal.tck.model.SkippedVersionEntry; +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; +import org.gradle.util.internal.VersionNumber; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("unused") +public abstract class FetchExistingLibrariesWithNewerVersionsTask extends DefaultTask { + + @Input + public abstract ListProperty getAllLibraryCoordinates(); + + private static final List INFRASTRUCTURE_TESTS = List.of("samples", "org.example"); + + @TaskAction + public void action() { + Set libraries = new LinkedHashSet<>(); + for (String coord : getAllLibraryCoordinates().get()) { + int last = coord.lastIndexOf(':'); + if (last > 0) { + libraries.add(coord.substring(0, last)); + } + } + + List newerVersions = new ArrayList<>(); + for (String libraryName : libraries) { + if (INFRASTRUCTURE_TESTS.stream().noneMatch(libraryName::startsWith)) { + List versions = getNewerVersionsFor(libraryName, getLatestLibraryVersion(libraryName)); + List skipped = getSkippedVersions(libraryName); + versions.removeAll(skipped); + for (String v : versions) { + newerVersions.add(libraryName + ":" + v); + } + } + } + + Map> grouped = new LinkedHashMap<>(); + for (String coord : newerVersions) { + String[] parts = coord.split(":", -1); + String key = parts[0] + ":" + parts[1]; + grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(parts[2]); + } + + List> pairs = new ArrayList<>(); + for (Map.Entry> e : grouped.entrySet()) { + Map m = new LinkedHashMap<>(); + m.put("name", e.getKey()); + m.put("versions", e.getValue()); + pairs.add(m); + } + + try { + ObjectMapper om = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println(om.writeValueAsString(pairs)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List getNewerVersionsFor(String library, String startingVersion) { + try { + String baseUrl = "https://repo1.maven.org/maven2"; + String[] libraryParts = library.split(":"); + String group = libraryParts[0].replace(".", "/"); + String artifact = libraryParts[1]; + String data = new String(new URL(baseUrl + "/" + group + "/" + artifact + "/maven-metadata.xml").openStream().readAllBytes()); + + List newerVersions = getNewerVersionsFromLibraryIndex(data, startingVersion, library); + + List testedVersions = getTestedVersions(library); + newerVersions.removeAll(testedVersions); + + return filterPreReleases(newerVersions); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List getNewerVersionsFromLibraryIndex(String index, String startingVersion, String libraryName) { + Pattern pattern = Pattern.compile("(.*)"); + Matcher matcher = pattern.matcher(index); + List allVersions = new ArrayList<>(); + + while (matcher.find()) { + allVersions.add(matcher.group(1)); + } + + int indexOfStartingVersion = allVersions.indexOf(startingVersion); + if (indexOfStartingVersion < 0) { + return new ArrayList<>(); + } + + allVersions = allVersions.subList(indexOfStartingVersion, allVersions.size()); + return new ArrayList<>(allVersions.subList(1, allVersions.size())); + } + + static List filterPreReleases(List versions) { + Set releases = new HashSet<>(); + for (String v : versions) { + Matcher m = TestedVersionUpdaterTask.VERSION_PATTERN.matcher(v); + if (m.matches() && m.group(2) == null) { + releases.add(m.group(1)); + } + } + + List result = new ArrayList<>(); + for (String v : versions) { + Matcher m = TestedVersionUpdaterTask.VERSION_PATTERN.matcher(v); + if (m.matches()) { + String base = m.group(1); + String preSuffix = m.groupCount() > 1 ? m.group(2) : null; + if (preSuffix == null || !releases.contains(base)) { + result.add(v); + } + } else { + result.add(v); + } + } + return result; + } + + static String getLatestLibraryVersion(String libraryModule) { + try { + List testedVersions = getTestedVersions(libraryModule); + if (testedVersions.isEmpty()) { + throw new IllegalStateException("Cannot find any tested version for: " + libraryModule); + } + testedVersions.sort(Comparator.comparing(VersionNumber::parse)); + return testedVersions.get(testedVersions.size() - 1); + } catch (RuntimeException e) { + throw e; + } + } + + static List getTestedVersions(String libraryModule) { + try { + String[] coordinates = libraryModule.split(":"); + String group = coordinates[0]; + String artifact = coordinates[1]; + + File indexFile = new File("metadata/" + group + "/" + artifact + "/index.json"); + if (!indexFile.exists()) { + return Collections.emptyList(); + } + + ObjectMapper objectMapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + List entries = objectMapper.readValue( + indexFile, new TypeReference>() {}); + List testedVersions = new ArrayList<>(); + for (MetadataVersionsIndexEntry entry : entries) { + testedVersions.addAll(entry.testedVersions()); + } + return testedVersions; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List getSkippedVersions(String libraryModule) { + try { + String[] coordinates = libraryModule.split(":"); + String group = coordinates[0]; + String artifact = coordinates[1]; + + File coordinatesMetadataIndex = new File("metadata/" + group + "/" + artifact + "/index.json"); + if (!coordinatesMetadataIndex.exists()) { + throw new RuntimeException("Missing index.json for " + libraryModule); + } + + ObjectMapper objectMapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + List entries = objectMapper.readValue( + coordinatesMetadataIndex, new TypeReference>() {}); + + List skipped = new ArrayList<>(); + for (MetadataVersionsIndexEntry entry : entries) { + if (entry.skippedVersions() != null) { + for (SkippedVersionEntry sve : entry.skippedVersions()) { + skipped.add(sve.version()); + } + } + } + return skipped; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}