diff --git a/license-header b/license-header new file mode 100644 index 00000000..7fdff0ae --- /dev/null +++ b/license-header @@ -0,0 +1,15 @@ +/* + * Copyright © 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/pom.xml b/pom.xml index 881f4c0d..f996021e 100644 --- a/pom.xml +++ b/pom.xml @@ -685,6 +685,43 @@ limitations under the License.]]> + + + com.diffplug.spotless + spotless-maven-plugin + + + + src/* + + + + true + 4 + + + + + + + true + + + ${project.basedir}/license-header --> + + + + + + + + + check + + process-sources + + + diff --git a/src/main/java/com/redhat/exhort/Api.java b/src/main/java/com/redhat/exhort/Api.java index 3b6bc136..d728cffa 100644 --- a/src/main/java/com/redhat/exhort/Api.java +++ b/src/main/java/com/redhat/exhort/Api.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; import java.io.IOException; import java.util.Arrays; import java.util.Map; @@ -22,10 +24,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; - -/** The Api interface is used for contracting API implementations. **/ +/** The Api interface is used for contracting API implementations. * */ public interface Api { public static final String CYCLONEDX_MEDIA_TYPE = "application/vnd.cyclonedx+json"; @@ -43,18 +42,19 @@ public String toString() { /** POJO class used for aggregating multipart/mixed analysis requests. */ class MixedReport { - final public byte[] html; - final public AnalysisReport json; + public final byte[] html; + public final AnalysisReport json; public MixedReport(final byte[] html, final AnalysisReport json) { this.html = html; this.json = json; } - public MixedReport() - { + + public MixedReport() { this.html = new byte[0]; this.json = new AnalysisReport(); } + @Override public boolean equals(final Object o) { if (this == o) return true; @@ -104,11 +104,13 @@ public int hashCode() { * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture * @throws IOException when failed to load the manifest content */ - CompletableFuture componentAnalysis(String manifestType, byte[] manifestContent) throws IOException; + CompletableFuture componentAnalysis(String manifestType, byte[] manifestContent) + throws IOException; CompletableFuture componentAnalysis(String manifestFile) throws IOException; - CompletableFuture> imageAnalysis(Set imageRefs) throws IOException; + CompletableFuture> imageAnalysis(Set imageRefs) + throws IOException; CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException; } diff --git a/src/main/java/com/redhat/exhort/Provider.java b/src/main/java/com/redhat/exhort/Provider.java index 1840f58e..a3b72753 100644 --- a/src/main/java/com/redhat/exhort/Provider.java +++ b/src/main/java/com/redhat/exhort/Provider.java @@ -15,25 +15,25 @@ */ package com.redhat.exhort; -import java.io.IOException; -import java.nio.file.Path; - import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.tools.Ecosystem; +import java.io.IOException; +import java.nio.file.Path; /** - * The Provider abstraction is used for contracting providers providing a {@link Content} - * per manifest type for constructing backend requests. - **/ + * The Provider abstraction is used for contracting providers providing a {@link Content} per + * manifest type for constructing backend requests. + */ public abstract class Provider { /** - * Content is used to aggregate a content buffer and a content type. - * These will be used to construct the backend API request. - **/ + * Content is used to aggregate a content buffer and a content type. These will be used to + * construct the backend API request. + */ public static class Content { public final byte[] buffer; public final String type; - public Content(byte[] buffer, String type){ + + public Content(byte[] buffer, String type) { this.buffer = buffer; this.type = type; } @@ -41,6 +41,7 @@ public Content(byte[] buffer, String type){ /** The ecosystem of this provider, i.e. maven. */ public final Ecosystem.Type ecosystem; + protected final ObjectMapper objectMapper = new ObjectMapper(); protected Provider(Ecosystem.Type ecosystem) { @@ -64,5 +65,6 @@ protected Provider(Ecosystem.Type ecosystem) { * @throws IOException when failed to load the manifest content */ public abstract Content provideComponent(byte[] manifestContent) throws IOException; + public abstract Content provideComponent(Path manifestPath) throws IOException; } diff --git a/src/main/java/com/redhat/exhort/api/PackageRef.java b/src/main/java/com/redhat/exhort/api/PackageRef.java index 96eeb7b0..347d48bc 100644 --- a/src/main/java/com/redhat/exhort/api/PackageRef.java +++ b/src/main/java/com/redhat/exhort/api/PackageRef.java @@ -15,13 +15,12 @@ */ package com.redhat.exhort.api; -import java.util.Objects; - import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.api.serialization.PackageURLSerializer; +import java.util.Objects; public class PackageRef { @@ -55,8 +54,8 @@ public String ref() { public String name() { if (purl.getNamespace() == null) { - return purl.getName(); - } + return purl.getName(); + } return purl.getNamespace() + ":" + purl.getName(); } diff --git a/src/main/java/com/redhat/exhort/api/package-info.java b/src/main/java/com/redhat/exhort/api/package-info.java index dbfc2771..0a887edc 100644 --- a/src/main/java/com/redhat/exhort/api/package-info.java +++ b/src/main/java/com/redhat/exhort/api/package-info.java @@ -1,2 +1,2 @@ -/** Package hosting various the Exhort API implementation. **/ +/** Package hosting various the Exhort API implementation. * */ package com.redhat.exhort.api; diff --git a/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java b/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java index 391ebf26..f192e2bf 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java @@ -15,33 +15,32 @@ */ package com.redhat.exhort.api.serialization; -import java.io.IOException; - import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.redhat.exhort.api.PackageRef; +import java.io.IOException; public class PackageRefDeserializer extends StdDeserializer { - public PackageRefDeserializer() { - this(null); - } + public PackageRefDeserializer() { + this(null); + } - public PackageRefDeserializer(Class c) { - super(c); - } + public PackageRefDeserializer(Class c) { + super(c); + } - @Override - public PackageRef deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { - JsonNode n = p.getCodec().readTree(p); - String purl = n.asText(); - if (purl == null) { - return null; - } - return new PackageRef(purl); + @Override + public PackageRef deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + JsonNode n = p.getCodec().readTree(p); + String purl = n.asText(); + if (purl == null) { + return null; } - + return new PackageRef(purl); + } } diff --git a/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java b/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java index 914a2857..e61c786c 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.api.serialization; -import java.io.IOException; - import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.github.packageurl.PackageURL; +import java.io.IOException; public class PackageURLSerializer extends StdSerializer { diff --git a/src/main/java/com/redhat/exhort/api/serialization/package-info.java b/src/main/java/com/redhat/exhort/api/serialization/package-info.java index ccc9a59c..866be540 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/package-info.java +++ b/src/main/java/com/redhat/exhort/api/serialization/package-info.java @@ -1,2 +1,2 @@ -/** Package hosting various the Exhort API implementation. **/ +/** Package hosting various the Exhort API implementation. * */ package com.redhat.exhort.api.serialization; diff --git a/src/main/java/com/redhat/exhort/image/Image.java b/src/main/java/com/redhat/exhort/image/Image.java index 1eee5044..a3ba9297 100644 --- a/src/main/java/com/redhat/exhort/image/Image.java +++ b/src/main/java/com/redhat/exhort/image/Image.java @@ -19,7 +19,6 @@ * Contents in this file are from: * https://github.com/fabric8io/docker-maven-plugin/blob/6eeb78a9b074328ef5817c1c91392d8d8350984e/src/main/java/io/fabric8/maven/docker/util/ImageName.java */ - import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -30,18 +29,19 @@ * Helper class for parsing docker repository/image names: * *
    - *
  • If the first part before the slash contains a "." or a ":" it is considered to be a registry URL
  • - *
  • A last part starting with a ":" is considered to be a tag
  • - *
  • The rest is considered the repository name (which might be separated via slashes)
  • + *
  • If the first part before the slash contains a "." or a ":" it is considered to be a + * registry URL + *
  • A last part starting with a ":" is considered to be a tag + *
  • The rest is considered the repository name (which might be separated via slashes) *
- *

- * Example of valid names: + * + *

Example of valid names: * *

    - *
  • consol/tomcat-8.0
  • - *
  • consol/tomcat-8.0:8.0.9
  • - *
  • docker.consol.de:5000/tomcat-8.0
  • - *
  • docker.consol.de:5000/jolokia/tomcat-8.0:8.0.9
  • + *
  • consol/tomcat-8.0 + *
  • consol/tomcat-8.0:8.0.9 + *
  • docker.consol.de:5000/tomcat-8.0 + *
  • docker.consol.de:5000/jolokia/tomcat-8.0:8.0.9 *
* * @author roland @@ -53,13 +53,17 @@ public class Image { // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 private final String nameComponentRegexp = "[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?"; // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L25 - private final String domainComponentRegexp = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"; + private final String domainComponentRegexp = + "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"; // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 private final Pattern NAME_COMP_REGEXP = Pattern.compile(nameComponentRegexp); // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L53 - private final Pattern IMAGE_NAME_REGEXP = Pattern.compile(nameComponentRegexp + "(?:(?:/" + nameComponentRegexp + ")+)?"); + private final Pattern IMAGE_NAME_REGEXP = + Pattern.compile(nameComponentRegexp + "(?:(?:/" + nameComponentRegexp + ")+)?"); // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L31 - private final Pattern DOMAIN_REGEXP = Pattern.compile("^" + domainComponentRegexp + "(?:\\." + domainComponentRegexp + ")*(?::[0-9]+)?$"); + private final Pattern DOMAIN_REGEXP = + Pattern.compile( + "^" + domainComponentRegexp + "(?:\\." + domainComponentRegexp + ")*(?::[0-9]+)?$"); // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L37 private final Pattern TAG_REGEXP = Pattern.compile("^[\\w][\\w.-]{0,127}$"); private final Pattern DIGEST_REGEXP = Pattern.compile("^sha256:[a-z0-9]{32,}$"); @@ -108,7 +112,8 @@ public Image(String fullName, String givenTag) { Pattern tagPattern = Pattern.compile("^(.+?)(?::([^:/]+))?$"); Matcher matcher = tagPattern.matcher(fullName); if (!matcher.matches()) { - throw new IllegalArgumentException(fullName + " is not a proper image name ([registry/][repo][:port]"); + throw new IllegalArgumentException( + fullName + " is not a proper image name ([registry/][repo][:port]"); } // extract tag if it exists tag = givenTag != null ? givenTag : matcher.group(2); @@ -130,10 +135,10 @@ public Image(String fullName, String givenTag) { } /** - * Check whether the given name validates agains the Docker rules for names + * Check whether the given name validates against the Docker rules for names * - * @param image image name to validate - * d@throws IllegalArgumentException if the name doesnt validate + * @param image image name to validate d@throws IllegalArgumentException if the name doesnt + * validate */ public static void validate(String image) { // Validation will be triggered during construction @@ -167,11 +172,11 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Image image = (Image) o; - return Objects.equals(repository, image.repository) && - Objects.equals(registry, image.registry) && - Objects.equals(tag, image.tag) && - Objects.equals(digest, image.digest) && - Objects.equals(user, image.user); + return Objects.equals(repository, image.repository) + && Objects.equals(registry, image.registry) + && Objects.equals(tag, image.tag) + && Objects.equals(digest, image.digest) + && Objects.equals(user, image.user); } @Override @@ -204,8 +209,8 @@ private boolean isRegistry(String part) { } /** - * Get the full name of this image, including the registry but without - * any tag (e.g. privateregistry:fabric8io/java) + * Get the full name of this image, including the registry but without any tag (e.g. + * privateregistry:fabric8io/java) * * @return full name with the original registry */ @@ -214,13 +219,13 @@ public String getNameWithoutTag() { } /** - * Get the full name of this image like {@link #getNameWithoutTag()} does, but allow - * an optional registry. This registry is used when this image does not already - * contain a registry. + * Get the full name of this image like {@link #getNameWithoutTag()} does, but allow an optional + * registry. This registry is used when this image does not already contain a registry. * - * @param optionalRegistry optional registry to use when this image does not provide - * a registry. Can be null in which case no optional registry is used* - * @return full name with original registry (if set) or optional registry (if not null) + * @param optionalRegistry optional registry to use when this image does not provide a registry. + * Can be null in which case no optional registry is used* + * @return full name with original registry (if set) or optional registry (if not null + * ) */ public String getNameWithoutTag(String optionalRegistry) { StringBuilder ret = new StringBuilder(); @@ -238,8 +243,8 @@ public String getNameWithoutTag(String optionalRegistry) { // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/reference.go /** - * Get the full name of this image, including the registry and tag - * (e.g. privateregistry:fabric8io/java:7u53) + * Get the full name of this image, including the registry and tag (e.g. + * privateregistry:fabric8io/java:7u53) * * @return full name with the original registry and the original tag given (if any). */ @@ -248,13 +253,14 @@ public String getFullName() { } /** - * Get the full name of this image like {@link #getFullName(String)} does, but allow - * an optional registry. This registry is used when this image does not already - * contain a registry. If no tag was provided in the initial name, latest is used. + * Get the full name of this image like {@link #getFullName(String)} does, but allow an optional + * registry. This registry is used when this image does not already contain a registry. If no tag + * was provided in the initial name, latest is used. * - * @param optionalRegistry optional registry to use when this image does not provide - * a registry. Can be null in which case no optional registry is used* - * @return full name with original registry (if set) or optional registry (if not null). + * @param optionalRegistry optional registry to use when this image does not provide a registry. + * Can be null in which case no optional registry is used* + * @return full name with original registry (if set) or optional registry (if not null + * ). */ public String getFullName(String optionalRegistry) { String fullName = getNameWithoutTag(optionalRegistry); @@ -270,8 +276,8 @@ public String getFullName(String optionalRegistry) { // ========================================================== /** - * Get the user (or "project") part of the image name. This is the part after the registry and before - * the image name + * Get the user (or "project") part of the image name. This is the part after the registry and + * before the image name * * @return user part or null if no user is present in the name */ @@ -305,20 +311,22 @@ private void doValidate() { List errors = new ArrayList<>(); // Strip off user from repository name String image = user != null ? repository.substring(user.length() + 1) : repository; - Object[] checks = new Object[]{ - "registry", DOMAIN_REGEXP, registry, - "image", IMAGE_NAME_REGEXP, image, - "user", NAME_COMP_REGEXP, user, - "tag", TAG_REGEXP, tag, - "digest", DIGEST_REGEXP, digest - }; + Object[] checks = + new Object[] { + "registry", DOMAIN_REGEXP, registry, + "image", IMAGE_NAME_REGEXP, image, + "user", NAME_COMP_REGEXP, user, + "tag", TAG_REGEXP, tag, + "digest", DIGEST_REGEXP, digest + }; for (int i = 0; i < checks.length; i += 3) { String value = (String) checks[i + 2]; Pattern checkPattern = (Pattern) checks[i + 1]; - if (value != null && - !checkPattern.matcher(value).matches()) { - errors.add(String.format("%s part '%s' doesn't match allowed pattern '%s'", - checks[i], value, checkPattern.pattern())); + if (value != null && !checkPattern.matcher(value).matches()) { + errors.add( + String.format( + "%s part '%s' doesn't match allowed pattern '%s'", + checks[i], value, checkPattern.pattern())); } } if (errors.size() > 0) { diff --git a/src/main/java/com/redhat/exhort/image/ImageRef.java b/src/main/java/com/redhat/exhort/image/ImageRef.java index 92e71dec..265ce9b1 100644 --- a/src/main/java/com/redhat/exhort/image/ImageRef.java +++ b/src/main/java/com/redhat/exhort/image/ImageRef.java @@ -15,17 +15,16 @@ */ package com.redhat.exhort.image; +import static com.redhat.exhort.image.ImageUtils.getImageDigests; +import static com.redhat.exhort.image.ImageUtils.getImagePlatform; + import com.fasterxml.jackson.core.JsonProcessingException; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; - import java.util.Map; import java.util.Objects; import java.util.TreeMap; -import static com.redhat.exhort.image.ImageUtils.getImageDigests; -import static com.redhat.exhort.image.ImageUtils.getImagePlatform; - public class ImageRef { public static final String OCI_TYPE = "oci"; @@ -109,10 +108,7 @@ public int hashCode() { @Override public String toString() { - return "ImageRef{" + - "image='" + image + '\'' + - ", platform='" + platform + '\'' + - '}'; + return "ImageRef{" + "image='" + image + '\'' + ", platform='" + platform + '\'' + '}'; } void checkImageDigest() { @@ -132,7 +128,8 @@ void checkImageDigest() { throw new RuntimeException("Failed to get image platform for image digest"); } if (!digests.containsKey(this.platform)) { - throw new RuntimeException(String.format("Failed to get image digest for platform %s", this.platform)); + throw new RuntimeException( + String.format("Failed to get image digest for platform %s", this.platform)); } this.image.setDigest(digests.get(this.platform)); } @@ -162,11 +159,12 @@ public PackageURL getPackageURL() throws MalformedPackageURLException { qualifiers.put(TAG_QUALIFIER, tag); } - return new PackageURL(OCI_TYPE, - null, - this.image.getSimpleName().toLowerCase(), - image.getDigest().toLowerCase(), - qualifiers, - null); + return new PackageURL( + OCI_TYPE, + null, + this.image.getSimpleName().toLowerCase(), + image.getDigest().toLowerCase(), + qualifiers, + null); } } diff --git a/src/main/java/com/redhat/exhort/image/ImageUtils.java b/src/main/java/com/redhat/exhort/image/ImageUtils.java index 75b5f822..8268393e 100644 --- a/src/main/java/com/redhat/exhort/image/ImageUtils.java +++ b/src/main/java/com/redhat/exhort/image/ImageUtils.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.image; +import static com.redhat.exhort.image.Platform.EMPTY_PLATFORM; +import static com.redhat.exhort.impl.ExhortApi.getStringValueEnvironment; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,7 +26,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.redhat.exhort.logging.LoggersFactory; import com.redhat.exhort.tools.Operations; - import java.io.File; import java.io.IOException; import java.util.AbstractMap; @@ -39,9 +40,6 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import static com.redhat.exhort.image.Platform.EMPTY_PLATFORM; -import static com.redhat.exhort.impl.ExhortApi.getStringValueEnvironment; - public class ImageUtils { static final String EXHORT_SYFT_CONFIG_PATH = "EXHORT_SYFT_CONFIG_PATH"; @@ -52,45 +50,49 @@ public class ImageUtils { static final String EXHORT_IMAGE_VARIANT = "EXHORT_IMAGE_VARIANT"; static final String EXHORT_SKOPEO_CONFIG_PATH = "EXHORT_SKOPEO_CONFIG_PATH"; static final String EXHORT_IMAGE_SERVICE_ENDPOINT = "EXHORT_IMAGE_SERVICE_ENDPOINT"; - private static final String MEDIA_TYPE_DOCKER2_MANIFEST = "application/vnd.docker.distribution.manifest.v2+json"; - private static final String MEDIA_TYPE_DOCKER2_MANIFEST_LIST = "application/vnd.docker.distribution.manifest.list.v2+json"; - private static final String MEDIA_TYPE_OCI1_MANIFEST = "application/vnd.oci.image.manifest.v1+json"; - private static final String MEDIA_TYPE_OCI1_MANIFEST_LIST = "application/vnd.oci.image.index.v1+json"; + private static final String MEDIA_TYPE_DOCKER2_MANIFEST = + "application/vnd.docker.distribution.manifest.v2+json"; + private static final String MEDIA_TYPE_DOCKER2_MANIFEST_LIST = + "application/vnd.docker.distribution.manifest.list.v2+json"; + private static final String MEDIA_TYPE_OCI1_MANIFEST = + "application/vnd.oci.image.manifest.v1+json"; + private static final String MEDIA_TYPE_OCI1_MANIFEST_LIST = + "application/vnd.oci.image.index.v1+json"; private static final Logger logger = LoggersFactory.getLogger(ImageUtils.class.getName()); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final Map archMapping = Map.ofEntries( - new AbstractMap.SimpleEntry<>("amd64", "amd64"), - new AbstractMap.SimpleEntry<>("x86_64", "amd64"), - new AbstractMap.SimpleEntry<>("armv5tl", "arm"), - new AbstractMap.SimpleEntry<>("armv5tel", "arm"), - new AbstractMap.SimpleEntry<>("armv5tejl", "arm"), - new AbstractMap.SimpleEntry<>("armv6l", "arm"), - new AbstractMap.SimpleEntry<>("armv7l", "arm"), - new AbstractMap.SimpleEntry<>("armv7ml", "arm"), - new AbstractMap.SimpleEntry<>("arm64", "arm64"), - new AbstractMap.SimpleEntry<>("aarch64", "arm64"), - new AbstractMap.SimpleEntry<>("i386", "386"), - new AbstractMap.SimpleEntry<>("i486", "386"), - new AbstractMap.SimpleEntry<>("i586", "386"), - new AbstractMap.SimpleEntry<>("i686", "386"), - new AbstractMap.SimpleEntry<>("mips64le", "mips64le"), - new AbstractMap.SimpleEntry<>("ppc64le", "ppc64le"), - new AbstractMap.SimpleEntry<>("riscv64", "riscv64"), - new AbstractMap.SimpleEntry<>("s390x", "s390x") - ); - private static final Map variantMapping = Map.ofEntries( - new AbstractMap.SimpleEntry<>("armv5tl", "v5"), - new AbstractMap.SimpleEntry<>("armv5tel", "v5"), - new AbstractMap.SimpleEntry<>("armv5tejl", "v5"), - new AbstractMap.SimpleEntry<>("armv6l", "v6"), - new AbstractMap.SimpleEntry<>("armv7l", "v7"), - new AbstractMap.SimpleEntry<>("armv7ml", "v7"), - new AbstractMap.SimpleEntry<>("arm64", "v8"), - new AbstractMap.SimpleEntry<>("aarch64", "v8") - ); + private static final Map archMapping = + Map.ofEntries( + new AbstractMap.SimpleEntry<>("amd64", "amd64"), + new AbstractMap.SimpleEntry<>("x86_64", "amd64"), + new AbstractMap.SimpleEntry<>("armv5tl", "arm"), + new AbstractMap.SimpleEntry<>("armv5tel", "arm"), + new AbstractMap.SimpleEntry<>("armv5tejl", "arm"), + new AbstractMap.SimpleEntry<>("armv6l", "arm"), + new AbstractMap.SimpleEntry<>("armv7l", "arm"), + new AbstractMap.SimpleEntry<>("armv7ml", "arm"), + new AbstractMap.SimpleEntry<>("arm64", "arm64"), + new AbstractMap.SimpleEntry<>("aarch64", "arm64"), + new AbstractMap.SimpleEntry<>("i386", "386"), + new AbstractMap.SimpleEntry<>("i486", "386"), + new AbstractMap.SimpleEntry<>("i586", "386"), + new AbstractMap.SimpleEntry<>("i686", "386"), + new AbstractMap.SimpleEntry<>("mips64le", "mips64le"), + new AbstractMap.SimpleEntry<>("ppc64le", "ppc64le"), + new AbstractMap.SimpleEntry<>("riscv64", "riscv64"), + new AbstractMap.SimpleEntry<>("s390x", "s390x")); + private static final Map variantMapping = + Map.ofEntries( + new AbstractMap.SimpleEntry<>("armv5tl", "v5"), + new AbstractMap.SimpleEntry<>("armv5tel", "v5"), + new AbstractMap.SimpleEntry<>("armv5tejl", "v5"), + new AbstractMap.SimpleEntry<>("armv6l", "v6"), + new AbstractMap.SimpleEntry<>("armv7l", "v7"), + new AbstractMap.SimpleEntry<>("armv7ml", "v7"), + new AbstractMap.SimpleEntry<>("arm64", "v8"), + new AbstractMap.SimpleEntry<>("aarch64", "v8")); static String updatePATHEnv(String execPath) { String path = System.getenv("PATH"); @@ -101,7 +103,8 @@ static String updatePATHEnv(String execPath) { } } - public static JsonNode generateImageSBOM(ImageRef imageRef) throws IOException, MalformedPackageURLException { + public static JsonNode generateImageSBOM(ImageRef imageRef) + throws IOException, MalformedPackageURLException { var output = execSyft(imageRef); if (!output.getError().isEmpty() || output.getExitCode() != 0) { @@ -121,7 +124,8 @@ public static JsonNode generateImageSBOM(ImageRef imageRef) throws IOException, } } - throw new RuntimeException(String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); + throw new RuntimeException( + String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); } static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { @@ -133,27 +137,57 @@ static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); SyftImageSource.getImageSource(imageSource); - var dockerPath = docker != null && docker.contains(File.separator) ? - docker.substring(0, docker.lastIndexOf(File.separator) + 1) : ""; - var podmanPath = podman != null && podman.contains(File.separator) ? - podman.substring(0, podman.lastIndexOf(File.separator) + 1) : ""; + var dockerPath = + docker != null && docker.contains(File.separator) + ? docker.substring(0, docker.lastIndexOf(File.separator) + 1) + : ""; + var podmanPath = + podman != null && podman.contains(File.separator) + ? podman.substring(0, podman.lastIndexOf(File.separator) + 1) + : ""; var envs = getSyftEnvs(dockerPath, podmanPath); var scheme = imageRef.getImage().toString(); String[] cmd; if (!imageSource.isEmpty()) { - cmd = syftConfigPath.isEmpty() ? - new String[]{syft, scheme, "--from", imageSource, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} : - new String[]{syft, scheme, "--from", imageSource, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q"}; + cmd = + syftConfigPath.isEmpty() + ? new String[] { + syft, + scheme, + "--from", + imageSource, + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + } + : new String[] { + syft, + scheme, + "--from", + imageSource, + "-c", + syftConfigPath, + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }; } else { - cmd = syftConfigPath.isEmpty() ? - new String[]{syft, scheme, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} : - new String[]{syft, scheme, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q"}; + cmd = + syftConfigPath.isEmpty() + ? new String[] {syft, scheme, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} + : new String[] { + syft, scheme, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q" + }; } - return Operations.runProcessGetFullOutput(null, cmd, - envs.isEmpty() ? null : envs.toArray(new String[1])); + return Operations.runProcessGetFullOutput( + null, cmd, envs.isEmpty() ? null : envs.toArray(new String[1])); } static List getSyftEnvs(String dockerPath, String podmanPath) { @@ -210,19 +244,21 @@ public static Platform getImagePlatform() { static String hostInfo(String engine, String info) { var exec = Operations.getCustomPathOrElse(engine); - var cmd = new String[]{exec, "info"}; + var cmd = new String[] {exec, "info"}; var output = Operations.runProcessGetFullOutput(null, cmd, null); - if (output.getOutput().isEmpty() && (!output.getError().isEmpty() || output.getExitCode() != 0)) { + if (output.getOutput().isEmpty() + && (!output.getError().isEmpty() || output.getExitCode() != 0)) { throw new RuntimeException(output.getError()); } - return output.getOutput() - .lines() - .filter(line -> line.stripLeading().startsWith(info + ":")) - .map(line -> line.strip().substring(info.length() + 1).strip()) - .findAny() - .orElse(""); + return output + .getOutput() + .lines() + .filter(line -> line.stripLeading().startsWith(info + ":")) + .map(line -> line.strip().substring(info.length() + 1).strip()) + .findAny() + .orElse(""); } static String dockerGetOs() { @@ -261,7 +297,8 @@ static String dockerPodmanInfo(Supplier dockerSupplier, Supplier return info; } - public static Map getImageDigests(ImageRef imageRef) throws JsonProcessingException { + public static Map getImageDigests(ImageRef imageRef) + throws JsonProcessingException { var output = execSkopeoInspect(imageRef, true); if (!output.getError().isEmpty() || output.getExitCode() != 0) { @@ -293,23 +330,23 @@ static Map getMultiImageDigests(JsonNode node) { var manifestsNode = node.get("manifests"); if (manifestsNode.isArray()) { return StreamSupport.stream(manifestsNode.spliterator(), false) - .filter(ImageUtils::filterMediaType) - .filter(ImageUtils::filterDigest) - .filter(ImageUtils::filterPlatform) - .collect(Collectors.toMap( - manifestNode -> { - var platformNode = manifestNode.get("platform"); - var arch = platformNode.get("architecture").asText(); - var os = platformNode.get("os").asText(); - if (platformNode.hasNonNull("variant")) { - var variant = platformNode.get("variant").asText(); - return new Platform(String.format("%s/%s/%s", os, arch, variant)); - } else { - return new Platform(String.format("%s/%s", os, arch)); - } - }, - manifestNode -> manifestNode.get("digest").asText() - )); + .filter(ImageUtils::filterMediaType) + .filter(ImageUtils::filterDigest) + .filter(ImageUtils::filterPlatform) + .collect( + Collectors.toMap( + manifestNode -> { + var platformNode = manifestNode.get("platform"); + var arch = platformNode.get("architecture").asText(); + var os = platformNode.get("os").asText(); + if (platformNode.hasNonNull("variant")) { + var variant = platformNode.get("variant").asText(); + return new Platform(String.format("%s/%s/%s", os, arch, variant)); + } else { + return new Platform(String.format("%s/%s", os, arch)); + } + }, + manifestNode -> manifestNode.get("digest").asText())); } } return Collections.emptyMap(); @@ -320,7 +357,8 @@ static boolean filterMediaType(JsonNode manifestNode) { var mediaTypeNode = manifestNode.get("mediaType"); if (mediaTypeNode.isTextual()) { var mediaType = mediaTypeNode.asText(); - return MEDIA_TYPE_OCI1_MANIFEST.equals(mediaType) || MEDIA_TYPE_DOCKER2_MANIFEST.equals(mediaType); + return MEDIA_TYPE_OCI1_MANIFEST.equals(mediaType) + || MEDIA_TYPE_DOCKER2_MANIFEST.equals(mediaType); } } return false; @@ -346,7 +384,10 @@ static boolean filterPlatform(JsonNode manifestNode) { var variantNode = platformNode.get("variant"); if (variantNode.isTextual()) { try { - new Platform(String.format("%s/%s/%s", osNode.asText(), architectureNode.asText(), variantNode.asText())); + new Platform( + String.format( + "%s/%s/%s", + osNode.asText(), architectureNode.asText(), variantNode.asText())); } catch (IllegalArgumentException e) { return false; } @@ -366,7 +407,8 @@ static boolean filterPlatform(JsonNode manifestNode) { return false; } - static Map getSingleImageDigest(ImageRef imageRef) throws JsonProcessingException { + static Map getSingleImageDigest(ImageRef imageRef) + throws JsonProcessingException { var output = execSkopeoInspect(imageRef, false); if (!output.getError().isEmpty() || output.getExitCode() != 0) { @@ -392,49 +434,74 @@ static Operations.ProcessExecOutput execSkopeoInspect(ImageRef imageRef, boolean String[] cmd; if (daemonHost.isEmpty()) { - cmd = configPath.isEmpty() ? - new String[]{skopeo, "inspect", raw ? "--raw" : "", - String.format("docker://%s", imageRef.getImage().getFullName())} : - new String[]{skopeo, "inspect", "--authfile", configPath, raw ? "--raw" : "", - String.format("docker://%s", imageRef.getImage().getFullName())}; + cmd = + configPath.isEmpty() + ? new String[] { + skopeo, + "inspect", + raw ? "--raw" : "", + String.format("docker://%s", imageRef.getImage().getFullName()) + } + : new String[] { + skopeo, + "inspect", + "--authfile", + configPath, + raw ? "--raw" : "", + String.format("docker://%s", imageRef.getImage().getFullName()) + }; } else { - cmd = configPath.isEmpty() ? - new String[]{skopeo, "inspect", "--daemon-host", daemonHost, raw ? "--raw" : "", - String.format("docker-daemon:%s", imageRef.getImage().getFullName())} : - new String[]{skopeo, "inspect", "--authfile", configPath, "--daemon-host", daemonHost, raw ? "--raw" : "", - String.format("docker-daemon:%s", imageRef.getImage().getFullName())}; + cmd = + configPath.isEmpty() + ? new String[] { + skopeo, + "inspect", + "--daemon-host", + daemonHost, + raw ? "--raw" : "", + String.format("docker-daemon:%s", imageRef.getImage().getFullName()) + } + : new String[] { + skopeo, + "inspect", + "--authfile", + configPath, + "--daemon-host", + daemonHost, + raw ? "--raw" : "", + String.format("docker-daemon:%s", imageRef.getImage().getFullName()) + }; } return Operations.runProcessGetFullOutput(null, cmd, null); } private enum SyftImageSource { - DEFAULT("", - () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), - () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), - () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), - REGISTRY("registry", - () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), - () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), - () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), - DOCKER("docker", - ImageUtils::dockerGetOs, - ImageUtils::dockerGetArch, - ImageUtils::dockerGetVariant), - PODMAN("podman", - ImageUtils::podmanGetOs, - ImageUtils::podmanGetArch, - ImageUtils::podmanGetVariant); + DEFAULT( + "", + () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), + () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), + () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), + REGISTRY( + "registry", + () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), + () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), + () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), + DOCKER( + "docker", ImageUtils::dockerGetOs, ImageUtils::dockerGetArch, ImageUtils::dockerGetVariant), + PODMAN( + "podman", ImageUtils::podmanGetOs, ImageUtils::podmanGetArch, ImageUtils::podmanGetVariant); private final String name; private final Supplier osSupplier; private final Supplier archSupplier; private final Supplier variantSupplier; - SyftImageSource(String name, - Supplier osSupplier, - Supplier archSupplier, - Supplier variantSupplier) { + SyftImageSource( + String name, + Supplier osSupplier, + Supplier archSupplier, + Supplier variantSupplier) { this.name = name; this.osSupplier = osSupplier; this.archSupplier = archSupplier; @@ -443,9 +510,12 @@ private enum SyftImageSource { static SyftImageSource getImageSource(String name) { return EnumSet.allOf(SyftImageSource.class).stream() - .filter(s -> s.name.equals(name)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException(String.format("The image source for syft is not valid: %s", name))); + .filter(s -> s.name.equals(name)) + .findAny() + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("The image source for syft is not valid: %s", name))); } String getOs() { @@ -461,4 +531,3 @@ String getVariant() { } } } - diff --git a/src/main/java/com/redhat/exhort/image/Platform.java b/src/main/java/com/redhat/exhort/image/Platform.java index d2636a96..70a7deb6 100644 --- a/src/main/java/com/redhat/exhort/image/Platform.java +++ b/src/main/java/com/redhat/exhort/image/Platform.java @@ -22,7 +22,8 @@ public class Platform { // $GOOS and $GOARCH // https://github.com/docker-library/bashbrew/blob/v0.1.2/architecture/oci-platform.go#L14-L27 - private static final Set SUPPORTED_PLATFORMS = Set.of( + private static final Set SUPPORTED_PLATFORMS = + Set.of( new Platform().os("linux").arch("amd64"), new Platform().os("linux").arch("arm").variant("v5"), new Platform().os("linux").arch("arm").variant("v6"), @@ -33,9 +34,7 @@ public class Platform { new Platform().os("linux").arch("ppc64le"), new Platform().os("linux").arch("riscv64"), new Platform().os("linux").arch("s390x"), - - new Platform().os("windows").arch("arm64") - ); + new Platform().os("windows").arch("arm64")); public static final Platform EMPTY_PLATFORM = new Platform(); @@ -43,8 +42,7 @@ public class Platform { private String architecture; private String variant; - private Platform() { - } + private Platform() {} public Platform(String platform) { if (platform == null) { @@ -68,7 +66,8 @@ public Platform(String platform) { } if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException(String.format("Image platform is not supported: %s", platform)); + throw new IllegalArgumentException( + String.format("Image platform is not supported: %s", platform)); } } @@ -91,7 +90,8 @@ public Platform(String os, String arch, String variant) { } if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException(String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); + throw new IllegalArgumentException( + String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); } } @@ -138,7 +138,9 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Platform platform = (Platform) o; - return Objects.equals(os, platform.os) && Objects.equals(architecture, platform.architecture) && Objects.equals(variant, platform.variant); + return Objects.equals(os, platform.os) + && Objects.equals(architecture, platform.architecture) + && Objects.equals(variant, platform.variant); } @Override diff --git a/src/main/java/com/redhat/exhort/impl/ExhortApi.java b/src/main/java/com/redhat/exhort/impl/ExhortApi.java index 25f3012c..ddca1efe 100644 --- a/src/main/java/com/redhat/exhort/impl/ExhortApi.java +++ b/src/main/java/com/redhat/exhort/impl/ExhortApi.java @@ -15,6 +15,22 @@ */ package com.redhat.exhort.impl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.Api; +import com.redhat.exhort.Provider; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; +import com.redhat.exhort.image.ImageUtils; +import com.redhat.exhort.logging.LoggersFactory; +import com.redhat.exhort.tools.Ecosystem; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -43,35 +59,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.packageurl.MalformedPackageURLException; -import com.github.packageurl.PackageURL; -import com.redhat.exhort.Api; -import com.redhat.exhort.Provider; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; -import com.redhat.exhort.image.ImageUtils; -import com.redhat.exhort.logging.LoggersFactory; -import com.redhat.exhort.tools.Ecosystem; - -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMultipart; -import jakarta.mail.util.ByteArrayDataSource; - -/** - * Concrete implementation of the Exhort {@link Api} Service. - **/ +/** Concrete implementation of the Exhort {@link Api} Service. */ public final class ExhortApi implements Api { -// private static final System.Logger LOG = System.getLogger(ExhortApi.class.getName()); + // private static final System.Logger LOG = System.getLogger(ExhortApi.class.getName()); private static final Logger LOG = LoggersFactory.getLogger(ExhortApi.class.getName()); - - public static final String DEFAULT_ENDPOINT = "https://rhda.rhcloud.com"; public static final String DEFAULT_ENDPOINT_DEV = "https://exhort.stage.devshift.net"; public static final String RHDA_TOKEN_HEADER = "rhda-token"; @@ -85,27 +79,31 @@ public String getEndpoint() { return endpoint; } - public static final void main(String[] args) throws IOException, InterruptedException, ExecutionException { + public static final void main(String[] args) + throws IOException, InterruptedException, ExecutionException { System.setProperty("EXHORT_DEV_MODE", "true"); - AnalysisReport analysisReport = new ExhortApi() - .stackAnalysisMixed("/tmp/exhort_test_10582748308498949664/pom.xml").get().json; -// ObjectMapper om = new ObjectMapper().configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); -// System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(analysisReport)); -// AnalysisReport analysisReport = new ExhortApi() -// byte[] analysisReport = new ExhortApi(). -// stackAnalysisHtml("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_with_one_ignored_prefix_go/go.mod").get(); -// Path html = Files.createFile(Path.of("/","tmp", "golang0210.html")); -// Files.write(html,analysisReport); - - } - - /** - * Enum for identifying token environment variables and their - * corresponding request headers. - */ + AnalysisReport analysisReport = + new ExhortApi() + .stackAnalysisMixed("/tmp/exhort_test_10582748308498949664/pom.xml") + .get() + .json; + // ObjectMapper om = new + // ObjectMapper().configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); + // + // System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(analysisReport)); + // AnalysisReport analysisReport = new ExhortApi() + // byte[] analysisReport = new ExhortApi(). + // + // stackAnalysisHtml("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_with_one_ignored_prefix_go/go.mod").get(); + // Path html = Files.createFile(Path.of("/","tmp", "golang0210.html")); + // Files.write(html,analysisReport); + + } + + /** Enum for identifying token environment variables and their corresponding request headers. */ private enum TokenProvider { - SNYK, OSS_INDEX; - + SNYK, + OSS_INDEX; /** * Get the expected environment variable name. @@ -146,26 +144,35 @@ public ExhortApi() { } /** - * Get the HTTP protocol Version set by client in environment variable, if not set, the default is HTTP Protocol Version 1.1 + * Get the HTTP protocol Version set by client in environment variable, if not set, the default is + * HTTP Protocol Version 1.1 * * @return i.e. HttpClient.Version.HTTP_1.1 */ static HttpClient.Version getHttpVersion() { - return (System.getenv("HTTP_VERSION_EXHORT_CLIENT") != null && System.getenv("HTTP_VERSION_EXHORT_CLIENT").contains("2")) ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1; + return (System.getenv("HTTP_VERSION_EXHORT_CLIENT") != null + && System.getenv("HTTP_VERSION_EXHORT_CLIENT").contains("2")) + ? HttpClient.Version.HTTP_2 + : HttpClient.Version.HTTP_1_1; } ExhortApi(final HttpClient client) { -// // temp system property - as long as prod exhort url not implemented the multi-source v4 endpoint, this property needs to be true -// System.setProperty("EXHORT_DEV_MODE","true"); + // // temp system property - as long as prod exhort url not implemented the multi-source v4 + // endpoint, this + // property needs to be true + // System.setProperty("EXHORT_DEV_MODE","true"); commonHookBeginning(true); this.client = client; this.mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // Take default from config.properties in case client didn't override DEV MODE if (System.getProperty("EXHORT_DEV_MODE") == null) { try { - InputStream exhortConfig = this.getClass().getClassLoader().getResourceAsStream("config.properties"); + InputStream exhortConfig = + this.getClass().getClassLoader().getResourceAsStream("config.properties"); if (exhortConfig == null) { - LOG.info("config.properties not found on the class path, fallback to default DEV MODE = false"); + LOG.info( + "config.properties not found on the class path, fallback to default DEV MODE =" + + " false"); System.setProperty("EXHORT_DEV_MODE", "false"); } else { Properties properties = new Properties(); @@ -173,7 +180,11 @@ static HttpClient.Version getHttpVersion() { System.setProperty("EXHORT_DEV_MODE", (String) properties.get("EXHORT_DEV_MODE")); } } catch (IOException e) { - LOG.info(String.format("Error loading config.properties , fallback to set default property DEV MODE = false, Error message = %s", e.getMessage())); + LOG.info( + String.format( + "Error loading config.properties , fallback to set default property DEV MODE =" + + " false, Error message = %s", + e.getMessage())); System.setProperty("EXHORT_DEV_MODE", "false"); } } @@ -182,14 +193,13 @@ static HttpClient.Version getHttpVersion() { } private String commonHookBeginning(boolean startOfApi) { - if(startOfApi) { + if (startOfApi) { generateClientRequestId(); if (debugLoggingIsNeeded()) { LOG.info("Start of exhort-java-api client"); } - } - else { - if(Objects.isNull(getClientRequestId())) { + } else { + if (Objects.isNull(getClientRequestId())) { generateClientRequestId(); } if (debugLoggingIsNeeded()) { @@ -219,97 +229,159 @@ public String getExhortUrl() { endpoint = DEFAULT_ENDPOINT; } if (debugLoggingIsNeeded()) { - LOG.info(String.format("EXHORT_DEV_MODE=%s,DEV_EXHORT_BACKEND_URL=%s, Chosen Backend URL=%s , DEFAULT_ENDPOINT_DEV=%s , DEFAULT_ENDPOINT=%s", getBooleanValueEnvironment("EXHORT_DEV_MODE", "false"), getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV), endpoint, DEFAULT_ENDPOINT_DEV, DEFAULT_ENDPOINT)); + LOG.info( + String.format( + "EXHORT_DEV_MODE=%s,DEV_EXHORT_BACKEND_URL=%s, Chosen Backend URL=%s ," + + " DEFAULT_ENDPOINT_DEV=%s , DEFAULT_ENDPOINT=%s", + getBooleanValueEnvironment("EXHORT_DEV_MODE", "false"), + getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV), + endpoint, + DEFAULT_ENDPOINT_DEV, + DEFAULT_ENDPOINT)); } return endpoint; } public static boolean getBooleanValueEnvironment(String key, String defaultValue) { - String result = Objects.requireNonNullElse(System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); + String result = + Objects.requireNonNullElse( + System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); return Boolean.parseBoolean(result.trim().toLowerCase()); } public static String getStringValueEnvironment(String key, String defaultValue) { - String result = Objects.requireNonNullElse(System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); + String result = + Objects.requireNonNullElse( + System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); return result; } @Override - public CompletableFuture stackAnalysisMixed(final String manifestFile) throws IOException { + public CompletableFuture stackAnalysisMixed(final String manifestFile) + throws IOException { String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.MULTIPART_MIXED), HttpResponse.BodyHandlers.ofByteArray()).thenApply(resp -> { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if(debugLoggingIsNeeded()) { - logExhortRequestId(resp); - } - if (resp.statusCode() == 200) { - byte[] htmlPart = null; - AnalysisReport jsonPart = null; - var ds = new ByteArrayDataSource(resp.body(), MediaType.MULTIPART_MIXED.toString()); - try { - var mp = new MimeMultipart(ds); - for (var i = 0; i < mp.getCount(); i++) { - if (Objects.isNull(htmlPart) && MediaType.TEXT_HTML.toString().equals(mp.getBodyPart(i).getContentType())) { - htmlPart = mp.getBodyPart(i).getInputStream().readAllBytes(); - } - if (Objects.isNull(jsonPart) && MediaType.APPLICATION_JSON.toString().equals(mp.getBodyPart(i).getContentType())) { - jsonPart = this.mapper.readValue(mp.getBodyPart(i).getInputStream().readAllBytes(), AnalysisReport.class); - } - } - } catch (IOException | MessagingException e) { - throw new RuntimeException(e); - } - commonHookAfterExhortResponse(); - return new MixedReport(Objects.requireNonNull(htmlPart), Objects.requireNonNull(jsonPart)); - } else { - LOG.severe(String.format("failed to invoke stackAnalysisMixed for getting the html and json reports, Http Response Status=%s , received message from server= %s ", resp.statusCode(), new String(resp.body()))); - return new MixedReport(); - } - }); + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.MULTIPART_MIXED), + HttpResponse.BodyHandlers.ofByteArray()) + .thenApply( + resp -> { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(resp); + } + if (resp.statusCode() == 200) { + byte[] htmlPart = null; + AnalysisReport jsonPart = null; + var ds = new ByteArrayDataSource(resp.body(), MediaType.MULTIPART_MIXED.toString()); + try { + var mp = new MimeMultipart(ds); + for (var i = 0; i < mp.getCount(); i++) { + if (Objects.isNull(htmlPart) + && MediaType.TEXT_HTML + .toString() + .equals(mp.getBodyPart(i).getContentType())) { + htmlPart = mp.getBodyPart(i).getInputStream().readAllBytes(); + } + if (Objects.isNull(jsonPart) + && MediaType.APPLICATION_JSON + .toString() + .equals(mp.getBodyPart(i).getContentType())) { + jsonPart = + this.mapper.readValue( + mp.getBodyPart(i).getInputStream().readAllBytes(), + AnalysisReport.class); + } + } + } catch (IOException | MessagingException e) { + throw new RuntimeException(e); + } + commonHookAfterExhortResponse(); + return new MixedReport( + Objects.requireNonNull(htmlPart), Objects.requireNonNull(jsonPart)); + } else { + LOG.severe( + String.format( + "failed to invoke stackAnalysisMixed for getting the html and json reports," + + " Http Response Status=%s , received message from server= %s ", + resp.statusCode(), new String(resp.body()))); + return new MixedReport(); + } + }); } @Override public CompletableFuture stackAnalysisHtml(final String manifestFile) throws IOException { String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.TEXT_HTML), HttpResponse.BodyHandlers.ofByteArray()).thenApply(httpResponse -> { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if(debugLoggingIsNeeded()) { - logExhortRequestId(httpResponse); - } - if (httpResponse.statusCode() != 200) { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the html report, Http Response Status=%s , received message from server= %s ", httpResponse.statusCode(), new String(httpResponse.body()))); - } - commonHookAfterExhortResponse(); - return httpResponse.body(); - }).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the html report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - commonHookAfterExhortResponse(); - return new byte[0]; - }); + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.TEXT_HTML), + HttpResponse.BodyHandlers.ofByteArray()) + .thenApply( + httpResponse -> { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(httpResponse); + } + if (httpResponse.statusCode() != 200) { + LOG.severe( + String.format( + "failed to invoke stackAnalysis for getting the html report, Http Response" + + " Status=%s , received message from server= %s ", + httpResponse.statusCode(), new String(httpResponse.body()))); + } + commonHookAfterExhortResponse(); + return httpResponse.body(); + }) + .exceptionally( + exception -> { + LOG.severe( + String.format( + "failed to invoke stackAnalysis for getting the html report, received" + + " message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + commonHookAfterExhortResponse(); + return new byte[0]; + }); } @Override - public CompletableFuture stackAnalysis(final String manifestFile) throws IOException { + public CompletableFuture stackAnalysis(final String manifestFile) + throws IOException { String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON), HttpResponse.BodyHandlers.ofString()) -// .thenApply(HttpResponse::body) - .thenApply(response -> getAnalysisReportFromResponse(response, "StackAnalysis", "json",exClientTraceId)).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the json report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - return new AnalysisReport(); - }); - } - - private AnalysisReport getAnalysisReportFromResponse(HttpResponse response, String operation, String reportName,String exClientTraceId) { + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON), + HttpResponse.BodyHandlers.ofString()) + // .thenApply(HttpResponse::body) + .thenApply( + response -> + getAnalysisReportFromResponse(response, "StackAnalysis", "json", exClientTraceId)) + .exceptionally( + exception -> { + LOG.severe( + String.format( + "failed to invoke stackAnalysis for getting the json report, received" + + " message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + return new AnalysisReport(); + }); + } + + private AnalysisReport getAnalysisReportFromResponse( + HttpResponse response, String operation, String reportName, String exClientTraceId) { RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); if (debugLoggingIsNeeded()) { logExhortRequestId(response); } if (response.statusCode() == 200) { if (debugLoggingIsNeeded()) { - LOG.info(String.format("Response body received from exhort server : %s %s", System.lineSeparator(), response.body())); - + LOG.info( + String.format( + "Response body received from exhort server : %s %s", + System.lineSeparator(), response.body())); } commonHookAfterExhortResponse(); try { @@ -320,55 +392,77 @@ private AnalysisReport getAnalysisReportFromResponse(HttpResponse respon } } else { - LOG.severe(String.format("failed to invoke %s for getting the %s report, Http Response Status=%s , received message from server= %s ", operation, reportName, response.statusCode(), response.body())); + LOG.severe( + String.format( + "failed to invoke %s for getting the %s report, Http Response Status=%s , received" + + " message from server= %s ", + operation, reportName, response.statusCode(), response.body())); return new AnalysisReport(); } - } private static void logExhortRequestId(HttpResponse response) { - Optional headerExRequestId = response.headers().allValues(EXHORT_REQUEST_ID_HEADER_NAME).stream().findFirst(); - headerExRequestId.ifPresent(value -> LOG.info(String.format("Unique Identifier associated with this request ( Received from Exhort Backend ) - ex-request-id= : %s", value))); + Optional headerExRequestId = + response.headers().allValues(EXHORT_REQUEST_ID_HEADER_NAME).stream().findFirst(); + headerExRequestId.ifPresent( + value -> + LOG.info( + String.format( + "Unique Identifier associated with this request ( Received from Exhort Backend" + + " ) - ex-request-id= : %s", + value))); } public static boolean debugLoggingIsNeeded() { - return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG","false")); + return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG", "false")); } @Override - public CompletableFuture componentAnalysis(final String manifestType, final byte[] manifestContent) throws IOException { + public CompletableFuture componentAnalysis( + final String manifestType, final byte[] manifestContent) throws IOException { String exClientTraceId = commonHookBeginning(false); var provider = Ecosystem.getProvider(manifestType); var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); var content = provider.provideComponent(manifestContent); commonHookAfterProviderCreatedSbomAndBeforeExhort(); - return getAnalysisReportForComponent(uri, content,exClientTraceId); + return getAnalysisReportForComponent(uri, content, exClientTraceId); } private void commonHookAfterProviderCreatedSbomAndBeforeExhort() { - if(debugLoggingIsNeeded()) { + if (debugLoggingIsNeeded()) { LOG.info("After Provider created sbom hook"); this.providerEndTime = LocalDateTime.now(); LOG.info(String.format("After Creating Sbom time: %s", this.startTime)); - LOG.info(String.format("Time took to create sbom file to be sent to exhort backend, in ms : %s, in seconds: %s",this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS),(float)(this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS) / 1000F))); + LOG.info( + String.format( + "Time took to create sbom file to be sent to exhort backend, in ms : %s, in seconds:" + + " %s", + this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS), + (float) (this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS) / 1000F))); } - - - } private void commonHookAfterExhortResponse() { - if(debugLoggingIsNeeded()) { + if (debugLoggingIsNeeded()) { this.endTime = LocalDateTime.now(); LOG.info(String.format("After got response from exhort time: %s", this.endTime)); - LOG.info(String.format("Time took to get response from exhort backend, in ms: %s, in seconds: %s",this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS),this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); - LOG.info(String.format("Total time took for complete analysis, in ms: %s, in seconds: %s",this.startTime.until(this.endTime, ChronoUnit.MILLIS),this.startTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); - + LOG.info( + String.format( + "Time took to get response from exhort backend, in ms: %s, in seconds: %s", + this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS), + this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); + LOG.info( + String.format( + "Total time took for complete analysis, in ms: %s, in seconds: %s", + this.startTime.until(this.endTime, ChronoUnit.MILLIS), + this.startTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); } RequestManager.getInstance().removeClientTraceIdFromRequest(); } + @Override - public CompletableFuture componentAnalysis(String manifestFile) throws IOException { + public CompletableFuture componentAnalysis(String manifestFile) + throws IOException { String exClientTraceId = commonHookBeginning(false); var manifestPath = Paths.get(manifestFile); var provider = Ecosystem.getProvider(manifestPath); @@ -378,25 +472,39 @@ public CompletableFuture componentAnalysis(String manifestFile) return getAnalysisReportForComponent(uri, content, exClientTraceId); } - private CompletableFuture getAnalysisReportForComponent(URI uri, Provider.Content content, String exClientTraceId) { - return this.client.sendAsync(this.buildRequest(content, uri, MediaType.APPLICATION_JSON, "Component Analysis"), HttpResponse.BodyHandlers.ofString()) -// .thenApply(HttpResponse::body) - .thenApply(response -> getAnalysisReportFromResponse(response, "Component Analysis", "json",exClientTraceId)).exceptionally(exception -> { - LOG.severe( String.format("failed to invoke Component Analysis for getting the json report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - return new AnalysisReport(); - }); + private CompletableFuture getAnalysisReportForComponent( + URI uri, Provider.Content content, String exClientTraceId) { + return this.client + .sendAsync( + this.buildRequest(content, uri, MediaType.APPLICATION_JSON, "Component Analysis"), + HttpResponse.BodyHandlers.ofString()) + // .thenApply(HttpResponse::body) + .thenApply( + response -> + getAnalysisReportFromResponse( + response, "Component Analysis", "json", exClientTraceId)) + .exceptionally( + exception -> { + LOG.severe( + String.format( + "failed to invoke Component Analysis for getting the json report, received" + + " message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + return new AnalysisReport(); + }); } /** * Build an HTTP request wrapper for sending to the Backend API for Stack Analysis only. * * @param manifestFile the path for the manifest file - * @param acceptType the type of requested content + * @param acceptType the type of requested content * @return a HttpRequest ready to be sent to the Backend API * @throws IOException when failed to load the manifest file */ - private HttpRequest buildStackRequest(final String manifestFile, final MediaType acceptType) throws IOException { + private HttpRequest buildStackRequest(final String manifestFile, final MediaType acceptType) + throws IOException { var manifestPath = Paths.get(manifestFile); var provider = Ecosystem.getProvider(manifestPath); var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); @@ -407,56 +515,60 @@ private HttpRequest buildStackRequest(final String manifestFile, final MediaType } @Override - public CompletableFuture> imageAnalysis(final Set imageRefs) throws IOException { + public CompletableFuture> imageAnalysis( + final Set imageRefs) throws IOException { return this.performBatchAnalysis( - () -> getBatchImageSboms(imageRefs), - MediaType.APPLICATION_JSON, - HttpResponse.BodyHandlers.ofString(), - this::getBatchImageAnalysisReports, - Collections::emptyMap, - "Image Analysis"); + () -> getBatchImageSboms(imageRefs), + MediaType.APPLICATION_JSON, + HttpResponse.BodyHandlers.ofString(), + this::getBatchImageAnalysisReports, + Collections::emptyMap, + "Image Analysis"); } @Override public CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException { return this.performBatchAnalysis( - () -> getBatchImageSboms(imageRefs), - MediaType.TEXT_HTML, - HttpResponse.BodyHandlers.ofByteArray(), - HttpResponse::body, - () -> new byte[0], - "Image Analysis"); + () -> getBatchImageSboms(imageRefs), + MediaType.TEXT_HTML, + HttpResponse.BodyHandlers.ofByteArray(), + HttpResponse::body, + () -> new byte[0], + "Image Analysis"); } Map getBatchImageSboms(final Set imageRefs) { - return imageRefs.parallelStream().map(imageRef -> { - try { - return new AbstractMap.SimpleEntry<>( - imageRef.getPackageURL().canonicalize(), - ImageUtils.generateImageSBOM(imageRef)); - } catch (IOException | MalformedPackageURLException ex) { - throw new RuntimeException(ex); - } - }).collect(Collectors.toMap( - AbstractMap.SimpleEntry::getKey, - AbstractMap.SimpleEntry::getValue - )); - } - - Map getBatchImageAnalysisReports(final HttpResponse httpResponse) { + return imageRefs.parallelStream() + .map( + imageRef -> { + try { + return new AbstractMap.SimpleEntry<>( + imageRef.getPackageURL().canonicalize(), + ImageUtils.generateImageSBOM(imageRef)); + } catch (IOException | MalformedPackageURLException ex) { + throw new RuntimeException(ex); + } + }) + .collect( + Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + + Map getBatchImageAnalysisReports( + final HttpResponse httpResponse) { if (httpResponse.statusCode() == 200) { try { Map reports = this.mapper.readValue(httpResponse.body(), Map.class); - return reports.entrySet().stream().collect(Collectors.toMap( - e -> { - try { - return new ImageRef(new PackageURL(e.getKey().toString())); - } catch (MalformedPackageURLException ex) { - throw new RuntimeException(ex); - } - }, - e -> mapper.convertValue(e.getValue(), AnalysisReport.class) - )); + return reports.entrySet().stream() + .collect( + Collectors.toMap( + e -> { + try { + return new ImageRef(new PackageURL(e.getKey().toString())); + } catch (MalformedPackageURLException ex) { + throw new RuntimeException(ex); + } + }, + e -> mapper.convertValue(e.getValue(), AnalysisReport.class))); } catch (JsonProcessingException e) { throw new CompletionException(e); } @@ -465,49 +577,62 @@ Map getBatchImageAnalysisReports(final HttpResponse CompletableFuture performBatchAnalysis(final Supplier> sbomsGenerator, - final MediaType mediaType, - final HttpResponse.BodyHandler responseBodyHandler, - final Function, T> responseGenerator, - final Supplier exceptionResponseGenerator, - final String analysisName) throws IOException { + CompletableFuture performBatchAnalysis( + final Supplier> sbomsGenerator, + final MediaType mediaType, + final HttpResponse.BodyHandler responseBodyHandler, + final Function, T> responseGenerator, + final Supplier exceptionResponseGenerator, + final String analysisName) + throws IOException { String exClientTraceId = commonHookBeginning(false); var uri = URI.create(String.format("%s/api/v4/batch-analysis", this.endpoint)); var sboms = sbomsGenerator.get(); - var content = new Provider.Content( - mapper.writeValueAsString(sboms).getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); + var content = + new Provider.Content( + mapper.writeValueAsString(sboms).getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); commonHookAfterProviderCreatedSbomAndBeforeExhort(); return this.client - .sendAsync( - this.buildRequest(content, uri, mediaType, analysisName), responseBodyHandler) - .thenApply( - response -> getBatchAnalysisReportsFromResponse(response, responseGenerator, analysisName, - "json", exClientTraceId) - ).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke %s for getting the json report, received message= %s ", - analysisName, exception.getMessage())); - commonHookAfterExhortResponse(); - return exceptionResponseGenerator.get(); - }); - } - - T getBatchAnalysisReportsFromResponse(final HttpResponse response, - final Function, T> responseGenerator, - final String operation, final String reportName, - final String exClientTraceId) { + .sendAsync(this.buildRequest(content, uri, mediaType, analysisName), responseBodyHandler) + .thenApply( + response -> + getBatchAnalysisReportsFromResponse( + response, responseGenerator, analysisName, "json", exClientTraceId)) + .exceptionally( + exception -> { + LOG.severe( + String.format( + "failed to invoke %s for getting the json report, received message= %s ", + analysisName, exception.getMessage())); + commonHookAfterExhortResponse(); + return exceptionResponseGenerator.get(); + }); + } + + T getBatchAnalysisReportsFromResponse( + final HttpResponse response, + final Function, T> responseGenerator, + final String operation, + final String reportName, + final String exClientTraceId) { RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); if (debugLoggingIsNeeded()) { logExhortRequestId(response); } if (response.statusCode() == 200) { if (debugLoggingIsNeeded()) { - LOG.info(String.format("Response body received from exhort server : %s %s", - System.lineSeparator(), response.body())); + LOG.info( + String.format( + "Response body received from exhort server : %s %s", + System.lineSeparator(), response.body())); } } else { - LOG.severe(String.format("failed to invoke %s for getting the %s report, Http Response Status=%s , " + - "received message from server= %s ", operation, reportName, response.statusCode(), response.body())); + LOG.severe( + String.format( + "failed to invoke %s for getting the %s report, Http Response Status=%s , " + + "received message from server= %s ", + operation, reportName, response.statusCode(), response.body())); } commonHookAfterExhortResponse(); return responseGenerator.apply(response); @@ -516,43 +641,53 @@ T getBatchAnalysisReportsFromResponse(final HttpResponse response, /** * Build an HTTP request for sending to the Backend API. * - * @param content the {@link com.redhat.exhort.Provider.Content} info for the request body - * @param uri the {@link URI} for sending the request to + * @param content the {@link com.redhat.exhort.Provider.Content} info for the request body + * @param uri the {@link URI} for sending the request to * @param acceptType value the Accept header in the request, indicating the required response type * @return a HttpRequest ready to be sent to the Backend API */ - private HttpRequest buildRequest(final Provider.Content content, final URI uri, final MediaType acceptType, final String analysisType) { - var request = HttpRequest.newBuilder(uri).version(Version.HTTP_1_1).setHeader("Accept", acceptType.toString()).setHeader("Content-Type", content.type).POST(HttpRequest.BodyPublishers.ofString(new String(content.buffer))); + private HttpRequest buildRequest( + final Provider.Content content, + final URI uri, + final MediaType acceptType, + final String analysisType) { + var request = + HttpRequest.newBuilder(uri) + .version(Version.HTTP_1_1) + .setHeader("Accept", acceptType.toString()) + .setHeader("Content-Type", content.type) + .POST(HttpRequest.BodyPublishers.ofString(new String(content.buffer))); // include tokens from environment variables of java properties as request headers - Stream.of(ExhortApi.TokenProvider.values()).forEach(p -> { - var envToken = System.getenv(p.getVarName()); - if (Objects.nonNull(envToken)) { - request.setHeader(p.getHeaderName(), envToken); - } else { - var propToken = System.getProperty(p.getVarName()); - if (Objects.nonNull(propToken)) { - request.setHeader(p.getHeaderName(), propToken); - } - } - var envUser = System.getenv(p.getUserHeaderName()); - if (Objects.nonNull(envUser)) { - request.setHeader(p.getUserHeaderName(), envUser); - } else { - var propUser = System.getProperty(p.getUserVarName()); - if (Objects.nonNull(propUser)) { - request.setHeader(p.getUserHeaderName(), propUser); - } - } - - }); - //set rhda-token + Stream.of(ExhortApi.TokenProvider.values()) + .forEach( + p -> { + var envToken = System.getenv(p.getVarName()); + if (Objects.nonNull(envToken)) { + request.setHeader(p.getHeaderName(), envToken); + } else { + var propToken = System.getProperty(p.getVarName()); + if (Objects.nonNull(propToken)) { + request.setHeader(p.getHeaderName(), propToken); + } + } + var envUser = System.getenv(p.getUserHeaderName()); + if (Objects.nonNull(envUser)) { + request.setHeader(p.getUserHeaderName(), envUser); + } else { + var propUser = System.getProperty(p.getUserVarName()); + if (Objects.nonNull(propUser)) { + request.setHeader(p.getUserHeaderName(), propUser); + } + } + }); + // set rhda-token // Environment variable/property name = RHDA_TOKEN String rhdaToken = calculateHeaderValue(RHDA_TOKEN_HEADER); if (rhdaToken != null && Optional.of(rhdaToken).isPresent()) { request.setHeader(RHDA_TOKEN_HEADER, rhdaToken); } - //set rhda-source ( extension/plugin id/name) + // set rhda-source ( extension/plugin id/name) // Environment variable/property name = RHDA_SOURCE String rhdaSource = calculateHeaderValue(RHDA_SOURCE_HEADER); if (rhdaSource != null && Optional.of(rhdaSource).isPresent()) { diff --git a/src/main/java/com/redhat/exhort/impl/RequestManager.java b/src/main/java/com/redhat/exhort/impl/RequestManager.java index f40da1cf..1e40bea2 100644 --- a/src/main/java/com/redhat/exhort/impl/RequestManager.java +++ b/src/main/java/com/redhat/exhort/impl/RequestManager.java @@ -21,20 +21,17 @@ public class RequestManager { - private static RequestManager requestManager; - private Map requests; + private Map requests; - public static RequestManager getInstance() - { - if(Objects.isNull(requestManager)) { + public static RequestManager getInstance() { + if (Objects.isNull(requestManager)) { requestManager = new RequestManager(); } return requestManager; } - private RequestManager() - { + private RequestManager() { requests = new HashMap<>(); } @@ -51,7 +48,6 @@ public String getTraceIdOfRequest() { } private static String concatenatedThreadId() { - return String.format("%s-%s",Thread.currentThread().getName(),Thread.currentThread().getId()); + return String.format("%s-%s", Thread.currentThread().getName(), Thread.currentThread().getId()); } - } diff --git a/src/main/java/com/redhat/exhort/impl/package-info.java b/src/main/java/com/redhat/exhort/impl/package-info.java index 34a28bae..e2d0748e 100644 --- a/src/main/java/com/redhat/exhort/impl/package-info.java +++ b/src/main/java/com/redhat/exhort/impl/package-info.java @@ -1,2 +1,2 @@ -/** Package hosting various the Exhort API implementation. **/ +/** Package hosting various the Exhort API implementation. * */ package com.redhat.exhort.impl; diff --git a/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java b/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java index c42a4a91..2d067cfb 100644 --- a/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java +++ b/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java @@ -19,12 +19,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; import com.redhat.exhort.impl.RequestManager; - import java.io.PrintWriter; import java.io.StringWriter; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.HashMap; @@ -35,22 +32,23 @@ public class ClientTraceIdSimpleFormatter extends SimpleFormatter { - private final ObjectMapper objectMapper; public ClientTraceIdSimpleFormatter() { this.objectMapper = new ObjectMapper(); this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } + @Override public String format(LogRecord record) { -// return String.format("%s, ex-client-trace-id: %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); - Map messageKeysValues = new HashMap<>(); + // return String.format("%s, ex-client-trace-id: + // %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + + // System.lineSeparator()); + Map messageKeysValues = new HashMap<>(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - ZonedDateTime zdt = ZonedDateTime.ofInstant( - record.getInstant(), ZoneId.systemDefault()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); String source; if (record.getSourceClassName() != null) { source = record.getSourceClassName(); @@ -70,48 +68,55 @@ public String format(LogRecord record) { pw.close(); throwable = sw.toString(); } -// return String.format(super.format, -// zdt, -// source, -// record.getLoggerName(), -// record.getLevel().getLocalizedLevelName(), -// message, -// throwable); - messageKeysValues.put("timestamp",zdt.toString()); + // return String.format(super.format, + // zdt, + // source, + // record.getLoggerName(), + // record.getLevel().getLocalizedLevelName(), + // message, + // throwable); + messageKeysValues.put("timestamp", zdt.toString()); messageKeysValues.put("ex-client-trace-id", RequestManager.getInstance().getTraceIdOfRequest()); - messageKeysValues.put("methodName",source); - messageKeysValues.put("loggerName",record.getLoggerName()); - messageKeysValues.put("logLevel",record.getLevel().toString()); - messageKeysValues.put("threadName",Thread.currentThread().getName()); - messageKeysValues.put("threadId",Thread.currentThread().getId()); + messageKeysValues.put("methodName", source); + messageKeysValues.put("loggerName", record.getLoggerName()); + messageKeysValues.put("logLevel", record.getLevel().toString()); + messageKeysValues.put("threadName", Thread.currentThread().getName()); + messageKeysValues.put("threadId", Thread.currentThread().getId()); String jsonPartOfMessage = getJsonPartOfMessage(message); - if(isValidJson(jsonPartOfMessage) || messageContainsOutputStructure(message)) { - messageKeysValues.put("logMessage", "log Message Contains a structure , and it will follow after the log entry"); - } - else { + if (isValidJson(jsonPartOfMessage) || messageContainsOutputStructure(message)) { + messageKeysValues.put( + "logMessage", + "log Message Contains a structure , and it will follow after the log entry"); + } else { messageKeysValues.put("logMessage", message); } try { - String jsonLogRecord = objectMapper.writeValueAsString(messageKeysValues) + System.lineSeparator(); - return jsonLogRecord + suffixRequired(messageKeysValues,message); + String jsonLogRecord = + objectMapper.writeValueAsString(messageKeysValues) + System.lineSeparator(); + return jsonLogRecord + suffixRequired(messageKeysValues, message); } catch (JsonProcessingException e) { - return String.format("%s, ex-client-trace-id: %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); + return String.format( + "%s, ex-client-trace-id: %s", + super.format(record).trim(), + RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); } } private String suffixRequired(Map messageKeysValues, String message) { - if(((String)messageKeysValues.get("logMessage")).trim().contains("log Message Contains a structure")) { + if (((String) messageKeysValues.get("logMessage")) + .trim() + .contains("log Message Contains a structure")) { return message.trim() + System.lineSeparator(); - } - else { + } else { return ""; } } - private boolean messageContainsOutputStructure(String message) - { + + private boolean messageContainsOutputStructure(String message) { String messageWithLC = message.toLowerCase(); return messageWithLC.contains("package manager") && messageWithLC.contains("output"); } + private boolean isValidJson(String jsonPartOfMessage) { if (Objects.isNull(jsonPartOfMessage)) { return false; @@ -127,13 +132,10 @@ private boolean isValidJson(String jsonPartOfMessage) { private String getJsonPartOfMessage(String message) { int startOfJson = message.indexOf("{"); int endOfJson = message.lastIndexOf("}"); - if( startOfJson > -1 && endOfJson > 0) { - return message.substring(startOfJson,endOfJson + 1); - } - else { + if (startOfJson > -1 && endOfJson > 0) { + return message.substring(startOfJson, endOfJson + 1); + } else { return null; } - - } } diff --git a/src/main/java/com/redhat/exhort/logging/LoggersFactory.java b/src/main/java/com/redhat/exhort/logging/LoggersFactory.java index 059cd2ee..e57baea3 100644 --- a/src/main/java/com/redhat/exhort/logging/LoggersFactory.java +++ b/src/main/java/com/redhat/exhort/logging/LoggersFactory.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.logging; import java.util.logging.ConsoleHandler; @@ -22,13 +21,12 @@ public class LoggersFactory { public static Logger getLogger(String loggerName) { Logger logger = Logger.getLogger(loggerName); - if(logger.getHandlers().length == 0 ) { + if (logger.getHandlers().length == 0) { ConsoleHandler handler = new ConsoleHandler(); handler.setFormatter(new ClientTraceIdSimpleFormatter()); logger.addHandler(handler); } logger.setUseParentHandlers(false); return logger; - } } diff --git a/src/main/java/com/redhat/exhort/package-info.java b/src/main/java/com/redhat/exhort/package-info.java index 02c45f8a..5d36173e 100644 --- a/src/main/java/com/redhat/exhort/package-info.java +++ b/src/main/java/com/redhat/exhort/package-info.java @@ -1,2 +1,2 @@ -/** Package hosting various Interfaces for the Exhort module. **/ +/** Package hosting various Interfaces for the Exhort module. * */ package com.redhat.exhort; diff --git a/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java b/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java index 6c9d42ab..c6c4778f 100644 --- a/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java +++ b/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java @@ -20,7 +20,6 @@ import com.redhat.exhort.Provider; import com.redhat.exhort.sbom.Sbom; import com.redhat.exhort.tools.Ecosystem; - import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -32,50 +31,47 @@ protected BaseJavaProvider(Ecosystem.Type ecosystem) { super(ecosystem); } - void parseDependencyTree(String src, int srcDepth, String [] lines, Sbom sbom) { - if(lines.length == 0) { + void parseDependencyTree(String src, int srcDepth, String[] lines, Sbom sbom) { + if (lines.length == 0) { return; } - if(lines.length == 1 && lines[0].trim().equals("")){ + if (lines.length == 1 && lines[0].trim().equals("")) { return; } int index = 0; String target = lines[index]; int targetDepth = getDepth(target); - while(targetDepth > srcDepth && index < lines.length ) - { - if(targetDepth == srcDepth + 1) { + while (targetDepth > srcDepth && index < lines.length) { + if (targetDepth == srcDepth + 1) { PackageURL from = parseDep(src); PackageURL to = parseDep(target); - if(dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { + if (dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { sbom.addDependency(from, to); } - } - else { + } else { String[] modifiedLines = Arrays.copyOfRange(lines, index, lines.length); - parseDependencyTree(lines[index-1],getDepth(lines[index-1]),modifiedLines,sbom); + parseDependencyTree(lines[index - 1], getDepth(lines[index - 1]), modifiedLines, sbom); } - if(index< lines.length - 1) { + if (index < lines.length - 1) { target = lines[++index]; targetDepth = getDepth(target); - } - else - { + } else { index++; } } } static boolean dependencyIsNotTestScope(PackageURL artifact) { - return (Objects.nonNull(artifact.getQualifiers()) && !artifact.getQualifiers().get("scope").equals("test")) || Objects.isNull(artifact.getQualifiers()); + return (Objects.nonNull(artifact.getQualifiers()) + && !artifact.getQualifiers().get("scope").equals("test")) + || Objects.isNull(artifact.getQualifiers()); } PackageURL parseDep(String dep) { - //root package + // root package DependencyAggregator dependencyAggregator = new DependencyAggregator(); // in case line in dependency tree text starts with a letter ( for root artifact). - if(dep.matches("^\\w.*")) - { + if (dep.matches("^\\w.*")) { dependencyAggregator = new DependencyAggregator(); String[] parts = dep.split(":"); dependencyAggregator.groupId = parts[0]; @@ -83,85 +79,81 @@ PackageURL parseDep(String dep) { dependencyAggregator.version = parts[3]; return dependencyAggregator.toPurl(); - } int firstDash = dep.indexOf("-"); String dependency = dep.substring(++firstDash).trim(); - if(dependency.startsWith("(")) - { + if (dependency.startsWith("(")) { dependency = dependency.substring(1); } dependency = dependency.replace(":runtime", ":compile").replace(":provided", ":compile"); - int endIndex = Math.max(dependency.indexOf(":compile"),dependency.indexOf(":test")); + int endIndex = Math.max(dependency.indexOf(":compile"), dependency.indexOf(":test")); int scopeLength; - if(dependency.indexOf(":compile") > -1) { - scopeLength = ":compile".length(); - } - else { - scopeLength = ":test".length(); + if (dependency.indexOf(":compile") > -1) { + scopeLength = ":compile".length(); + } else { + scopeLength = ":test".length(); } - dependency = dependency.substring(0,endIndex + scopeLength); + dependency = dependency.substring(0, endIndex + scopeLength); String[] parts = dependency.split(":"); // contains only GAV + packaging + scope - if(parts.length == 5) - { + if (parts.length == 5) { dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId= parts[1]; + dependencyAggregator.artifactId = parts[1]; dependencyAggregator.version = parts[3]; String conflictMessage = "omitted for conflict with"; - if (dep.contains(conflictMessage)) - { - dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()).replace(")", "").trim(); + if (dep.contains(conflictMessage)) { + dependencyAggregator.version = + dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()) + .replace(")", "") + .trim(); } } // In case there are 6 parts, there is also a classifier for artifact (version suffix) // contains GAV + packaging + classifier + scope - else if(parts.length == 6) - { + else if (parts.length == 6) { dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId= parts[1]; - dependencyAggregator.version = String.format("%s-%s",parts[4],parts[3]); + dependencyAggregator.artifactId = parts[1]; + dependencyAggregator.version = String.format("%s-%s", parts[4], parts[3]); String conflictMessage = "omitted for conflict with"; - if (dep.contains(conflictMessage)) - { - dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()).replace(")", "").trim(); + if (dep.contains(conflictMessage)) { + dependencyAggregator.version = + dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()) + .replace(")", "") + .trim(); } + } else { + throw new RuntimeException( + String.format("Cannot parse dependency into PackageUrl from line = \"%s\"", dep)); } - else{ - throw new RuntimeException(String.format("Cannot parse dependency into PackageUrl from line = \"%s\"",dep)); - } - if(parts[parts.length - 1].matches(".*[a-z]$")) { + if (parts[parts.length - 1].matches(".*[a-z]$")) { dependencyAggregator.scope = parts[parts.length - 1]; - } - else { - int endOfLine = Integer.min(parts[parts.length - 1].indexOf(""), parts[parts.length - 1].indexOf("-")); + } else { + int endOfLine = + Integer.min(parts[parts.length - 1].indexOf(""), parts[parts.length - 1].indexOf("-")); dependencyAggregator.scope = parts[parts.length - 1].substring(0, endOfLine).trim(); } return dependencyAggregator.toPurl(); } int getDepth(String line) { - if(line == null || line.trim().equals("")){ + if (line == null || line.trim().equals("")) { return -1; } - if(line.matches("^\\w.*")) - { + if (line.matches("^\\w.*")) { return 0; } - return ( (line.indexOf('-') -1 ) / 3) + 1; + return ((line.indexOf('-') - 1) / 3) + 1; } // NOTE if we want to include "scope" tags in ignore, // add property here and a case in the start-element-switch in the getIgnored method - /** - * Aggregator class for aggregating Dependency data over stream iterations, - **/ - final static class DependencyAggregator { + /** Aggregator class for aggregating Dependency data over stream iterations, */ + static final class DependencyAggregator { String scope = "*"; String groupId; String artifactId; @@ -189,7 +181,13 @@ boolean isTestDependency() { PackageURL toPurl() { try { - return new PackageURL(Ecosystem.Type.MAVEN.getType(), groupId, artifactId, version, this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), null); + return new PackageURL( + Ecosystem.Type.MAVEN.getType(), + groupId, + artifactId, + version, + this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), + null); } catch (MalformedPackageURLException e) { throw new IllegalArgumentException("Unable to parse PackageURL", e); } @@ -203,10 +201,9 @@ public boolean equals(Object o) { // NOTE we do not compare the ignored field // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't // contain comments indicating ignore - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.artifactId, that.artifactId) && - Objects.equals(this.version, that.version); - + return Objects.equals(this.groupId, that.groupId) + && Objects.equals(this.artifactId, that.artifactId) + && Objects.equals(this.version, that.version); } @Override diff --git a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java index a7379a80..80f3fc88 100644 --- a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java @@ -15,6 +15,9 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import static com.redhat.exhort.impl.ExhortApi.getBooleanValueEnvironment; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.github.packageurl.MalformedPackageURLException; @@ -29,7 +32,6 @@ import com.redhat.exhort.vcs.GitVersionControlSystemImpl; import com.redhat.exhort.vcs.TagInfo; import com.redhat.exhort.vcs.VersionControlSystem; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -40,15 +42,10 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; -import static com.redhat.exhort.impl.ExhortApi.getBooleanValueEnvironment; - /** - * Concrete implementation of the {@link Provider} used for converting - * dependency trees - * for npm projects (package.json) into a SBOM content for Stack analysis or - * Component analysis. - **/ + * Concrete implementation of the {@link Provider} used for converting dependency trees for npm + * projects (package.json) into a SBOM content for Stack analysis or Component analysis. + */ public final class GoModulesProvider extends Provider { private Logger log = LoggersFactory.getLogger(this.getClass().getName()); @@ -67,13 +64,16 @@ public String getMainModuleVersion() { public static void main(String[] args) { TreeMap qualifiers = GoModulesProvider.getQualifiers(true); -// Path path = Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod"); + // Path path = + // Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod"); Path path = Path.of("/tmp/xieshen/go.mod"); Provider provider = new GoModulesProvider(); GoModulesProvider goProvider = (GoModulesProvider) provider; -// boolean answer = goProvider.IgnoredLine(" github.com/davecgh/go-spew v1.1.1 // indirect //exhortignore"); + // boolean answer = goProvider.IgnoredLine(" github.com/davecgh/go-spew v1.1.1 // + // indirect + // //exhortignore"); try { -// provider.provideStack(path); + // provider.provideStack(path); byte[] bytes = Files.readAllBytes(path); provider.provideComponent(bytes); } catch (IOException e) { @@ -83,30 +83,31 @@ public static void main(String[] args) { public GoModulesProvider() { super(Type.GOLANG); - this.goEnvironmentVariableForPurl=getQualifiers(true); - this.goEnvironmentVariablesForRef =getQualifiers(false); - this.mainModuleVersion= getDefaultMainModuleVersion(); + this.goEnvironmentVariableForPurl = getQualifiers(true); + this.goEnvironmentVariablesForRef = getQualifiers(false); + this.mainModuleVersion = getDefaultMainModuleVersion(); } - - @Override public Content provideStack(final Path manifestPath) throws IOException { // check for custom npm executable Sbom sbom = getDependenciesSbom(manifestPath, true); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } @Override public Content provideComponent(byte[] manifestContent) throws IOException { // check for custom npm executable - return new Content(getDependenciesSbomCa(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + getDependenciesSbomCa(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); } @Override public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException("provideComponent with file system path for GoModules package manager not implemented yet"); + throw new IllegalArgumentException( + "provideComponent with file system path for GoModules package manager not implemented yet"); } private Sbom getDependenciesSbomCa(byte[] manifestContent) { @@ -127,61 +128,62 @@ private Sbom getDependenciesSbomCa(byte[] manifestContent) { return sbom; } - private PackageURL getRoot(String DependenciesGolang) { + private PackageURL getRoot(String DependenciesGolang) { return null; } private PackageURL toPurl(String dependency, String delimiter, TreeMap qualifiers) { try { int lastSlashIndex = dependency.lastIndexOf("/"); - //there is no '/' char in module/package, so there is no namespace, only name - if (lastSlashIndex == -1) - { + // there is no '/' char in module/package, so there is no namespace, only name + if (lastSlashIndex == -1) { String[] splitParts = dependency.split(delimiter); if (splitParts.length == 2) { - return new PackageURL(Type.GOLANG.getType(), null, splitParts[0], splitParts[1], qualifiers, null); + return new PackageURL( + Type.GOLANG.getType(), null, splitParts[0], splitParts[1], qualifiers, null); } else { - return new PackageURL(Type.GOLANG.getType(), null, splitParts[0], this.mainModuleVersion, qualifiers, null); + return new PackageURL( + Type.GOLANG.getType(), null, splitParts[0], this.mainModuleVersion, qualifiers, null); } } String namespace = dependency.substring(0, lastSlashIndex); String dependencyAndVersion = dependency.substring(lastSlashIndex + 1); String[] parts = dependencyAndVersion.split(delimiter); - if (parts.length == 2) { - return new PackageURL(Type.GOLANG.getType(), namespace, parts[0], parts[1], qualifiers, null); - // in this case, there is no version (happens with main module), thus need to take it from precalculated main module version. - } else { - return new PackageURL(Type.GOLANG.getType(), namespace, parts[0], this.mainModuleVersion, qualifiers, null); - } + if (parts.length == 2) { + return new PackageURL( + Type.GOLANG.getType(), namespace, parts[0], parts[1], qualifiers, null); + // in this case, there is no version (happens with main module), thus need to take it from + // precalculated + // main module version. + } else { + return new PackageURL( + Type.GOLANG.getType(), namespace, parts[0], this.mainModuleVersion, qualifiers, null); + } } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse golang module package : " + dependency , e); + throw new IllegalArgumentException( + "Unable to parse golang module package : " + dependency, e); } - } - - - Sbom getDependenciesSbom(Path manifestPath, boolean buildTree) throws IOException { + Sbom getDependenciesSbom(Path manifestPath, boolean buildTree) throws IOException { var goModulesResult = buildGoModulesDependencies(manifestPath); calculateMainModuleVersion(manifestPath.getParent()); Sbom sbom; List ignoredDeps = getIgnoredDeps(manifestPath); boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "false"); - if(matchManifestVersions) { -// String rootName = getParentVertex() + if (matchManifestVersions) { + // String rootName = getParentVertex() String[] goModGraphLines = goModulesResult.split(System.lineSeparator()); - performManifestVersionsCheck(goModGraphLines,manifestPath); + performManifestVersionsCheck(goModGraphLines, manifestPath); } if (!buildTree) { - sbom = buildSbomFromList(goModulesResult,ignoredDeps); - } - else - { - sbom = buildSbomFromGraph(goModulesResult,ignoredDeps,manifestPath); + sbom = buildSbomFromList(goModulesResult, ignoredDeps); + } else { + sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath); } -// List ignoredDeps = getIgnoredDeps(manifestPath); -// sbom.filterIgnoredDeps(ignoredDeps); + // List ignoredDeps = getIgnoredDeps(manifestPath); + // sbom.filterIgnoredDeps(ignoredDeps); return sbom; } @@ -190,50 +192,70 @@ private void performManifestVersionsCheck(String[] goModGraphLines, Path manifes String goModLines = Files.readString(manifestPath); String[] lines = goModLines.split(System.lineSeparator()); String root = getParentVertex(goModGraphLines[0]); - List comparisonLines = Arrays.stream(goModGraphLines).filter((line) -> line.startsWith(root)).map((line) -> getChildVertex(line)).collect(Collectors.toList()); - List goModDependencies = collectAllDepsFromManifest(lines,goModLines); - comparisonLines.stream().forEach((dependency) -> - { - String[] parts = dependency.split("@"); - String version = parts[1]; - String depName = parts[0]; - goModDependencies.stream().forEach((dep) -> - { - String[] artifactParts = dep.trim().split(" "); - String currentDepName = artifactParts[0]; - String currentVersion = artifactParts[1]; - if(currentDepName.trim().equals(depName.trim())) - { - if(!currentVersion.trim().equals(version.trim())) - { - throw new RuntimeException(String.format("Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", depName, currentVersion, version)); - } - } - }); - }); + List comparisonLines = + Arrays.stream(goModGraphLines) + .filter((line) -> line.startsWith(root)) + .map((line) -> getChildVertex(line)) + .collect(Collectors.toList()); + List goModDependencies = collectAllDepsFromManifest(lines, goModLines); + comparisonLines.stream() + .forEach( + (dependency) -> { + String[] parts = dependency.split("@"); + String version = parts[1]; + String depName = parts[0]; + goModDependencies.stream() + .forEach( + (dep) -> { + String[] artifactParts = dep.trim().split(" "); + String currentDepName = artifactParts[0]; + String currentVersion = artifactParts[1]; + if (currentDepName.trim().equals(depName.trim())) { + if (!currentVersion.trim().equals(version.trim())) { + throw new RuntimeException( + String.format( + "Can't continue with analysis - versions mismatch for" + + " dependency name=%s, manifest version=%s, installed" + + " Version=%s, if you want to allow version mismatch for" + + " analysis between installed and requested packages," + + " set environment variable/setting -" + + " MATCH_MANIFEST_VERSIONS=false", + depName, currentVersion, version)); + } + } + }); + }); } catch (IOException e) { - throw new RuntimeException("Failed to open go.mod file for manifest versions check validation!"); + throw new RuntimeException( + "Failed to open go.mod file for manifest versions check validation!"); } } private List collectAllDepsFromManifest(String[] lines, String goModLines) { List result = new ArrayList<>(); // collect all deps that starts with require keyword - result = Arrays.stream(lines).filter((line) -> line.trim().startsWith("require") && !line.contains("(")).map((dep) -> dep.substring("require".length()).trim()).collect(Collectors.toList()); + result = + Arrays.stream(lines) + .filter((line) -> line.trim().startsWith("require") && !line.contains("(")) + .map((dep) -> dep.substring("require".length()).trim()) + .collect(Collectors.toList()); // collect all deps that are inside `require` blocks String currentSegmentOfGoMod = goModLines; - Map requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); - while(requirePosObject.get("index") > -1) - { - String depsInsideRequirementsBlock = currentSegmentOfGoMod.substring(requirePosObject.get("index") + requirePosObject.get("length")).trim(); + Map requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); + while (requirePosObject.get("index") > -1) { + String depsInsideRequirementsBlock = + currentSegmentOfGoMod + .substring(requirePosObject.get("index") + requirePosObject.get("length")) + .trim(); int endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")"); int currentIndex = 0; - while(currentIndex < endOfBlockIndex) - { - int endOfLinePosition = depsInsideRequirementsBlock.indexOf(System.lineSeparator(),currentIndex); - String dependency = depsInsideRequirementsBlock.substring(currentIndex,endOfLinePosition).trim(); + while (currentIndex < endOfBlockIndex) { + int endOfLinePosition = + depsInsideRequirementsBlock.indexOf(System.lineSeparator(), currentIndex); + String dependency = + depsInsideRequirementsBlock.substring(currentIndex, endOfLinePosition).trim(); result.add(dependency); currentIndex = endOfLinePosition + 1; } @@ -246,187 +268,211 @@ private List collectAllDepsFromManifest(String[] lines, String goModLine private Map decideRequireBlockIndex(String currentSegmentOfGoMod) { int index = currentSegmentOfGoMod.indexOf("require("); - int length = "require(".length(); - if(index == -1) - { + int length = "require(".length(); + if (index == -1) { index = currentSegmentOfGoMod.indexOf("require ("); length = "require (".length(); - if(index == -1 ) - { + if (index == -1) { index = currentSegmentOfGoMod.indexOf("require ("); length = "require (".length(); } } - return Map.of("index",index,"length",length); - + return Map.of("index", index, "length", length); } - public void determineMainModuleVersion(Path directory) - { + public void determineMainModuleVersion(Path directory) { this.calculateMainModuleVersion(directory); } + private void calculateMainModuleVersion(Path directory) { VersionControlSystem vcs = new GitVersionControlSystemImpl(); - if(vcs.isDirectoryRepo(directory)) { - TagInfo latestTagInfo = vcs.getLatestTag(directory); - if (!latestTagInfo.getTagName().trim().equals("")) { - if(!latestTagInfo.isCurrentCommitPointedByTag()) - { - String nextTagVersion = vcs.getNextTagVersion(latestTagInfo); - this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, nextTagVersion); - } - else - { - this.mainModuleVersion = latestTagInfo.getTagName(); - } - } - else - { - if(!latestTagInfo.getCurrentCommitDigest().trim().equals("")) { - this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, getDefaultMainModuleVersion()); - } - } + if (vcs.isDirectoryRepo(directory)) { + TagInfo latestTagInfo = vcs.getLatestTag(directory); + if (!latestTagInfo.getTagName().trim().equals("")) { + if (!latestTagInfo.isCurrentCommitPointedByTag()) { + String nextTagVersion = vcs.getNextTagVersion(latestTagInfo); + this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, nextTagVersion); + } else { + this.mainModuleVersion = latestTagInfo.getTagName(); + } + } else { + if (!latestTagInfo.getCurrentCommitDigest().trim().equals("")) { + this.mainModuleVersion = + vcs.getPseudoVersion(latestTagInfo, getDefaultMainModuleVersion()); + } + } } } - private Sbom buildSbomFromGraph(String goModulesResult, List ignoredDeps, Path manifestPath) throws IOException{ -// Each entry contains a key of the module, and the list represents the module direct dependencies , so pairing of the key with each of the dependencies in a list is basically an edge in the graph. - Map edges = new HashMap<>(); - // iterate over go mod graph line by line and create map , with each entry to contain module as a key , and value of list of that module' dependencies. + private Sbom buildSbomFromGraph( + String goModulesResult, List ignoredDeps, Path manifestPath) throws IOException { + // Each entry contains a key of the module, and the list represents the module direct + // dependencies , so + // pairing of the key with each of the dependencies in a list is basically an edge in the graph. + Map edges = new HashMap<>(); + // iterate over go mod graph line by line and create map , with each entry to contain module as + // a key , and + // value of list of that module' dependencies. String[] lines = goModulesResult.split(System.lineSeparator()); List linesList = Arrays.asList(lines); -// System.out.print("Start time: " + LocalDateTime.now() + System.lineSeparator()); + // System.out.print("Start time: " + LocalDateTime.now() + System.lineSeparator()); - Integer startingIndex=0; - Integer EndingIndex=lines.length - 1; - String[] targetLines = Arrays.copyOfRange(lines,0,lines.length-1); + Integer startingIndex = 0; + Integer EndingIndex = lines.length - 1; + String[] targetLines = Arrays.copyOfRange(lines, 0, lines.length - 1); for (String line : linesList) { - if (!edges.containsKey(getParentVertex(line))) - { - //Collect all direct dependencies of the current module into a list. + if (!edges.containsKey(getParentVertex(line))) { + // Collect all direct dependencies of the current module into a list. List deps = collectAllDirectDependencies(targetLines, line); - edges.put(getParentVertex(line),deps); - startingIndex+=deps.size(); - // Because all the deps of the current module were collected, not need to search for next modules on these lines, so truncate these lines from search array to make the search more rapid and efficient. - if(startingIndex < EndingIndex) { + edges.put(getParentVertex(line), deps); + startingIndex += deps.size(); + // Because all the deps of the current module were collected, not need to search for next + // modules on + // these lines, so truncate these lines from search array to make the search more rapid and + // efficient. + if (startingIndex < EndingIndex) { targetLines = Arrays.copyOfRange(lines, startingIndex, EndingIndex); } - } } - //DEBUG -// System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); + // DEBUG + // System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); boolean goMvsLogicEnabled = getBooleanValueEnvironment("EXHORT_GO_MVS_LOGIC_ENABLED", "false"); - if(goMvsLogicEnabled) { - edges = getFinalPackagesVersionsForModule(edges,manifestPath); + if (goMvsLogicEnabled) { + edges = getFinalPackagesVersionsForModule(edges, manifestPath); } -// Build Sbom + // Build Sbom String rootPackage = getParentVertex(lines[0]); PackageURL root = toPurl(rootPackage, "@", this.goEnvironmentVariableForPurl); - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); sbom.addRoot(root); - edges.forEach((key,value)-> { - PackageURL source = toPurl(key,"@",this.goEnvironmentVariableForPurl); - value.forEach(dep -> { - PackageURL targetPurl = toPurl((String) dep, "@", this.goEnvironmentVariableForPurl); - sbom.addDependency(source, targetPurl); - }); - - }); - List ignoredDepsPurl = ignoredDeps.stream().map(PackageURL::getCoordinates).collect(Collectors.toList()); + edges.forEach( + (key, value) -> { + PackageURL source = toPurl(key, "@", this.goEnvironmentVariableForPurl); + value.forEach( + dep -> { + PackageURL targetPurl = + toPurl((String) dep, "@", this.goEnvironmentVariableForPurl); + sbom.addDependency(source, targetPurl); + }); + }); + List ignoredDepsPurl = + ignoredDeps.stream().map(PackageURL::getCoordinates).collect(Collectors.toList()); sbom.filterIgnoredDeps(ignoredDepsPurl); ArrayList ignoredDepsByName = new ArrayList<>(); - ignoredDeps.forEach(purl -> - { - if(sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(),purl.getName())) - { - ignoredDepsByName.add(purl.getName()); - } - }); + ignoredDeps.forEach( + purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } + }); sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); sbom.filterIgnoredDeps(ignoredDepsByName); - return sbom; - + return sbom; } - private Map getFinalPackagesVersionsForModule(Map edges, Path manifestPath) { - Operations.runProcessGetOutput(manifestPath.getParent(),"go","mod","download"); - String finalVersionsForAllModules = Operations.runProcessGetOutput(manifestPath.getParent(), "go", "list", "-m", "all"); - Map finalModulesVersions = Arrays.stream(finalVersionsForAllModules.split(System.lineSeparator())).filter(string -> string.trim().split(" ").length == 2).collect(Collectors.toMap(t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second)); + private Map getFinalPackagesVersionsForModule( + Map edges, Path manifestPath) { + Operations.runProcessGetOutput(manifestPath.getParent(), "go", "mod", "download"); + String finalVersionsForAllModules = + Operations.runProcessGetOutput(manifestPath.getParent(), "go", "list", "-m", "all"); + Map finalModulesVersions = + Arrays.stream(finalVersionsForAllModules.split(System.lineSeparator())) + .filter(string -> string.trim().split(" ").length == 2) + .collect( + Collectors.toMap( + t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second)); Map listWithModifiedVersions = new HashMap<>(); - edges.entrySet().stream().filter(string -> string.getKey().trim().split("@").length == 2).collect(Collectors.toList()).forEach((entry) -> { - String packageWithSelectedVersion = getPackageWithFinalVersion(finalModulesVersions, entry.getKey()); - List packagesWithFinalVersions = getListOfPackagesWithFinlVersions(finalModulesVersions,entry); - listWithModifiedVersions.put(packageWithSelectedVersion,packagesWithFinalVersions); - }); - + edges.entrySet().stream() + .filter(string -> string.getKey().trim().split("@").length == 2) + .collect(Collectors.toList()) + .forEach( + (entry) -> { + String packageWithSelectedVersion = + getPackageWithFinalVersion(finalModulesVersions, entry.getKey()); + List packagesWithFinalVersions = + getListOfPackagesWithFinlVersions(finalModulesVersions, entry); + listWithModifiedVersions.put(packageWithSelectedVersion, packagesWithFinalVersions); + }); return listWithModifiedVersions; } - private List getListOfPackagesWithFinlVersions(Map finalModulesVersions, Map.Entry entry) { - return (List)entry.getValue().stream().map((packageWithVersion) -> getPackageWithFinalVersion(finalModulesVersions,(String)packageWithVersion)).collect(Collectors.toList()); + private List getListOfPackagesWithFinlVersions( + Map finalModulesVersions, Map.Entry entry) { + return (List) + entry.getValue().stream() + .map( + (packageWithVersion) -> + getPackageWithFinalVersion(finalModulesVersions, (String) packageWithVersion)) + .collect(Collectors.toList()); } - public static String getPackageWithFinalVersion(Map finalModulesVersions, String packagePlusVersion) { + public static String getPackageWithFinalVersion( + Map finalModulesVersions, String packagePlusVersion) { String packageName = packagePlusVersion.split("@")[0]; String originalVersion = packagePlusVersion.split("@")[1]; String finalVersion = finalModulesVersions.get(packageName); - if(Objects.nonNull(finalVersion)) { - return String.format("%s@%s",packageName,finalVersion); - } - else { + if (Objects.nonNull(finalVersion)) { + return String.format("%s@%s", packageName, finalVersion); + } else { return packagePlusVersion; } } private boolean dependencyNotToBeIgnored(List ignoredDeps, PackageURL checkedPurl) { - return ignoredDeps.stream().noneMatch(dependencyPurl -> dependencyPurl.getCoordinates().equals(checkedPurl.getCoordinates())); + return ignoredDeps.stream() + .noneMatch( + dependencyPurl -> dependencyPurl.getCoordinates().equals(checkedPurl.getCoordinates())); } private static List collectAllDirectDependencies(String[] targetLines, String edge) { return Arrays.stream(targetLines) - .filter(line2 -> getParentVertex(line2).equals(getParentVertex(edge))) - .map(GoModulesProvider::getChildVertex) - .collect(Collectors.toList()); + .filter(line2 -> getParentVertex(line2).equals(getParentVertex(edge))) + .map(GoModulesProvider::getChildVertex) + .collect(Collectors.toList()); } private static TreeMap getQualifiers(boolean includeOsAndArch) { - if(includeOsAndArch) - { + if (includeOsAndArch) { var go = Operations.getCustomPathOrElse("go"); - String goEnvironmentVariables = Operations.runProcessGetOutput(null, new String[]{go, "env"}); + String goEnvironmentVariables = + Operations.runProcessGetOutput(null, new String[] {go, "env"}); String hostArch = getEnvironmentVariable(goEnvironmentVariables, goHostArchitectureEnvName); String hostOS = getEnvironmentVariable(goEnvironmentVariables, goHostOperationSystemEnvName); - return new TreeMap(Map.of("type", "module","goos",hostOS,"goarch",hostArch)); + return new TreeMap(Map.of("type", "module", "goos", hostOS, "goarch", hostArch)); } return new TreeMap(Map.of("type", "module")); } - private static String getEnvironmentVariable(String goEnvironmentVariables,String envName) { - int i = goEnvironmentVariables.indexOf(String.format("%s=",envName)); + private static String getEnvironmentVariable(String goEnvironmentVariables, String envName) { + int i = goEnvironmentVariables.indexOf(String.format("%s=", envName)); int beginIndex = i + String.format("%s=", envName).length(); - int endOfLineIndex = goEnvironmentVariables.substring(beginIndex).indexOf(System.lineSeparator()); + int endOfLineIndex = + goEnvironmentVariables.substring(beginIndex).indexOf(System.lineSeparator()); String envValue = goEnvironmentVariables.substring(beginIndex).substring(0, endOfLineIndex); - return envValue.replaceAll("\"",""); - + return envValue.replaceAll("\"", ""); } private String buildGoModulesDependencies(Path manifestPath) throws JsonMappingException, JsonProcessingException { var go = Operations.getCustomPathOrElse("go"); String[] goModulesDeps; - goModulesDeps = new String[]{go, "mod", "graph"}; + goModulesDeps = new String[] {go, "mod", "graph"}; // execute the clean command - String goModulesOutput = Operations.runProcessGetOutput(manifestPath.getParent(),goModulesDeps); - if(debugLoggingIsNeeded()) { - log.info(String.format("Package Manager Go Mod Graph output : %s%s",System.lineSeparator(),goModulesOutput)); + String goModulesOutput = + Operations.runProcessGetOutput(manifestPath.getParent(), goModulesDeps); + if (debugLoggingIsNeeded()) { + log.info( + String.format( + "Package Manager Go Mod Graph output : %s%s", + System.lineSeparator(), goModulesOutput)); } return goModulesOutput; } @@ -434,26 +480,26 @@ private String buildGoModulesDependencies(Path manifestPath) private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) { String[] allModulesFlat = golangDeps.split(System.lineSeparator()); String parentVertex = getParentVertex(allModulesFlat[0]); - PackageURL root = toPurl(parentVertex,"@",this.goEnvironmentVariableForPurl); + PackageURL root = toPurl(parentVertex, "@", this.goEnvironmentVariableForPurl); // Get only direct dependencies of root package/module, and that's it. List deps = collectAllDirectDependencies(allModulesFlat, parentVertex); - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); sbom.addRoot(root); - deps.forEach(dep -> { - PackageURL targetPurl = toPurl(dep, "@", this.goEnvironmentVariableForPurl); - if(dependencyNotToBeIgnored(ignoredDeps,targetPurl)) { - sbom.addDependency(root, targetPurl); - } - }); + deps.forEach( + dep -> { + PackageURL targetPurl = toPurl(dep, "@", this.goEnvironmentVariableForPurl); + if (dependencyNotToBeIgnored(ignoredDeps, targetPurl)) { + sbom.addDependency(root, targetPurl); + } + }); List ignoredDepsByName = new ArrayList<>(); - ignoredDeps.forEach(purl -> - { - if(sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(),purl.getName())) - { - ignoredDepsByName.add(purl.getName()); - } - }); + ignoredDeps.forEach( + purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } + }); sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); sbom.filterIgnoredDeps(ignoredDepsByName); return sbom; @@ -462,53 +508,61 @@ private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) private List getIgnoredDeps(Path manifestPath) throws IOException { List goModlines = Files.readAllLines(manifestPath); - List ignored = goModlines.stream() - .filter(this::IgnoredLine) - .map(this::extractPackageName) - .map(dep -> toPurl(dep,"\\s{1,3}",this.goEnvironmentVariableForPurl)) - .collect(Collectors.toList()); + List ignored = + goModlines.stream() + .filter(this::IgnoredLine) + .map(this::extractPackageName) + .map(dep -> toPurl(dep, "\\s{1,3}", this.goEnvironmentVariableForPurl)) + .collect(Collectors.toList()); return ignored; } private String extractPackageName(String line) { String trimmedRow = line.trim(); int firstRemarkNotationOccurrence = trimmedRow.indexOf("//"); - return trimmedRow.substring(0,firstRemarkNotationOccurrence).trim(); - + return trimmedRow.substring(0, firstRemarkNotationOccurrence).trim(); } public boolean IgnoredLine(String line) { - boolean result = false; - if (line.contains("exhortignore")) - { - // if exhortignore is alone in a comment or is in a comment together with indirect or as a comment inside a comment ( e.g // indirect //exhort) - // then this line is to be checked if it's a comment after a package name. - if(Pattern.matches(".+//\\s*exhortignore",line) || Pattern.matches(".+//\\sindirect (//)?\\s*exhortignore",line) ) - { - String trimmedRow = line.trim(); - // filter out lines where exhortignore has no meaning - if(!trimmedRow.startsWith("module ") && !trimmedRow.startsWith("go ") && !trimmedRow.startsWith("require (") && !trimmedRow.startsWith("require(") - && !trimmedRow.startsWith("exclude ") && !trimmedRow.startsWith("replace ") && !trimmedRow.startsWith("retract ") && !trimmedRow.startsWith("use ") - && !trimmedRow.contains("=>")) - { //only for lines that after trimming starts with "require " or starting with package name followd by one space, and then a semver version. - if( trimmedRow.startsWith("require ") || Pattern.matches("^[a-z.0-9/-]+\\s{1,2}[vV][0-9]\\.[0-9](\\.[0-9]){0,2}.*",trimmedRow)) - { - result = true; - } + boolean result = false; + if (line.contains("exhortignore")) { + // if exhortignore is alone in a comment or is in a comment together with indirect or as a + // comment inside a + // comment ( e.g // indirect //exhort) + // then this line is to be checked if it's a comment after a package name. + if (Pattern.matches(".+//\\s*exhortignore", line) + || Pattern.matches(".+//\\sindirect (//)?\\s*exhortignore", line)) { + String trimmedRow = line.trim(); + // filter out lines where exhortignore has no meaning + if (!trimmedRow.startsWith("module ") + && !trimmedRow.startsWith("go ") + && !trimmedRow.startsWith("require (") + && !trimmedRow.startsWith("require(") + && !trimmedRow.startsWith("exclude ") + && !trimmedRow.startsWith("replace ") + && !trimmedRow.startsWith("retract ") + && !trimmedRow.startsWith("use ") + && !trimmedRow.contains( + "=>")) { // only for lines that after trimming starts with "require " or starting + // with + // package name followd by one space, and then a semver version. + if (trimmedRow.startsWith("require ") + || Pattern.matches( + "^[a-z.0-9/-]+\\s{1,2}[vV][0-9]\\.[0-9](\\.[0-9]){0,2}.*", trimmedRow)) { + result = true; } } } - return result; - + } + return result; } - private static String getParentVertex(String edge) - { + private static String getParentVertex(String edge) { String[] edgeParts = edge.trim().split(" "); return edgeParts[0]; } - private static String getChildVertex(String edge) - { + + private static String getChildVertex(String edge) { String[] edgeParts = edge.trim().split(" "); return edgeParts[1]; @@ -517,5 +571,4 @@ private static String getChildVertex(String edge) private static String getDefaultMainModuleVersion() { return defaultMainVersion; } - } diff --git a/src/main/java/com/redhat/exhort/providers/GradleProvider.java b/src/main/java/com/redhat/exhort/providers/GradleProvider.java index 86b007f8..27b6387e 100644 --- a/src/main/java/com/redhat/exhort/providers/GradleProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GradleProvider.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; + import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.Api; @@ -24,10 +26,6 @@ import com.redhat.exhort.sbom.SbomFactory; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; -import org.tomlj.Toml; -import org.tomlj.TomlParseResult; -import org.tomlj.TomlTable; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -39,14 +37,15 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; +import org.tomlj.TomlTable; /** - * Concrete implementation of the {@link Provider} used for converting dependency trees - * for Gradle projects (gradle.build) into a content Dot Graphs for Stack analysis or Json for - * Component analysis. - **/ + * Concrete implementation of the {@link Provider} used for converting dependency trees for Gradle + * projects (gradle.build) into a content Dot Graphs for Stack analysis or Json for Component + * analysis. + */ public final class GradleProvider extends BaseJavaProvider { private Logger log = LoggersFactory.getLogger(this.getClass().getName()); @@ -60,24 +59,29 @@ public Content provideStack(final Path manifestPath) throws IOException { Path tempFile = getDependencies(manifestPath); if (debugLoggingIsNeeded()) { String stackAnalysisDependencyTree = Files.readString(tempFile); - log.info(String.format("Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s", System.lineSeparator(), stackAnalysisDependencyTree)); + log.info( + String.format( + "Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s", + System.lineSeparator(), stackAnalysisDependencyTree)); } Map propertiesMap = extractProperties(manifestPath); var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, "runtimeClasspath"); var ignored = getIgnoredDeps(manifestPath); - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); } private List getIgnoredDeps(Path manifestPath) throws IOException { List buildGradleLines = Files.readAllLines(manifestPath); List ignored = new ArrayList<>(); - var ignoredLines = buildGradleLines.stream() - .filter(this::isIgnoredLine) - .map(this::extractPackageName) - .collect(Collectors.toList()); + var ignoredLines = + buildGradleLines.stream() + .filter(this::isIgnoredLine) + .map(this::extractPackageName) + .collect(Collectors.toList()); // Process each ignored dependency for (String dependency : ignoredLines) { @@ -98,7 +102,9 @@ private List getIgnoredDeps(Path manifestPath) throws IOException { private String getDepInfo(String dependencyLine) { // Check if the line contains "group:", "name:", and "version:" - if (dependencyLine.contains("group:") && dependencyLine.contains("name:") && dependencyLine.contains("version:")) { + if (dependencyLine.contains("group:") + && dependencyLine.contains("name:") + && dependencyLine.contains("version:")) { Pattern pattern = Pattern.compile("(group|name|version):\\s*['\"](.*?)['\"]"); Matcher matcher = pattern.matcher(dependencyLine); String groupId = null, artifactId = null, version = null; @@ -119,10 +125,10 @@ private String getDepInfo(String dependencyLine) { break; } } - if (groupId != null && artifactId != null && version != null) { - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); - } + if (groupId != null && artifactId != null && version != null) { + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); + } } else { // Regular expression pattern to capture content inside single or double quotes Pattern pattern = Pattern.compile("['\"](.*?)['\"]"); @@ -157,13 +163,13 @@ private String getDepFromNotation(String dependency, Path manifestPath) throws I if (dependencyTable != null) { String groupId = dependencyTable.getString("module").split(":")[0]; String artifactId = dependencyTable.getString("module").split(":")[1]; - String version = toml.getTable("versions").getString(dependencyTable.getString("version.ref")); + String version = + toml.getTable("versions").getString(dependencyTable.getString("version.ref")); PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); return ignoredPackageUrl.getCoordinates(); } return null; - } private Path getLibsVersionsTomlPath(Path manifestPath) { @@ -210,7 +216,6 @@ private String extractPackageName(String line) { return packageName; } - private static Path getDependencies(Path manifestPath) throws IOException { // check for custom gradle executable var gradle = Operations.getCustomPathOrElse("gradle"); @@ -220,7 +225,8 @@ private static Path getDependencies(Path manifestPath) throws IOException { String gradleCommand = gradle + " dependencies"; String[] cmdList = gradleCommand.split("\\s+"); - String gradleOutput = Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); + String gradleOutput = + Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); Files.writeString(tempFile, gradleOutput); return tempFile; @@ -231,14 +237,17 @@ private Path getProperties(Path manifestPath) throws IOException { var gradle = Operations.getCustomPathOrElse("gradle"); String propCmd = gradle + " properties"; String[] propCmdList = propCmd.split("\\s+"); - String properties = Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList); + String properties = + Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList); // Create a temporary file Files.writeString(propsTempFile, properties); return propsTempFile; } - private Sbom buildSbomFromTextFormat(Path textFormatFile, Map propertiesMap, String configName) throws IOException { + private Sbom buildSbomFromTextFormat( + Path textFormatFile, Map propertiesMap, String configName) + throws IOException { var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); String root = getRoot(textFormatFile, propertiesMap); @@ -260,11 +269,12 @@ private Sbom buildSbomFromTextFormat(Path textFormatFile, Map pr return sbom; } - private String getRoot(Path textFormatFile, Map propertiesMap) throws IOException { + private String getRoot(Path textFormatFile, Map propertiesMap) + throws IOException { String group = propertiesMap.get("group"); String version = propertiesMap.get("version"); String rootName = extractRootProjectValue(textFormatFile); - String root = group + ':' + rootName + ':' + "jar" + ':' + version ; + String root = group + ':' + rootName + ':' + "jar" + ':' + version; return root; } @@ -330,7 +340,10 @@ private List extractLines(Path inputFilePath, String startMarker) throws @Override public Content provideComponent(byte[] manifestContent) throws IOException { - throw new IllegalArgumentException("Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); + throw new IllegalArgumentException( + "Gradle Package Manager requires the full package directory, not just the manifest content," + + " to generate the dependency tree. Please provide the complete package directory" + + " path."); } @Override @@ -355,6 +368,7 @@ public Content provideComponent(Path manifestPath) throws IOException { var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, configName); var ignored = getIgnoredDeps(manifestPath); - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); } } diff --git a/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java b/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java index 3ec2d75a..26290196 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java @@ -15,19 +15,7 @@ */ package com.redhat.exhort.providers; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamConstants; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; @@ -40,80 +28,100 @@ import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; /** - * Concrete implementation of the {@link Provider} used for converting dependency trees - * for Java Maven projects (pom.xml) into a content Dot Graphs for Stack analysis or Json for - * Component analysis. - **/ + * Concrete implementation of the {@link Provider} used for converting dependency trees for Java + * Maven projects (pom.xml) into a content Dot Graphs for Stack analysis or Json for Component + * analysis. + */ public final class JavaMavenProvider extends BaseJavaProvider { private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + public static void main(String[] args) throws IOException { JavaMavenProvider javaMavenProvider = new JavaMavenProvider(); - PackageURL packageURL = javaMavenProvider.parseDep("+- org.assertj:assertj-core:jar:3.24.2:test"); + PackageURL packageURL = + javaMavenProvider.parseDep("+- org.assertj:assertj-core:jar:3.24.2:test"); LocalDateTime start = LocalDateTime.now(); System.out.print(start); - Content content = javaMavenProvider.provideStack(Path.of("/tmp/devfile-sample-java-springboot-basic/pom.xml")); + Content content = + javaMavenProvider.provideStack( + Path.of("/tmp/devfile-sample-java-springboot-basic/pom.xml")); -// PackageURL packageURL = javaMavenProvider.parseDep("pom-with-deps-no-ignore:pom-with-dependency-not-ignored-common-paths:jar:0.0.1"); -// String report = new String(content.buffer); + // PackageURL packageURL = + // javaMavenProvider.parseDep("pom-with-deps-no-ignore:pom-with-dependency-not-ignored-common-paths:jar:0.0.1"); + // String report = new String(content.buffer); System.out.println(new String(content.buffer)); LocalDateTime end = LocalDateTime.now(); System.out.print(end); System.out.print("Total time elapsed = " + start.until(end, ChronoUnit.NANOS)); - } + public JavaMavenProvider() { super(Type.MAVEN); } - - @Override public Content provideStack(final Path manifestPath) throws IOException { // check for custom mvn executable var mvn = Operations.getCustomPathOrElse("mvn"); // clean command used to clean build target - var mvnCleanCmd = new String[]{mvn, "clean", "-f", manifestPath.toString()}; + var mvnCleanCmd = new String[] {mvn, "clean", "-f", manifestPath.toString()}; var mvnEnvs = getMvnExecEnvs(); // execute the clean command Operations.runProcess(mvnCleanCmd, mvnEnvs); // create a temp file for storing the dependency tree in var tmpFile = Files.createTempFile("exhort_dot_graph_", null); // the tree command will build the project and create the dependency tree in the temp file - var mvnTreeCmd = new ArrayList() {{ - add(mvn); - add("org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree"); - add("-Dverbose"); - add("-DoutputType=text"); - add(String.format("-DoutputFile=%s", tmpFile.toString())); - add("-f"); - add(manifestPath.toString()); - }}; + var mvnTreeCmd = + new ArrayList() { + { + add(mvn); + add("org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree"); + add("-Dverbose"); + add("-DoutputType=text"); + add(String.format("-DoutputFile=%s", tmpFile.toString())); + add("-f"); + add(manifestPath.toString()); + } + }; // if we have dependencies marked as ignored, exclude them from the tree command - var ignored = getDependencies(manifestPath).stream() - .filter(d -> d.ignored) - .map(DependencyAggregator::toPurl) - .map(PackageURL::getCoordinates) - .collect(Collectors.toList()); + var ignored = + getDependencies(manifestPath).stream() + .filter(d -> d.ignored) + .map(DependencyAggregator::toPurl) + .map(PackageURL::getCoordinates) + .collect(Collectors.toList()); // execute the tree command Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs); - if(debugLoggingIsNeeded()) - { + if (debugLoggingIsNeeded()) { String stackAnalysisDependencyTree = Files.readString(tmpFile); - log.info(String.format("Package Manager Maven Stack Analysis Dependency Tree Output: %s %s",System.lineSeparator(),stackAnalysisDependencyTree)); + log.info( + String.format( + "Package Manager Maven Stack Analysis Dependency Tree Output: %s %s", + System.lineSeparator(), stackAnalysisDependencyTree)); } var sbom = buildSbomFromTextFormat(tmpFile); // build and return content for constructing request to the backend - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); } private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { - var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); + var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); List lines = Files.readAllLines(textFormatFile); var root = lines.get(0); var rootPurl = parseDep(root); @@ -121,18 +129,17 @@ private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { lines.remove(0); String[] array = new String[lines.size()]; lines.toArray(array); -// createSbomIteratively(lines,sbom); - parseDependencyTree(root, 0 , array, sbom); + // createSbomIteratively(lines,sbom); + parseDependencyTree(root, 0, array, sbom); return sbom; } private PackageURL txtPkgToPurl(String dotPkg) { - var parts = dotPkg. - replaceAll("\"", "") - .trim().split(":"); - if(parts.length >= 4) { + var parts = dotPkg.replaceAll("\"", "").trim().split(":"); + if (parts.length >= 4) { try { - return new PackageURL(Ecosystem.Type.MAVEN.getType(), parts[0], parts[1], parts[3], null, null); + return new PackageURL( + Ecosystem.Type.MAVEN.getType(), parts[0], parts[1], parts[3], null, null); } catch (MalformedPackageURLException e) { throw new IllegalArgumentException("Unable to parse dot package: " + dotPkg, e); } @@ -155,32 +162,49 @@ private Content generateSbomFromEffectivePom(Path originPom) throws IOException // check for custom mvn executable var mvn = Operations.getCustomPathOrElse("mvn"); var tmpEffPom = Files.createTempFile("exhort_eff_pom_", ".xml"); - var mvnEffPomCmd = new String[]{ - mvn, - "clean", - "help:effective-pom", - String.format("-Doutput=%s", tmpEffPom.toString()), - "-f", originPom.toString() - }; + var mvnEffPomCmd = + new String[] { + mvn, + "clean", + "help:effective-pom", + String.format("-Doutput=%s", tmpEffPom.toString()), + "-f", + originPom.toString() + }; // execute the effective pom command Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs()); - if(debugLoggingIsNeeded()) - { + if (debugLoggingIsNeeded()) { String CaEffectivePoM = Files.readString(tmpEffPom); - log.info(String.format("Package Manager Maven Component Analysis Effective POM Output : %s %s",System.lineSeparator(),CaEffectivePoM)); + log.info( + String.format( + "Package Manager Maven Component Analysis Effective POM Output : %s %s", + System.lineSeparator(), CaEffectivePoM)); } // if we have dependencies marked as ignored grab ignored dependencies from the original pom // the effective-pom goal doesn't carry comments List dependencies = getDependencies(originPom); - var ignored = dependencies.stream().filter(d -> d.ignored).map(DependencyAggregator::toPurl).collect(Collectors.toSet()); - var testsDeps = dependencies.stream().filter(DependencyAggregator::isTestDependency).collect(Collectors.toSet()); + var ignored = + dependencies.stream() + .filter(d -> d.ignored) + .map(DependencyAggregator::toPurl) + .collect(Collectors.toSet()); + var testsDeps = + dependencies.stream() + .filter(DependencyAggregator::isTestDependency) + .collect(Collectors.toSet()); var deps = getDependencies(tmpEffPom); var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom)); deps.stream() - .filter(dep -> !testsDeps.contains(dep)) - .map(DependencyAggregator::toPurl) - .filter(dep -> ignored.stream().filter(artifact -> artifact.isCoordinatesEquals(dep)).collect(Collectors.toList()).size() == 0) - .forEach(d -> sbom.addDependency(sbom.getRoot(), d)); + .filter(dep -> !testsDeps.contains(dep)) + .map(DependencyAggregator::toPurl) + .filter( + dep -> + ignored.stream() + .filter(artifact -> artifact.isCoordinatesEquals(dep)) + .collect(Collectors.toList()) + .size() + == 0) + .forEach(d -> sbom.addDependency(sbom.getRoot(), d)); // build and return content for constructing request to the backend return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); @@ -195,7 +219,8 @@ public Content provideComponent(Path manifestPath) throws IOException { private PackageURL getRoot(final Path manifestPath) throws IOException { XMLStreamReader reader = null; try { - reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); + reader = + XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); DependencyAggregator dependencyAggregator = null; boolean isRoot = false; while (reader.hasNext()) { @@ -246,8 +271,9 @@ private List getDependencies(final Path manifestPath) thro List deps = new ArrayList<>(); XMLStreamReader reader = null; try { - //get a xml stream reader for the manifest file - reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); + // get a xml stream reader for the manifest file + reader = + XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); // the following dependencyIgnore object is used to aggregate dependency data over iterations // when a "dependency" tag starts, it will be initiated, // when a "dependency" tag ends, it will be parsed, act upon, and reset @@ -255,9 +281,9 @@ private List getDependencies(final Path manifestPath) thro while (reader.hasNext()) { reader.next(); // get the next event if (reader.isStartElement() && "dependency".equals(reader.getLocalName())) { - // starting "dependency" tag, initiate aggregator - dependencyAggregator = new DependencyAggregator(); - continue; + // starting "dependency" tag, initiate aggregator + dependencyAggregator = new DependencyAggregator(); + continue; } // if dependency aggregator haven't been initiated, @@ -265,8 +291,7 @@ private List getDependencies(final Path manifestPath) thro if (!Objects.isNull(dependencyAggregator)) { // if we hit an ignore comment, mark aggregator to be ignored if (reader.getEventType() == XMLStreamConstants.COMMENT - && "exhortignore".equals(reader.getText().strip()) - ) { + && "exhortignore".equals(reader.getText().strip())) { dependencyAggregator.ignored = true; continue; } @@ -286,7 +311,8 @@ private List getDependencies(final Path manifestPath) thro case "scope": reader.next(); - dependencyAggregator.scope = reader.getText() != null ? reader.getText().trim() : "*"; + dependencyAggregator.scope = + reader.getText() != null ? reader.getText().trim() : "*"; break; case "version": // starting "version" tag, get next event and set to aggregator reader.next(); @@ -318,7 +344,7 @@ private List getDependencies(final Path manifestPath) thro } Map getMvnExecEnvs() { - var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME",""); + var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME", ""); if (javaHome != null && !javaHome.isBlank()) { return Collections.singletonMap("JAVA_HOME", javaHome); } @@ -327,9 +353,9 @@ Map getMvnExecEnvs() { // NOTE if we want to include "scope" tags in ignore, // add property here and a case in the start-element-switch in the getIgnored method - /** Aggregator class for aggregating Dependency data over stream iterations, **/ - private final static class DependencyAggregator { - private String scope="*"; + /** Aggregator class for aggregating Dependency data over stream iterations, * */ + private static final class DependencyAggregator { + private String scope = "*"; private String groupId; private String artifactId; private String version; @@ -337,26 +363,32 @@ private final static class DependencyAggregator { /** * Get the string representation of the dependency to use as excludes + * * @return an exclude string for the dependency:tree plugin, ie. group-id:artifact-id:*:version */ @Override public String toString() { // NOTE if you add scope, don't forget to replace the * with its value - return String.format("%s:%s:%s:%s", groupId, artifactId,scope, version); + return String.format("%s:%s:%s:%s", groupId, artifactId, scope, version); } public boolean isValid() { return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); } - public boolean isTestDependency() - { + public boolean isTestDependency() { return scope.trim().equals("test"); } public PackageURL toPurl() { try { - return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, this.scope == "*" ? null :new TreeMap<>(Map.of("scope",this.scope)), null); + return new PackageURL( + Type.MAVEN.getType(), + groupId, + artifactId, + version, + this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), + null); } catch (MalformedPackageURLException e) { throw new IllegalArgumentException("Unable to parse PackageURL", e); } @@ -370,10 +402,9 @@ public boolean equals(Object o) { // NOTE we do not compare the ignored field // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't // contain comments indicating ignore - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.artifactId, that.artifactId) && - Objects.equals(this.version, that.version); - + return Objects.equals(this.groupId, that.groupId) + && Objects.equals(this.artifactId, that.artifactId) + && Objects.equals(this.version, that.version); } @Override diff --git a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java index f58ab34a..54e4e7d4 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java @@ -15,17 +15,7 @@ */ package com.redhat.exhort.providers; -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.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -40,18 +30,26 @@ import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +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.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; /** - * Concrete implementation of the {@link Provider} used for converting - * dependency trees - * for npm projects (package.json) into a SBOM content for Stack analysis or - * Component analysis. - **/ + * Concrete implementation of the {@link Provider} used for converting dependency trees for npm + * projects (package.json) into a SBOM content for Stack analysis or Component analysis. + */ public final class JavaScriptNpmProvider extends Provider { private System.Logger log = System.getLogger(this.getClass().getName()); + public JavaScriptNpmProvider() { super(Type.NPM); } @@ -60,27 +58,32 @@ public JavaScriptNpmProvider() { public Content provideStack(final Path manifestPath) throws IOException { // check for custom npm executable Sbom sbom = getDependencySbom(manifestPath, true, false); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } @Override public Content provideComponent(byte[] manifestContent) throws IOException { // check for custom npm executable - return new Content(getDependencyTree(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + getDependencyTree(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); } @Override public Content provideComponent(Path manifestPath) throws IOException { - return new Content(getDependencySbom(manifestPath, false,false).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + getDependencySbom(manifestPath, false, false) + .getAsJsonString() + .getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); } private Sbom getDependencyTree(byte[] manifestContent) { Sbom sbom; try { Path tempDir = Files.createTempDirectory("exhort_npm"); - Path path = Files.createFile(Path.of(tempDir.toString(),"package.json")); + Path path = Files.createFile(Path.of(tempDir.toString(), "package.json")); Files.write(path, manifestContent); sbom = getDependencySbom(path, false, true); Files.delete(path); @@ -91,7 +94,8 @@ private Sbom getDependencyTree(byte[] manifestContent) { } private PackageURL getRoot(JsonNode jsonDependenciesNpm) throws MalformedPackageURLException { - return toPurl(jsonDependenciesNpm.get("name").asText(), jsonDependenciesNpm.get("version").asText()); + return toPurl( + jsonDependenciesNpm.get("name").asText(), jsonDependenciesNpm.get("version").asText()); } private PackageURL toPurl(String name, String version) throws MalformedPackageURLException { @@ -110,7 +114,7 @@ private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies String name = e.getKey(); JsonNode versionNode = e.getValue().get("version"); if (versionNode == null) { - continue; //ignore optional dependencies + continue; // ignore optional dependencies } String version = versionNode.asText(); PackageURL purl = toPurl(name, version); @@ -122,47 +126,74 @@ private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies } } - private Sbom getDependencySbom(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) throws IOException { - var npmListResult = buildNpmDependencyTree(manifestPath, includeTransitive,deletePackageLock); + private Sbom getDependencySbom( + Path manifestPath, boolean includeTransitive, boolean deletePackageLock) throws IOException { + var npmListResult = buildNpmDependencyTree(manifestPath, includeTransitive, deletePackageLock); var sbom = buildSbom(npmListResult); sbom.filterIgnoredDeps(getIgnoredDeps(manifestPath)); return sbom; } - private JsonNode buildNpmDependencyTree(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) + private JsonNode buildNpmDependencyTree( + Path manifestPath, boolean includeTransitive, boolean deletePackageLock) throws JsonMappingException, JsonProcessingException { var npm = Operations.getCustomPathOrElse("npm"); var npmEnvs = getNpmExecEnv(); // clean command used to clean build target Path packageLockJson = Path.of(manifestPath.getParent().toString(), "package-lock.json"); - var createPackageLock = new String[] { npm, "i", "--package-lock-only", "--prefix", - manifestPath.getParent().toString() }; + var createPackageLock = + new String[] { + npm, "i", "--package-lock-only", "--prefix", manifestPath.getParent().toString() + }; // execute the clean command Operations.runProcess(createPackageLock, npmEnvs); String[] npmAllDeps; - Path workDir=null; - if(!manifestPath.getParent().toString().trim().contains(" ")) { - - npmAllDeps = new String[]{npm, "ls", includeTransitive ? "--all" : "", "--omit=dev", "--package-lock-only", - "--json", "--prefix", manifestPath.getParent().toString()}; - } - else { - npmAllDeps = new String[]{npm, "ls", includeTransitive ? "--all" : "", "--omit=dev", "--package-lock-only", - "--json"}; + Path workDir = null; + if (!manifestPath.getParent().toString().trim().contains(" ")) { + + npmAllDeps = + new String[] { + npm, + "ls", + includeTransitive ? "--all" : "", + "--omit=dev", + "--package-lock-only", + "--json", + "--prefix", + manifestPath.getParent().toString() + }; + } else { + npmAllDeps = + new String[] { + npm, + "ls", + includeTransitive ? "--all" : "", + "--omit=dev", + "--package-lock-only", + "--json" + }; workDir = manifestPath.getParent(); } // execute the clean command String npmOutput; if (npmEnvs != null) { - npmOutput = Operations.runProcessGetOutput(workDir, npmAllDeps, - npmEnvs.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).toArray(String[]::new)); + npmOutput = + Operations.runProcessGetOutput( + workDir, + npmAllDeps, + npmEnvs.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .toArray(String[]::new)); } else { npmOutput = Operations.runProcessGetOutput(workDir, npmAllDeps); } - if(debugLoggingIsNeeded()) { - log.log(System.Logger.Level.INFO,String.format("Npm Listed Install Pacakges in Json : %s %s",System.lineSeparator(),npmOutput)); + if (debugLoggingIsNeeded()) { + log.log( + System.Logger.Level.INFO, + String.format( + "Npm Listed Install Pacakges in Json : %s %s", System.lineSeparator(), npmOutput)); } - if(!includeTransitive) { + if (!includeTransitive) { if (deletePackageLock) { try { Files.delete(packageLockJson); diff --git a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java index 1e51a6c7..da3a83e7 100644 --- a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java +++ b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; @@ -28,7 +30,6 @@ import com.redhat.exhort.utils.PythonControllerBase; import com.redhat.exhort.utils.PythonControllerRealEnv; import com.redhat.exhort.utils.PythonControllerVirtualEnv; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -38,22 +39,25 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import static com.redhat.exhort.impl.ExhortApi.*; - public final class PythonPipProvider extends Provider { private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + public void setPythonController(PythonControllerBase pythonController) { this.pythonController = pythonController; } private PythonControllerBase pythonController; + public static void main(String[] args) { try { PythonPipProvider pythonPipProvider = new PythonPipProvider(); -// byte[] bytes = Files.readAllBytes(Path.of("/tmp/exhort_env/requirements.txt")); -// Content content = pythonPipProvider.provideComponent(bytes); - Content content = pythonPipProvider.provideStack(Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt")); + // byte[] bytes = Files.readAllBytes(Path.of("/tmp/exhort_env/requirements.txt")); + // Content content = pythonPipProvider.provideComponent(bytes); + Content content = + pythonPipProvider.provideStack( + Path.of( + "/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt")); String s = new String(content.buffer); System.out.print(s); } catch (IOException e) { @@ -68,40 +72,50 @@ public PythonPipProvider() { @Override public Content provideStack(Path manifestPath) throws IOException { PythonControllerBase pythonController = getPythonController(); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), true); + List> dependencies = + pythonController.getDependencies(manifestPath.toString(), true); printDependenciesTree(dependencies); - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); try { sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); } catch (MalformedPackageURLException e) { throw new RuntimeException(e); } - dependencies.stream().forEach((component) -> - { - addAllDependencies(sbom.getRoot(), component, sbom); - - }); + dependencies.stream() + .forEach( + (component) -> { + addAllDependencies(sbom.getRoot(), component, sbom); + }); byte[] requirementsFile = Files.readAllBytes(manifestPath); handleIgnoredDependencies(new String(requirementsFile), sbom); - // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that was created for creating the sbom. + // In python' pip requirements.txt, there is no real root element, then need to remove dummy + // root element that + // was created for creating the sbom. sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + return new Content( + sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } private void addAllDependencies(PackageURL source, Map component, Sbom sbom) { - sbom.addDependency(source, toPurl((String) component.get("name"), (String) component.get("version"))); + sbom.addDependency( + source, toPurl((String) component.get("name"), (String) component.get("version"))); List directDeps = (List) component.get("dependencies"); if (directDeps != null) -// { - directDeps.stream().forEach(dep -> { - String name = (String) dep.get("name"); - String version = (String) dep.get("version"); - - addAllDependencies(toPurl((String) component.get("name"), (String) component.get("version")), dep, sbom); - }); -// -// } + // { + directDeps.stream() + .forEach( + dep -> { + String name = (String) dep.get("name"); + String version = (String) dep.get("version"); + + addAllDependencies( + toPurl((String) component.get("name"), (String) component.get("version")), + dep, + sbom); + }); + // + // } } @@ -109,11 +123,13 @@ private void addAllDependencies(PackageURL source, Map component public Content provideComponent(byte[] manifestContent) throws IOException { PythonControllerBase pythonController = getPythonController(); Path tempRepository = Files.createTempDirectory("exhort-pip"); - Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "requirements.txt"); + Path path = + Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "requirements.txt"); Files.deleteIfExists(path); Path manifestPath = Files.createFile(path); Files.write(manifestPath, manifestContent); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), false); + List> dependencies = + pythonController.getDependencies(manifestPath.toString(), false); printDependenciesTree(dependencies); Sbom sbom = SbomFactory.newInstance(); try { @@ -121,90 +137,104 @@ public Content provideComponent(byte[] manifestContent) throws IOException { } catch (MalformedPackageURLException e) { throw new RuntimeException(e); } - dependencies.stream().forEach((component) -> - { - sbom.addDependency(sbom.getRoot(), toPurl((String) component.get("name"), (String) component.get("version"))); - }); + dependencies.stream() + .forEach( + (component) -> { + sbom.addDependency( + sbom.getRoot(), + toPurl((String) component.get("name"), (String) component.get("version"))); + }); Files.delete(manifestPath); Files.delete(tempRepository); handleIgnoredDependencies(new String(manifestContent), sbom); - // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that was created for creating the sbom. + // In python' pip requirements.txt, there is no real root element, then need to remove dummy + // root element that + // was created for creating the sbom. sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); - + return new Content( + sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } - private void printDependenciesTree(List> dependencies) throws JsonProcessingException { - if(debugLoggingIsNeeded()) { - String pythonControllerTree = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); - log.info(String.format("Python Generated Dependency Tree in Json Format: %s %s %s",System.lineSeparator(),pythonControllerTree,System.lineSeparator())); - + private void printDependenciesTree(List> dependencies) + throws JsonProcessingException { + if (debugLoggingIsNeeded()) { + String pythonControllerTree = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); + log.info( + String.format( + "Python Generated Dependency Tree in Json Format: %s %s %s", + System.lineSeparator(), pythonControllerTree, System.lineSeparator())); } } private void handleIgnoredDependencies(String manifestContent, Sbom sbom) { Set ignoredDeps = getIgnoredDependencies(manifestContent); - Set ignoredDepsVersions = ignoredDeps - .stream() - .filter(dep -> !dep.getVersion().trim().equals("*")) - .map(PackageURL::getCoordinates) - .collect(Collectors.toSet()); - Set ignoredDepsNoVersions = ignoredDeps - .stream() - .filter(dep -> dep.getVersion().trim().equals("*")) - .map(PackageURL::getCoordinates) - .collect(Collectors.toSet()); - -// filter out by name only from sbom all exhortignore dependencies that their version will be resolved by pip. + Set ignoredDepsVersions = + ignoredDeps.stream() + .filter(dep -> !dep.getVersion().trim().equals("*")) + .map(PackageURL::getCoordinates) + .collect(Collectors.toSet()); + Set ignoredDepsNoVersions = + ignoredDeps.stream() + .filter(dep -> dep.getVersion().trim().equals("*")) + .map(PackageURL::getCoordinates) + .collect(Collectors.toSet()); + + // filter out by name only from sbom all exhortignore dependencies that their version will be + // resolved by pip. sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); sbom.filterIgnoredDeps(ignoredDepsNoVersions); boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - // filter out by purl from sbom all exhortignore dependencies that their version hardcoded in requirements.txt - in case all versions in manifest matching installed versions of packages in environment. - if(matchManifestVersions) - { + // filter out by purl from sbom all exhortignore dependencies that their version hardcoded in + // requirements.txt - + // in case all versions in manifest matching installed versions of packages in environment. + if (matchManifestVersions) { sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.PURL); sbom.filterIgnoredDeps(ignoredDepsVersions); - } - else - { -// in case version mismatch is possible (MATCH_MANIFEST_VERSIONS=false) , need to parse the name of package from the purl, and remove the package name from sbom according to name only - Set deps = (Set) ignoredDepsVersions.stream().map(purlString -> { - try { - return new PackageURL((String) purlString).getName(); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); - } - }).collect(Collectors.toSet()); + } else { + // in case version mismatch is possible (MATCH_MANIFEST_VERSIONS=false) , need to parse the + // name of package + // from the purl, and remove the package name from sbom according to name only + Set deps = + (Set) + ignoredDepsVersions.stream() + .map( + purlString -> { + try { + return new PackageURL((String) purlString).getName(); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); sbom.filterIgnoredDeps(deps); } - - - - } private Set getIgnoredDependencies(String requirementsDeps) { String[] requirementsLines = requirementsDeps.split(System.lineSeparator()); - Set collected = Arrays.stream(requirementsLines) - .filter(line -> line.contains("#exhortignore") || line.contains("# exhortignore")) - .map(PythonPipProvider::extractDepFull) - .map(this::splitToNameVersion) - .map(dep -> toPurl(dep[0], dep[1])) -// .map(packageURL -> packageURL.getCoordinates()) - .collect(Collectors.toSet()); - - return collected; + Set collected = + Arrays.stream(requirementsLines) + .filter(line -> line.contains("#exhortignore") || line.contains("# exhortignore")) + .map(PythonPipProvider::extractDepFull) + .map(this::splitToNameVersion) + .map(dep -> toPurl(dep[0], dep[1])) + // .map(packageURL -> packageURL.getCoordinates()) + .collect(Collectors.toSet()); + + return collected; } private String[] splitToNameVersion(String nameVersion) { String[] result; - if (nameVersion.matches("[a-zA-Z0-9-_()]+={2}[0-9]{1,4}[.][0-9]{1,4}(([.][0-9]{1,4})|([.][a-zA-Z0-9]+)|([a-zA-Z0-9]+)|([.][a-zA-Z0-9]+[.][a-z-A-Z0-9]+))?")) { + if (nameVersion.matches( + "[a-zA-Z0-9-_()]+={2}[0-9]{1,4}[.][0-9]{1,4}(([.][0-9]{1,4})|([.][a-zA-Z0-9]+)|([a-zA-Z0-9]+)|([.][a-zA-Z0-9]+[.][a-z-A-Z0-9]+))?")) { result = nameVersion.split("=="); } else { String dependencyName = PythonControllerBase.getDependencyName(nameVersion); - result = new String[]{dependencyName, "*"}; + result = new String[] {dependencyName, "*"}; } return result; } @@ -225,33 +255,37 @@ private PackageURL toPurl(String name, String version) { private PythonControllerBase getPythonController() { String pythonPipBinaries; String useVirtualPythonEnv; - if(!getStringValueEnvironment("EXHORT_PIP_SHOW","").trim().equals("") - && !getStringValueEnvironment("EXHORT_PIP_FREEZE","").trim().equals("")) { + if (!getStringValueEnvironment("EXHORT_PIP_SHOW", "").trim().equals("") + && !getStringValueEnvironment("EXHORT_PIP_FREEZE", "").trim().equals("")) { pythonPipBinaries = "python;;pip"; useVirtualPythonEnv = "false"; - } - else { + } else { pythonPipBinaries = getPythonPipBinaries(); - useVirtualPythonEnv = Objects.requireNonNullElseGet( - System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), - () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); + useVirtualPythonEnv = + Objects.requireNonNullElseGet( + System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), + () -> + Objects.requireNonNullElse( + System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); } String[] parts = pythonPipBinaries.split(";;"); var python = parts[0]; var pip = parts[1]; - useVirtualPythonEnv = Objects.requireNonNullElseGet( - System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), - () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); + useVirtualPythonEnv = + Objects.requireNonNullElseGet( + System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), + () -> + Objects.requireNonNullElse( + System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); PythonControllerBase pythonController; - if(this.pythonController == null) { + if (this.pythonController == null) { if (Boolean.parseBoolean(useVirtualPythonEnv)) { pythonController = new PythonControllerVirtualEnv(python); } else { pythonController = new PythonControllerRealEnv(python, pip); } - } - else { + } else { pythonController = this.pythonController; } return pythonController; @@ -274,6 +308,7 @@ private static String getPythonPipBinaries() { @Override public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException("provideComponent with file system path for Python pip package manager is not supported"); + throw new IllegalArgumentException( + "provideComponent with file system path for Python pip package manager is not supported"); } } diff --git a/src/main/java/com/redhat/exhort/providers/package-info.java b/src/main/java/com/redhat/exhort/providers/package-info.java index cf32e944..efff6d82 100644 --- a/src/main/java/com/redhat/exhort/providers/package-info.java +++ b/src/main/java/com/redhat/exhort/providers/package-info.java @@ -1,5 +1,5 @@ /** - * Package hosting various the content providers generating content - * that will be sent to the Backend API. - **/ + * Package hosting various the content providers generating content that will be sent to the Backend + * API. + */ package com.redhat.exhort.providers; diff --git a/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java b/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java index 85e9fb9c..891b63c5 100644 --- a/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java +++ b/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java @@ -15,14 +15,16 @@ */ package com.redhat.exhort.sbom; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.logging.LoggersFactory; import java.util.*; import java.util.function.BiPredicate; import java.util.function.Predicate; import java.util.logging.Logger; import java.util.stream.Collectors; - -import com.github.packageurl.MalformedPackageURLException; -import com.redhat.exhort.logging.LoggersFactory; import org.cyclonedx.BomGeneratorFactory; import org.cyclonedx.CycloneDxSchema.Version; import org.cyclonedx.model.Bom; @@ -31,122 +33,117 @@ import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Metadata; -import com.github.packageurl.PackageURL; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; - public class CycloneDXSbom implements Sbom { private Logger log = LoggersFactory.getLogger(this.getClass().getName()); private static final Version VERSION = Version.VERSION_14; private String exhortIgnoreMethod; private Bom bom; - private PackageURL root; - - private BiPredicate belongingCriteriaBinaryAlgorithm; + private PackageURL root; - private Predicate genericComparator(BiPredicate binaryBelongingCriteriaAlgorithm, X container) - { - return dep -> binaryBelongingCriteriaAlgorithm.test(container, dep); - } + private BiPredicate belongingCriteriaBinaryAlgorithm; - public CycloneDXSbom() { - bom = new Bom(); - bom.setVersion(1); - Metadata metadata = new Metadata(); - metadata.setTimestamp(new Date()); - bom.setMetadata(metadata); - bom.setComponents(new ArrayList<>()); - bom.setDependencies(new ArrayList<>()); - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - this.exhortIgnoreMethod = "insensitive"; + private Predicate genericComparator( + BiPredicate binaryBelongingCriteriaAlgorithm, X container) { + return dep -> binaryBelongingCriteriaAlgorithm.test(container, dep); + } - } + public CycloneDXSbom() { + bom = new Bom(); + bom.setVersion(1); + Metadata metadata = new Metadata(); + metadata.setTimestamp(new Date()); + bom.setMetadata(metadata); + bom.setComponents(new ArrayList<>()); + bom.setDependencies(new ArrayList<>()); + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + this.exhortIgnoreMethod = "insensitive"; + } private static BiPredicate getBelongingConditionByName() { return (collection, component) -> collection.contains(component.getName()); } - public CycloneDXSbom(BelongingCondition belongingCondition,String exhortIgnoreMethod) { - this(); - if(belongingCondition.equals(BelongingCondition.NAME)) - { - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - else if (belongingCondition.equals(BelongingCondition.PURL)){ - belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); - } - else - { - // fallback to belonging condition by name ( default) - this one in case the enum type will be extended and new BelongingType won't be implemented right away. - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - this.exhortIgnoreMethod = exhortIgnoreMethod; + public CycloneDXSbom(BelongingCondition belongingCondition, String exhortIgnoreMethod) { + this(); + if (belongingCondition.equals(BelongingCondition.NAME)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + } else if (belongingCondition.equals(BelongingCondition.PURL)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); + } else { + // fallback to belonging condition by name ( default) - this one in case the enum type will be + // extended and + // new BelongingType won't be implemented right away. + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); } + this.exhortIgnoreMethod = exhortIgnoreMethod; + } private BiPredicate getBelongingConditionByPurl() { - return (collection, component) -> collection.contains(componentToPurl(component).getCoordinates()); + return (collection, component) -> + collection.contains(componentToPurl(component).getCoordinates()); } public Sbom addRoot(PackageURL rootRef) { - this.root = rootRef; - Component rootComponent = newRootComponent(rootRef); - bom.getMetadata().setComponent(rootComponent); - bom.getComponents().add(rootComponent); - bom.getDependencies().add(newDependency(rootRef)); - return this; - } + this.root = rootRef; + Component rootComponent = newRootComponent(rootRef); + bom.getMetadata().setComponent(rootComponent); + bom.getComponents().add(rootComponent); + bom.getDependencies().add(newDependency(rootRef)); + return this; + } - public PackageURL getRoot() { - return root; - } + public PackageURL getRoot() { + return root; + } @Override public Sbom filterIgnoredDeps(Collection ignoredDeps) { - String exhortIgnoreMethod = Objects.requireNonNullElse(getExhortIgnoreMethod(),this.exhortIgnoreMethod ); - if(exhortIgnoreMethod.equals("insensitive")) - { + String exhortIgnoreMethod = + Objects.requireNonNullElse(getExhortIgnoreMethod(), this.exhortIgnoreMethod); + if (exhortIgnoreMethod.equals("insensitive")) { return filterIgnoredDepsInsensitive(ignoredDeps); - } - else { + } else { return filterIgnoredDepsSensitive(ignoredDeps); } - - } private String getExhortIgnoreMethod() { - boolean result; - return System.getenv("EXHORT_IGNORE_METHOD") != null ? System.getenv("EXHORT_IGNORE_METHOD").trim().toLowerCase() : getExhortIgnoreProperty(); + boolean result; + return System.getenv("EXHORT_IGNORE_METHOD") != null + ? System.getenv("EXHORT_IGNORE_METHOD").trim().toLowerCase() + : getExhortIgnoreProperty(); } private String getExhortIgnoreProperty() { - return System.getProperty("EXHORT_IGNORE_METHOD") != null ? System.getProperty("EXHORT_IGNORE_METHOD").trim().toLowerCase() : null ; + return System.getProperty("EXHORT_IGNORE_METHOD") != null + ? System.getProperty("EXHORT_IGNORE_METHOD").trim().toLowerCase() + : null; } private Component newRootComponent(PackageURL ref) { - Component c = new Component(); - c.setBomRef(ref.getCoordinates()); - c.setName(ref.getName()); - c.setGroup(ref.getNamespace()); - c.setVersion(ref.getVersion()); - c.setType(Type.APPLICATION); - c.setPurl(ref); - return c; - } + Component c = new Component(); + c.setBomRef(ref.getCoordinates()); + c.setName(ref.getName()); + c.setGroup(ref.getNamespace()); + c.setVersion(ref.getVersion()); + c.setType(Type.APPLICATION); + c.setPurl(ref); + return c; + } - private Component newComponent(PackageURL ref) { - Component c = new Component(); - c.setBomRef(ref.getCoordinates()); - c.setName(ref.getName()); - c.setGroup(ref.getNamespace()); - c.setVersion(ref.getVersion()); - c.setPurl(ref); - c.setType(Type.LIBRARY); - return c; - } + private Component newComponent(PackageURL ref) { + Component c = new Component(); + c.setBomRef(ref.getCoordinates()); + c.setName(ref.getName()); + c.setGroup(ref.getNamespace()); + c.setVersion(ref.getVersion()); + c.setPurl(ref); + c.setType(Type.LIBRARY); + return c; + } - private PackageURL componentToPurl(Component component) { + private PackageURL componentToPurl(Component component) { try { return new PackageURL(component.getPurl()); } catch (MalformedPackageURLException e) { @@ -154,147 +151,154 @@ private PackageURL componentToPurl(Component component) { } } - private Dependency newDependency(PackageURL ref) { - return new Dependency(ref.getCoordinates()); - } + private Dependency newDependency(PackageURL ref) { + return new Dependency(ref.getCoordinates()); + } - private Sbom filterIgnoredDepsInsensitive(Collection ignoredDeps) { + private Sbom filterIgnoredDepsInsensitive(Collection ignoredDeps) { - List initialIgnoreRefs = bom.getComponents() - .stream() - .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm,ignoredDeps).test(c)) - .map(Component::getBomRef).collect(Collectors.toList()); - List refsToIgnore = createIgnoreFilter(bom.getDependencies(), - initialIgnoreRefs); - return removeIgnoredDepsFromSbom(refsToIgnore); - } + List initialIgnoreRefs = + bom.getComponents().stream() + .filter( + c -> genericComparator(this.belongingCriteriaBinaryAlgorithm, ignoredDeps).test(c)) + .map(Component::getBomRef) + .collect(Collectors.toList()); + List refsToIgnore = createIgnoreFilter(bom.getDependencies(), initialIgnoreRefs); + return removeIgnoredDepsFromSbom(refsToIgnore); + } private Sbom removeIgnoredDepsFromSbom(List refsToIgnore) { - bom.setComponents(bom.getComponents() - .stream() + bom.setComponents( + bom.getComponents().stream() .filter(c -> !refsToIgnore.contains(c.getBomRef())) .collect(Collectors.toList())); - var newDeps = bom.getDependencies() - .stream() + var newDeps = + bom.getDependencies().stream() .filter(d -> !refsToIgnore.contains(d.getRef())) .collect(Collectors.toList()); bom.setDependencies(newDeps); - bom.getDependencies().stream().forEach(d -> { - if (d.getDependencies() != null) { - var filteredDeps = d.getDependencies() - .stream() - .filter(td -> !refsToIgnore.contains(td.getRef())) - .collect(Collectors.toList()); - d.setDependencies(filteredDeps); - } - }); + bom.getDependencies().stream() + .forEach( + d -> { + if (d.getDependencies() != null) { + var filteredDeps = + d.getDependencies().stream() + .filter(td -> !refsToIgnore.contains(td.getRef())) + .collect(Collectors.toList()); + d.setDependencies(filteredDeps); + } + }); return this; } private Sbom filterIgnoredDepsSensitive(Collection ignoredDeps) { - List refsToIgnore = bom.getComponents() - .stream() - .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm,ignoredDeps).test(c)) - .map(Component::getBomRef).collect(Collectors.toList()); + List refsToIgnore = + bom.getComponents().stream() + .filter( + c -> genericComparator(this.belongingCriteriaBinaryAlgorithm, ignoredDeps).test(c)) + .map(Component::getBomRef) + .collect(Collectors.toList()); return removeIgnoredDepsFromSbom(refsToIgnore); } - private List createIgnoreFilter(List deps, Collection toIgnore) { - List result = new ArrayList<>(toIgnore); - for (Dependency dep : deps) { - if (toIgnore.contains(dep.getRef()) && dep.getDependencies() != null) { - List collected = dep.getDependencies().stream().map(p -> p.getRef()).collect(Collectors.toList()); - result.addAll(collected); - if (dep.getDependencies().stream().filter(p -> p != null).count() > 0) { - result= createIgnoreFilter(dep.getDependencies(), result); - } - + private List createIgnoreFilter(List deps, Collection toIgnore) { + List result = new ArrayList<>(toIgnore); + for (Dependency dep : deps) { + if (toIgnore.contains(dep.getRef()) && dep.getDependencies() != null) { + List collected = + dep.getDependencies().stream().map(p -> p.getRef()).collect(Collectors.toList()); + result.addAll(collected); + if (dep.getDependencies().stream().filter(p -> p != null).count() > 0) { + result = createIgnoreFilter(dep.getDependencies(), result); } - } - return result; } + return result; + } - @Override - public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef) { - Component srcComp = newComponent(sourceRef); - Dependency srcDep; - if (bom.getComponents().stream().noneMatch(c -> c.getBomRef().equals(srcComp.getBomRef()))) { - bom.addComponent(srcComp); - srcDep = newDependency(sourceRef); - bom.addDependency(srcDep); - } else { - Optional existingDep = bom.getDependencies().stream() - .filter(d -> d.getRef().equals(srcComp.getBomRef())).findFirst(); - if (existingDep.isPresent()) { - srcDep = existingDep.get(); - } else { - srcDep = newDependency(sourceRef); - bom.addDependency(srcDep); - } - } - Dependency targetDep = newDependency(targetRef); - srcDep.addDependency(targetDep); - if (bom.getDependencies().stream().noneMatch(d -> d.getRef().equals(targetDep.getRef()))) { - bom.addDependency(targetDep); - } - if (bom.getComponents().stream().noneMatch(c -> c.getBomRef().equals(targetDep.getRef()))) { - bom.addComponent(newComponent(targetRef)); - } - return this; + @Override + public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef) { + Component srcComp = newComponent(sourceRef); + Dependency srcDep; + if (bom.getComponents().stream().noneMatch(c -> c.getBomRef().equals(srcComp.getBomRef()))) { + bom.addComponent(srcComp); + srcDep = newDependency(sourceRef); + bom.addDependency(srcDep); + } else { + Optional existingDep = + bom.getDependencies().stream() + .filter(d -> d.getRef().equals(srcComp.getBomRef())) + .findFirst(); + if (existingDep.isPresent()) { + srcDep = existingDep.get(); + } else { + srcDep = newDependency(sourceRef); + bom.addDependency(srcDep); + } } + Dependency targetDep = newDependency(targetRef); + srcDep.addDependency(targetDep); + if (bom.getDependencies().stream().noneMatch(d -> d.getRef().equals(targetDep.getRef()))) { + bom.addDependency(targetDep); + } + if (bom.getComponents().stream().noneMatch(c -> c.getBomRef().equals(targetDep.getRef()))) { + bom.addComponent(newComponent(targetRef)); + } + return this; + } - @Override - public String getAsJsonString() { - String jsonString = BomGeneratorFactory.createJson(VERSION, bom).toJsonString(); - if(debugLoggingIsNeeded()) - { - log.info("Generated Sbom Json:" + System.lineSeparator() + jsonString); - } - return jsonString; + @Override + public String getAsJsonString() { + String jsonString = BomGeneratorFactory.createJson(VERSION, bom).toJsonString(); + if (debugLoggingIsNeeded()) { + log.info("Generated Sbom Json:" + System.lineSeparator() + jsonString); } + return jsonString; + } @Override public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition) { - if(belongingCondition.equals(BelongingCondition.NAME)) - { + if (belongingCondition.equals(BelongingCondition.NAME)) { belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - else if (belongingCondition.equals(BelongingCondition.PURL)){ + } else if (belongingCondition.equals(BelongingCondition.PURL)) { belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); } - } @Override public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name) { boolean result = false; - Optional comp = this.bom.getDependencies().stream().filter(c -> c.getRef().equals(component.getCoordinates())).findFirst(); - if(comp.isPresent()) - { + Optional comp = + this.bom.getDependencies().stream() + .filter(c -> c.getRef().equals(component.getCoordinates())) + .findFirst(); + if (comp.isPresent()) { Dependency targetComponent = comp.get(); List deps = targetComponent.getDependencies(); - List allDirectDeps = deps.stream().map(dep -> { - try { - return new PackageURL(dep.getRef()); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); - } - }).collect(Collectors.toList()); + List allDirectDeps = + deps.stream() + .map( + dep -> { + try { + return new PackageURL(dep.getRef()); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); result = allDirectDeps.stream().filter(dep -> dep.getName().equals(name)).count() > 0; - } return result; } @Override - public void removeRootComponent() - { - bom.getComponents().removeIf( (component) -> component.getBomRef().equals(this.root.getCoordinates())); - bom.getDependencies().removeIf( (dependency) -> dependency.getRef().equals(this.root.getCoordinates())); + public void removeRootComponent() { + bom.getComponents() + .removeIf((component) -> component.getBomRef().equals(this.root.getCoordinates())); + bom.getDependencies() + .removeIf((dependency) -> dependency.getRef().equals(this.root.getCoordinates())); bom.getMetadata().setComponent(null); } - } diff --git a/src/main/java/com/redhat/exhort/sbom/Sbom.java b/src/main/java/com/redhat/exhort/sbom/Sbom.java index 079d94a8..45a49750 100644 --- a/src/main/java/com/redhat/exhort/sbom/Sbom.java +++ b/src/main/java/com/redhat/exhort/sbom/Sbom.java @@ -15,36 +15,39 @@ */ package com.redhat.exhort.sbom; -import java.util.Collection; import com.github.packageurl.PackageURL; +import java.util.Collection; public interface Sbom { - public Sbom addRoot(PackageURL root); - public PackageURL getRoot(); - public Sbom filterIgnoredDeps(Collection ignoredDeps); - public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef); - public String getAsJsonString(); - public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition); + public Sbom addRoot(PackageURL root); + + public PackageURL getRoot(); + + public Sbom filterIgnoredDeps(Collection ignoredDeps); - public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name); + public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef); - void removeRootComponent(); + public String getAsJsonString(); - public enum BelongingCondition - { - NAME("name"), - PURL("purl"); + public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition); - String belongingCondition; + public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name); - BelongingCondition(String belongingCondition) { - this.belongingCondition = belongingCondition; - } + void removeRootComponent(); - public String getBelongingCondition() { - return belongingCondition; - } + public enum BelongingCondition { + NAME("name"), + PURL("purl"); + + String belongingCondition; + + BelongingCondition(String belongingCondition) { + this.belongingCondition = belongingCondition; } + public String getBelongingCondition() { + return belongingCondition; + } + } } diff --git a/src/main/java/com/redhat/exhort/sbom/SbomFactory.java b/src/main/java/com/redhat/exhort/sbom/SbomFactory.java index 820a6043..5d45b771 100644 --- a/src/main/java/com/redhat/exhort/sbom/SbomFactory.java +++ b/src/main/java/com/redhat/exhort/sbom/SbomFactory.java @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.sbom; public class SbomFactory { - public static Sbom newInstance() { - return new CycloneDXSbom(); - } - public static Sbom newInstance(Sbom.BelongingCondition belongingCondition,String exhortIgnoreMethod) { - return new CycloneDXSbom(belongingCondition,exhortIgnoreMethod); - } + public static Sbom newInstance() { + return new CycloneDXSbom(); + } + public static Sbom newInstance( + Sbom.BelongingCondition belongingCondition, String exhortIgnoreMethod) { + return new CycloneDXSbom(belongingCondition, exhortIgnoreMethod); + } } diff --git a/src/main/java/com/redhat/exhort/tools/Ecosystem.java b/src/main/java/com/redhat/exhort/tools/Ecosystem.java index 782d2332..e6fb90d1 100644 --- a/src/main/java/com/redhat/exhort/tools/Ecosystem.java +++ b/src/main/java/com/redhat/exhort/tools/Ecosystem.java @@ -15,26 +15,23 @@ */ package com.redhat.exhort.tools; -import java.nio.file.Path; - - import com.redhat.exhort.Provider; import com.redhat.exhort.providers.GoModulesProvider; +import com.redhat.exhort.providers.GradleProvider; import com.redhat.exhort.providers.JavaMavenProvider; import com.redhat.exhort.providers.JavaScriptNpmProvider; import com.redhat.exhort.providers.PythonPipProvider; -import com.redhat.exhort.providers.GradleProvider; +import java.nio.file.Path; -/** Utility class used for instantiating providers. **/ +/** Utility class used for instantiating providers. * */ public final class Ecosystem { public enum Type { - - MAVEN ("maven"), - NPM ("npm"), - GOLANG ("golang"), - PYTHON ("pypi"), - GRADLE ("gradle"); + MAVEN("maven"), + NPM("npm"), + GOLANG("golang"), + PYTHON("pypi"), + GRADLE("gradle"); String type; @@ -45,9 +42,9 @@ public String getType() { Type(String type) { this.type = type; } - } - private Ecosystem(){ + + private Ecosystem() { // constructor not required for a utility class } @@ -81,8 +78,7 @@ public static Provider getProvider(final String manifestType) { return new GradleProvider(); default: - throw new IllegalStateException(String.format("Unknown manifest file %s", manifestType) - ); + throw new IllegalStateException(String.format("Unknown manifest file %s", manifestType)); } } } diff --git a/src/main/java/com/redhat/exhort/tools/Operations.java b/src/main/java/com/redhat/exhort/tools/Operations.java index bfd948da..4a4a3d13 100644 --- a/src/main/java/com/redhat/exhort/tools/Operations.java +++ b/src/main/java/com/redhat/exhort/tools/Operations.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.tools; +import static java.lang.String.join; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -25,37 +27,34 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static java.lang.String.join; - -/** Utility class used for executing process on the operating system. **/ +/** Utility class used for executing process on the operating system. * */ public final class Operations { - private Operations(){ + private Operations() { // constructor not required for a utility class } /** * Function for looking up custom executable path based on the default one provides as an - * argument. I.e. if defaultExecutable=mvn, this function will look for a custom mvn path - * set as an environment variable or a java property with the name EXHORT_MVN_PATH. If not found, - * the original mvn passed as defaultExecutable will be returned. - * Note, environment variables takes precedence on java properties. + * argument. I.e. if defaultExecutable=mvn, this function will look for a custom mvn path set as + * an environment variable or a java property with the name EXHORT_MVN_PATH. If not found, the + * original mvn passed as defaultExecutable will be returned. Note, environment variables takes + * precedence on java properties. * - * @param defaultExecutable default executable (uppercase spaces and dashes will be replaced with underscores). + * @param defaultExecutable default executable (uppercase spaces and dashes will be replaced with + * underscores). * @return the custom path from the relevant environment variable or the original argument. */ public static String getCustomPathOrElse(String defaultExecutable) { - var target = defaultExecutable.toUpperCase() - .replaceAll(" ", "_") - .replaceAll("-", "_"); + var target = defaultExecutable.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_"); var executableKey = String.format("EXHORT_%s_PATH", target); return Objects.requireNonNullElseGet( - System.getenv(executableKey), - () -> Objects.requireNonNullElse(System.getProperty(executableKey) ,defaultExecutable)); + System.getenv(executableKey), + () -> Objects.requireNonNullElse(System.getProperty(executableKey), defaultExecutable)); } /** - * Function for building a command from the command parts list and execute it as a process on - * the operating system. Will throw a RuntimeException if the command build or execution failed. + * Function for building a command from the command parts list and execute it as a process on the + * operating system. Will throw a RuntimeException if the command build or execution failed. * * @param cmdList list of command parts */ @@ -75,12 +74,8 @@ public static void runProcess(final String[] cmdList, final Map process = processBuilder.start(); } catch (final IOException e) { throw new RuntimeException( - String.format( - "failed to build process for '%s' got %s", - join(" ", cmdList), - e.getMessage() - ) - ); + String.format( + "failed to build process for '%s' got %s", join(" ", cmdList), e.getMessage())); } // execute the command or throw runtime exception if failed @@ -90,40 +85,33 @@ public static void runProcess(final String[] cmdList, final Map } catch (final InterruptedException e) { throw new RuntimeException( - String.format( - "built process for '%s' interrupted, got %s", - join(" ", cmdList), - e.getMessage() - ) - ); + String.format( + "built process for '%s' interrupted, got %s", join(" ", cmdList), e.getMessage())); } // verify the command was executed successfully or throw a runtime exception if (exitCode != 0) { - String errMsg = new BufferedReader(new InputStreamReader(process.getErrorStream())) - .lines().collect(Collectors.joining(System.lineSeparator())); + String errMsg = + new BufferedReader(new InputStreamReader(process.getErrorStream())) + .lines() + .collect(Collectors.joining(System.lineSeparator())); if (errMsg.isEmpty()) { - errMsg = new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines().collect(Collectors.joining(System.lineSeparator())); + errMsg = + new BufferedReader(new InputStreamReader(process.getInputStream())) + .lines() + .collect(Collectors.joining(System.lineSeparator())); } if (errMsg.isEmpty()) { throw new RuntimeException( - String.format( - "failed to execute '%s', exit-code %d", - join(" ", cmdList), - exitCode - ) - ); + String.format("failed to execute '%s', exit-code %d", join(" ", cmdList), exitCode)); } else { throw new RuntimeException( - String.format( - "failed to execute '%s', exit-code %d, message:%s%s%s", - join(" ", cmdList), - exitCode, - System.lineSeparator(), - errMsg, - System.lineSeparator() - ) - ); + String.format( + "failed to execute '%s', exit-code %d, message:%s%s%s", + join(" ", cmdList), + exitCode, + System.lineSeparator(), + errMsg, + System.lineSeparator())); } } } @@ -137,15 +125,13 @@ public static String runProcessGetOutput(Path dir, final String[] cmdList, Strin try { Process process; InputStream inputStream; - if(dir == null) { + if (dir == null) { if (envList != null) { process = Runtime.getRuntime().exec(join(" ", cmdList), envList); } else { process = Runtime.getRuntime().exec(join(" ", cmdList)); } - } - else - { + } else { if (envList != null) { process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); } else { @@ -153,24 +139,19 @@ public static String runProcessGetOutput(Path dir, final String[] cmdList, Strin } } + inputStream = process.getInputStream(); - inputStream = process.getInputStream(); - - BufferedReader reader = new BufferedReader( - new InputStreamReader(inputStream)); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; - while((line = reader.readLine()) != null) - { + while ((line = reader.readLine()) != null) { sb.append(line); - if (!line.endsWith(System.lineSeparator())) - { + if (!line.endsWith(System.lineSeparator())) { sb.append("\n"); } } - if(sb.toString().trim().equals("")) { + if (sb.toString().trim().equals("")) { inputStream = process.getErrorStream(); - reader = new BufferedReader( - new InputStreamReader(inputStream)); + reader = new BufferedReader(new InputStreamReader(inputStream)); while ((line = reader.readLine()) != null) { sb.append(line); if (!line.endsWith(System.lineSeparator())) { @@ -179,12 +160,14 @@ public static String runProcessGetOutput(Path dir, final String[] cmdList, Strin } } } catch (IOException e) { - throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ",cmdList)),e); + throw new RuntimeException( + String.format("Failed to execute command '%s' ", join(" ", cmdList)), e); } return sb.toString(); } - public static ProcessExecOutput runProcessGetFullOutput(Path dir, final String[] cmdList, String[] envList) { + public static ProcessExecOutput runProcessGetFullOutput( + Path dir, final String[] cmdList, String[] envList) { try { Process process; if (dir == null) { @@ -224,7 +207,8 @@ public static ProcessExecOutput runProcessGetFullOutput(Path dir, final String[] return new ProcessExecOutput(output.toString(), error.toString(), process.exitValue()); } catch (IOException | InterruptedException e) { - throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ",cmdList)),e); + throw new RuntimeException( + String.format("Failed to execute command '%s' ", join(" ", cmdList)), e); } } diff --git a/src/main/java/com/redhat/exhort/tools/package-info.java b/src/main/java/com/redhat/exhort/tools/package-info.java index 5e5ec006..f5dae356 100644 --- a/src/main/java/com/redhat/exhort/tools/package-info.java +++ b/src/main/java/com/redhat/exhort/tools/package-info.java @@ -1,2 +1,2 @@ -/** Package hosting various utility and tools used throughout the project. **/ -package com.redhat.exhort.tools; \ No newline at end of file +/** Package hosting various utility and tools used throughout the project. * */ +package com.redhat.exhort.tools; diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java b/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java index 796a905e..4dd1834d 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java @@ -15,12 +15,13 @@ */ package com.redhat.exhort.utils; +import static com.redhat.exhort.impl.ExhortApi.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.exception.PackageNotInstalledException; import com.redhat.exhort.logging.LoggersFactory; import com.redhat.exhort.tools.Operations; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -31,52 +32,51 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.redhat.exhort.impl.ExhortApi.*; -import static java.lang.String.join; - public abstract class PythonControllerBase { public static void main(String[] args) { PythonControllerBase pythonController; -// pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); + // pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); LocalDateTime start = LocalDateTime.now(); List> dependencies; -// dependencies = pythonController.getDependencies("/tmp/requirements.txt",true); + // dependencies = pythonController.getDependencies("/tmp/requirements.txt",true); LocalDateTime end = LocalDateTime.now(); System.out.println("start time:" + start + "\n"); - System.out.println("end time:" + end + "\n"); - System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); - pythonController = new PythonControllerRealEnv("/usr/bin/python3","/usr/bin/pip3"); + System.out.println("end time:" + end + "\n"); + System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n"); + pythonController = new PythonControllerRealEnv("/usr/bin/python3", "/usr/bin/pip3"); start = LocalDateTime.now(); try { - dependencies = pythonController.getDependencies("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt",true); + dependencies = + pythonController.getDependencies( + "/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt", + true); } catch (PackageNotInstalledException e) { System.out.println(e.getMessage()); - dependencies =null; + dependencies = null; } end = LocalDateTime.now(); -// LocalDateTime startNaive = LocalDateTime.now(); -// List> dependenciesNaive = pythonController.getDependenciesNaive(); -// LocalDateTime endNaive = LocalDateTime.now(); + // LocalDateTime startNaive = LocalDateTime.now(); + // List> dependenciesNaive = pythonController.getDependenciesNaive(); + // LocalDateTime endNaive = LocalDateTime.now(); System.out.println("start time:" + start + "\n"); - System.out.println("end time:" + end + "\n"); - System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); -// System.out.println("naive start time:" + startNaive + "\n" ); -// System.out.println("naive end time:" + endNaive + "\n"); -// System.out.println("elapsed time: " + startNaive.until(endNaive, ChronoUnit.SECONDS)); + System.out.println("end time:" + end + "\n"); + System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n"); + // System.out.println("naive start time:" + startNaive + "\n" ); + // System.out.println("naive end time:" + endNaive + "\n"); + // System.out.println("elapsed time: " + startNaive.until(endNaive, ChronoUnit.SECONDS)); ObjectMapper om = new ObjectMapper(); try { String json = om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); System.out.println(json); -// System.out.println(pythonController.counter); + // System.out.println(pythonController.counter); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); protected Path pythonEnvironmentDir; protected Path pipBinaryDir; @@ -85,7 +85,7 @@ public static void main(String[] args) { protected String pipBinaryLocation; -// public int counter =0; + // public int counter =0; public abstract void prepareEnvironment(String pathToPythonBin); @@ -93,129 +93,143 @@ public static void main(String[] args) { public abstract boolean isRealEnv(); - void installPackages(String pathToRequirements) - { + void installPackages(String pathToRequirements) { Operations.runProcess(pipBinaryLocation, "install", "-r", pathToRequirements); Operations.runProcess(pipBinaryLocation, "freeze"); - } public abstract boolean isVirtualEnv(); public abstract void cleanEnvironment(boolean deleteEnvironment); - -// public List> getDependenciesNaive() -// { -// List> dependencies = new ArrayList<>(); -// String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); -// String[] deps = freeze.split(System.lineSeparator()); -// Arrays.stream(deps).forEach(dep -> -// { -// Map component = new HashMap<>(); -// dependencies.add(component); -// bringAllDependenciesNaive(component, getDependencyName(dep)); -// }); -// -// -// -// return dependencies; -// } -// -// private void bringAllDependenciesNaive(Map dependencies, String depName) { -// -// if(dependencies == null || depName.trim().equals("")) -// return; -// counter++; -// LocalDateTime start = LocalDateTime.now(); -// String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depName); -// LocalDateTime end = LocalDateTime.now(); -// System.out.println("pip show start time:" + start + "\n"); -// System.out.println("pip show end time:" + end + "\n"); -// System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); -// String depVersion = getDependencyVersion(pipShowOutput); -// List directDeps = getDepsList(pipShowOutput); -// dependencies.put("name", depName); -// dependencies.put("version",depVersion); -// List> targetDeps = new ArrayList<>(); -// directDeps.stream().forEach(d -> { -// Map myMap = new HashMap<>(); -// targetDeps.add(myMap); -// bringAllDependenciesNaive(myMap,d); -// }); -// dependencies.put("dependencies",targetDeps); -// -// } -// public List> getDependencies() -// { -// List> dependencies = new ArrayList<>(); -// String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); -// String[] deps = freeze.split(System.lineSeparator()); -// String depNames = Arrays.stream(deps).map(this::getDependencyName).collect(Collectors.joining(" ")); -// bringAllDependencies(dependencies, depNames); -// -// -// -// -// return dependencies; -// } -// -// private void bringAllDependencies(List> dependencies, String depName) { -// -// if (dependencies == null || depName.trim().equals("")) -// return; -// counter++; -// LocalDateTime start = LocalDateTime.now(); -// String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depName); -// LocalDateTime end = LocalDateTime.now(); -// System.out.println("pip show start time:" + start + "\n"); -// System.out.println("pip show end time:" + end + "\n"); -// System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.MILLIS) + "\n" ); -// List allLines = Arrays.stream(pipShowOutput.split("---")).collect(Collectors.toList()); -// allLines.stream().forEach(record -> { -// String depVersion = getDependencyVersion(record); -// List directDeps = getDepsList(record); -// getDependencyNameShow(record); -// Map entry = new HashMap(); -// dependencies.add(entry); -// entry.put("name", getDependencyNameShow(record)); -// entry.put("version", depVersion); -// List> targetDeps = new ArrayList<>(); -// String depsList = directDeps.stream().map(str -> str.replace(",", "")).collect(Collectors.joining(" ")); -// bringAllDependencies(targetDeps, depsList); -// entry.put("dependencies",targetDeps); -// }); -// } - - public final List> getDependencies(String pathToRequirements, boolean includeTransitive) { - if(isVirtualEnv() || isRealEnv() ) { + // public List> getDependenciesNaive() + // { + // List> dependencies = new ArrayList<>(); + // String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, + // "freeze"); + // String[] deps = freeze.split(System.lineSeparator()); + // Arrays.stream(deps).forEach(dep -> + // { + // Map component = new HashMap<>(); + // dependencies.add(component); + // bringAllDependenciesNaive(component, getDependencyName(dep)); + // }); + // + // + // + // return dependencies; + // } + // + // private void bringAllDependenciesNaive(Map dependencies, String depName) { + // + // if(dependencies == null || depName.trim().equals("")) + // return; + // counter++; + // LocalDateTime start = LocalDateTime.now(); + // String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, + // pipBinaryLocation, "show", + // depName); + // LocalDateTime end = LocalDateTime.now(); + // System.out.println("pip show start time:" + start + "\n"); + // System.out.println("pip show end time:" + end + "\n"); + // System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" + // ); + // String depVersion = getDependencyVersion(pipShowOutput); + // List directDeps = getDepsList(pipShowOutput); + // dependencies.put("name", depName); + // dependencies.put("version",depVersion); + // List> targetDeps = new ArrayList<>(); + // directDeps.stream().forEach(d -> { + // Map myMap = new HashMap<>(); + // targetDeps.add(myMap); + // bringAllDependenciesNaive(myMap,d); + // }); + // dependencies.put("dependencies",targetDeps); + // + // } + // public List> getDependencies() + // { + // List> dependencies = new ArrayList<>(); + // String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, + // "freeze"); + // String[] deps = freeze.split(System.lineSeparator()); + // String depNames = + // Arrays.stream(deps).map(this::getDependencyName).collect(Collectors.joining(" ")); + // bringAllDependencies(dependencies, depNames); + // + // + // + // + // return dependencies; + // } + // + // private void bringAllDependencies(List> dependencies, String depName) { + // + // if (dependencies == null || depName.trim().equals("")) + // return; + // counter++; + // LocalDateTime start = LocalDateTime.now(); + // String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, + // pipBinaryLocation, "show", + // depName); + // LocalDateTime end = LocalDateTime.now(); + // System.out.println("pip show start time:" + start + "\n"); + // System.out.println("pip show end time:" + end + "\n"); + // System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.MILLIS) + "\n" + // ); + // List allLines = + // Arrays.stream(pipShowOutput.split("---")).collect(Collectors.toList()); + // allLines.stream().forEach(record -> { + // String depVersion = getDependencyVersion(record); + // List directDeps = getDepsList(record); + // getDependencyNameShow(record); + // Map entry = new HashMap(); + // dependencies.add(entry); + // entry.put("name", getDependencyNameShow(record)); + // entry.put("version", depVersion); + // List> targetDeps = new ArrayList<>(); + // String depsList = directDeps.stream().map(str -> str.replace(",", + // "")).collect(Collectors.joining(" ")); + // bringAllDependencies(targetDeps, depsList); + // entry.put("dependencies",targetDeps); + // }); + // } + + public final List> getDependencies( + String pathToRequirements, boolean includeTransitive) { + if (isVirtualEnv() || isRealEnv()) { prepareEnvironment(pathToPythonBin); } - if(automaticallyInstallPackageOnEnvironment()) - { - boolean installBestEfforts = getBooleanValueEnvironment("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "false"); - // make best efforts to install the requirements.txt on the virtual environment created from the python3 passed in. - // that means that it will install the packages without referring to the versions, but will let pip choose the version - // tailored for version of the python environment( and of pip package manager) for each package. - if(installBestEfforts) - { - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - if(matchManifestVersions) - { - throw new RuntimeException("Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false"); - } - else { + if (automaticallyInstallPackageOnEnvironment()) { + boolean installBestEfforts = + getBooleanValueEnvironment("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "false"); + // make best efforts to install the requirements.txt on the virtual environment created from + // the python3 + // passed in. + // that means that it will install the packages without referring to the versions, but will + // let pip choose + // the version + // tailored for version of the python environment( and of pip package manager) for each + // package. + if (installBestEfforts) { + boolean matchManifestVersions = + getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); + if (matchManifestVersions) { + throw new RuntimeException( + "Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with" + + " MATCH_MANIFEST_VERSIONS=false"); + } else { installingRequirementsOneByOne(pathToRequirements); } } - // + // else { installPackages(pathToRequirements); } } - List> dependencies = getDependenciesImpl(pathToRequirements, includeTransitive); - if(isVirtualEnv()) - { + List> dependencies = + getDependenciesImpl(pathToRequirements, includeTransitive); + if (isVirtualEnv()) { cleanEnvironment(false); } @@ -225,54 +239,82 @@ public final List> getDependencies(String pathToRequirements, private void installingRequirementsOneByOne(String pathToRequirements) { try { List requirementsRows = Files.readAllLines(Path.of(pathToRequirements)); - requirementsRows.stream().filter((line) -> !line.trim().startsWith("#")).filter((line) -> !line.trim().equals("")).forEach((dependency) -> - { - String dependencyName = getDependencyName(dependency); - try { - Operations.runProcess(this.pipBinaryLocation,"install",dependencyName); - } catch (RuntimeException e) { - throw new RuntimeException(String.format("Best efforts process - failed installing package - %s in created virtual python environment --> error message got from underlying process => %s ",dependencyName,e.getMessage())); - } - }); - + requirementsRows.stream() + .filter((line) -> !line.trim().startsWith("#")) + .filter((line) -> !line.trim().equals("")) + .forEach( + (dependency) -> { + String dependencyName = getDependencyName(dependency); + try { + Operations.runProcess(this.pipBinaryLocation, "install", dependencyName); + } catch (RuntimeException e) { + throw new RuntimeException( + String.format( + "Best efforts process - failed installing package - %s in created virtual" + + " python environment --> error message got from underlying process" + + " => %s ", + dependencyName, e.getMessage())); + } + }); } catch (IOException e) { - throw new RuntimeException("Cannot continue with analysis - error opening requirements.txt file in order to install packages one by one in a best efforts manner - related error message => " + e.getMessage()); + throw new RuntimeException( + "Cannot continue with analysis - error opening requirements.txt file in order to install" + + " packages one by one in a best efforts manner - related error message => " + + e.getMessage()); } } - private List> getDependenciesImpl(String pathToRequirements, boolean includeTransitive) { - List> dependencies = new ArrayList<>(); + private List> getDependenciesImpl( + String pathToRequirements, boolean includeTransitive) { + List> dependencies = new ArrayList<>(); String freeze = getPipFreezeFromEnvironment(); String freezeMessage = ""; - if(debugLoggingIsNeeded()) { - freezeMessage = String.format("Package Manager PIP freeze --all command result output -> %s %s", System.lineSeparator(), freeze); + if (debugLoggingIsNeeded()) { + freezeMessage = + String.format( + "Package Manager PIP freeze --all command result output -> %s %s", + System.lineSeparator(), freeze); log.info(freezeMessage); } String[] deps = freeze.split(System.lineSeparator()); - String depNames = Arrays.stream(deps).map(PythonControllerBase::getDependencyName).collect(Collectors.joining(" ")); + String depNames = + Arrays.stream(deps) + .map(PythonControllerBase::getDependencyName) + .collect(Collectors.joining(" ")); String pipShowOutput = getPipShowFromEnvironment(depNames); - if(debugLoggingIsNeeded()) { - String pipShowMessage = String.format("Package Manager PIP show command result output -> %s %s", System.lineSeparator(), pipShowOutput); + if (debugLoggingIsNeeded()) { + String pipShowMessage = + String.format( + "Package Manager PIP show command result output -> %s %s", + System.lineSeparator(), pipShowOutput); log.info(pipShowMessage); } List allPipShowLines = splitPipShowLines(pipShowOutput); boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - Map CachedTree = new HashMap<>(); + Map CachedTree = new HashMap<>(); List linesOfRequirements; try { - linesOfRequirements = Files.readAllLines(Path.of(pathToRequirements)).stream().filter( (line) -> !line.startsWith("#")).map(String::trim).collect(Collectors.toList()); + linesOfRequirements = + Files.readAllLines(Path.of(pathToRequirements)).stream() + .filter((line) -> !line.startsWith("#")) + .map(String::trim) + .collect(Collectors.toList()); } catch (IOException e) { throw new RuntimeException(e); } - allPipShowLines.stream().forEach(record -> { - String dependencyNameShow = getDependencyNameShow(record); - StringInsensitive stringInsensitive = new StringInsensitive(dependencyNameShow); - CachedTree.put(stringInsensitive,record); - CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("-","_")),record); - CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("_","-")),record); - }); + allPipShowLines.stream() + .forEach( + record -> { + String dependencyNameShow = getDependencyNameShow(record); + StringInsensitive stringInsensitive = new StringInsensitive(dependencyNameShow); + CachedTree.put(stringInsensitive, record); + CachedTree.putIfAbsent( + new StringInsensitive(dependencyNameShow.replace("-", "_")), record); + CachedTree.putIfAbsent( + new StringInsensitive(dependencyNameShow.replace("_", "-")), record); + }); ObjectMapper om = new ObjectMapper(); String tree; try { @@ -280,93 +322,119 @@ private List> getDependenciesImpl(String pathToRequirements, } catch (JsonProcessingException e) { throw new RuntimeException(e); } - linesOfRequirements.stream().forEach( dep -> { - if(matchManifestVersions) - { - String dependencyName; - String manifestVersion; - String installedVersion = ""; - int doubleEqualSignPosition; - if(dep.contains("==")) - { - doubleEqualSignPosition = dep.indexOf("=="); - manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); - if(manifestVersion.contains("#")) - { - var hashCharIndex = manifestVersion.indexOf("#"); - manifestVersion = manifestVersion.substring(0,hashCharIndex); - } - dependencyName = getDependencyName(dep); - String pipShowRecord = CachedTree.get(new StringInsensitive(dependencyName)); - if(pipShowRecord != null) - { - installedVersion = getDependencyVersion(pipShowRecord); - } - if(!installedVersion.trim().equals("")) { - if (!manifestVersion.trim().equals(installedVersion.trim())) { - throw new RuntimeException(String.format("Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", dependencyName, manifestVersion, installedVersion)); + linesOfRequirements.stream() + .forEach( + dep -> { + if (matchManifestVersions) { + String dependencyName; + String manifestVersion; + String installedVersion = ""; + int doubleEqualSignPosition; + if (dep.contains("==")) { + doubleEqualSignPosition = dep.indexOf("=="); + manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); + if (manifestVersion.contains("#")) { + var hashCharIndex = manifestVersion.indexOf("#"); + manifestVersion = manifestVersion.substring(0, hashCharIndex); + } + dependencyName = getDependencyName(dep); + String pipShowRecord = CachedTree.get(new StringInsensitive(dependencyName)); + if (pipShowRecord != null) { + installedVersion = getDependencyVersion(pipShowRecord); + } + if (!installedVersion.trim().equals("")) { + if (!manifestVersion.trim().equals(installedVersion.trim())) { + throw new RuntimeException( + String.format( + "Can't continue with analysis - versions mismatch for dependency" + + " name=%s, manifest version=%s, installed Version=%s, if you" + + " want to allow version mismatch for analysis between installed" + + " and requested packages, set environment variable/setting -" + + " MATCH_MANIFEST_VERSIONS=false", + dependencyName, manifestVersion, installedVersion)); + } + } + } } - } - } - } - List path = new ArrayList<>(); - String depName = getDependencyName(dep.toLowerCase()); - path.add(depName); - bringAllDependencies(dependencies, depName,CachedTree, includeTransitive,path); - }); + List path = new ArrayList<>(); + String depName = getDependencyName(dep.toLowerCase()); + path.add(depName); + bringAllDependencies(dependencies, depName, CachedTree, includeTransitive, path); + }); return dependencies; } private String getPipShowFromEnvironment(String depNames) { - return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_SHOW",pipBinaryLocation,"show",depNames); -// return Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depNames); + return getPipCommandInvokedOrDecodedFromEnvironment( + "EXHORT_PIP_SHOW", pipBinaryLocation, "show", depNames); + // return Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", + // depNames); } String getPipFreezeFromEnvironment() { - return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_FREEZE",pipBinaryLocation,"freeze","--all"); + return getPipCommandInvokedOrDecodedFromEnvironment( + "EXHORT_PIP_FREEZE", pipBinaryLocation, "freeze", "--all"); } private String getPipCommandInvokedOrDecodedFromEnvironment(String EnvVar, String... cmdList) { - return getStringValueEnvironment(EnvVar, "").trim().equals("") ? Operations.runProcessGetOutput(pythonEnvironmentDir, cmdList) : new String(Base64.getDecoder().decode(getStringValueEnvironment(EnvVar, ""))); + return getStringValueEnvironment(EnvVar, "").trim().equals("") + ? Operations.runProcessGetOutput(pythonEnvironmentDir, cmdList) + : new String(Base64.getDecoder().decode(getStringValueEnvironment(EnvVar, ""))); } - private void bringAllDependencies(List> dependencies, String depName, Map cachedTree, boolean includeTransitive, List path) { + private void bringAllDependencies( + List> dependencies, + String depName, + Map cachedTree, + boolean includeTransitive, + List path) { - if (dependencies == null || depName.trim().equals("")) - return; + if (dependencies == null || depName.trim().equals("")) return; String record = cachedTree.get(new StringInsensitive(depName)); - if(record == null) - { - throw new PackageNotInstalledException(String.format("Package name=>%s is not installed on your python environment, either install it ( better to install requirements.txt altogether) or turn on environment variable EXHORT_PYTHON_VIRTUAL_ENV=true to automatically installs it on virtual environment ( will slow down the analysis)",depName)); + if (record == null) { + throw new PackageNotInstalledException( + String.format( + "Package name=>%s is not installed on your python environment, either install it (" + + " better to install requirements.txt altogether) or turn on environment" + + " variable EXHORT_PYTHON_VIRTUAL_ENV=true to automatically installs it on" + + " virtual environment ( will slow down the analysis)", + depName)); } String depVersion = getDependencyVersion(record); List directDeps = getDepsList(record); getDependencyNameShow(record); - Map entry = new HashMap(); + Map entry = new HashMap(); dependencies.add(entry); entry.put("name", getDependencyNameShow(record)); entry.put("version", depVersion); List> targetDeps = new ArrayList<>(); - directDeps.stream().forEach( dep -> { - if(!path.contains(dep.toLowerCase())) { - List depList = new ArrayList(); - depList.add(dep.toLowerCase()); - - if (includeTransitive) { - bringAllDependencies(targetDeps, dep, cachedTree, includeTransitive, Stream.concat(path.stream(),depList.stream()).collect(Collectors.toList())); - } - } - Collections.sort(targetDeps, (o1, o2) -> { - String string1 = (String) (o1.get("name")); - String string2 = (String) (o2.get("name")); - return Arrays.compare(string1.toCharArray(),string2.toCharArray()); - }); - entry.put("dependencies",targetDeps); - }); - - + directDeps.stream() + .forEach( + dep -> { + if (!path.contains(dep.toLowerCase())) { + List depList = new ArrayList(); + depList.add(dep.toLowerCase()); + + if (includeTransitive) { + bringAllDependencies( + targetDeps, + dep, + cachedTree, + includeTransitive, + Stream.concat(path.stream(), depList.stream()).collect(Collectors.toList())); + } + } + Collections.sort( + targetDeps, + (o1, o2) -> { + String string1 = (String) (o1.get("name")); + String string2 = (String) (o2.get("name")); + return Arrays.compare(string1.toCharArray(), string2.toCharArray()); + }); + entry.put("dependencies", targetDeps); + }); } protected List getDepsList(String pipShowOutput) { @@ -374,54 +442,61 @@ protected List getDepsList(String pipShowOutput) { String requiresToken = pipShowOutput.substring(requiresKeyIndex + 9); int endOfLine = requiresToken.indexOf(System.lineSeparator()); String listOfDeps; - if(endOfLine > -1) { + if (endOfLine > -1) { listOfDeps = requiresToken.substring(0, endOfLine).trim(); - } - else { + } else { listOfDeps = requiresToken; } - return Arrays.stream(listOfDeps.split(",")).map(String::trim).filter(dep -> !dep.equals("")).collect(Collectors.toList()); + return Arrays.stream(listOfDeps.split(",")) + .map(String::trim) + .filter(dep -> !dep.equals("")) + .collect(Collectors.toList()); } protected String getDependencyVersion(String pipShowOutput) { int versionKeyIndex = pipShowOutput.indexOf("Version:"); String versionToken = pipShowOutput.substring(versionKeyIndex + 8); int endOfLine = versionToken.indexOf(System.lineSeparator()); - return versionToken.substring(0,endOfLine).trim(); + return versionToken.substring(0, endOfLine).trim(); } protected String getDependencyNameShow(String pipShowOutput) { int versionKeyIndex = pipShowOutput.indexOf("Name:"); String versionToken = pipShowOutput.substring(versionKeyIndex + 5); int endOfLine = versionToken.indexOf(System.lineSeparator()); - return versionToken.substring(0,endOfLine).trim(); + return versionToken.substring(0, endOfLine).trim(); } public static String getDependencyName(String dep) { int rightTriangleBracket = dep.indexOf(">"); int leftTriangleBracket = dep.indexOf("<"); - int equalsSign= dep.indexOf("="); - int minimumIndex = getFirstSign(rightTriangleBracket,leftTriangleBracket,equalsSign); + int equalsSign = dep.indexOf("="); + int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); String depName; - if(rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) - { + if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { depName = dep; - } - else { + } else { depName = dep.substring(0, minimumIndex); } return depName.trim(); } - private static int getFirstSign(int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { - rightTriangleBracket = rightTriangleBracket == -1 ? 999 : rightTriangleBracket; - leftTriangleBracket = leftTriangleBracket == -1 ? 999 : leftTriangleBracket; - equalsSign = equalsSign == -1 ? 999 : equalsSign; - return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket ? equalsSign : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket ? leftTriangleBracket : rightTriangleBracket); + private static int getFirstSign( + int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { + rightTriangleBracket = rightTriangleBracket == -1 ? 999 : rightTriangleBracket; + leftTriangleBracket = leftTriangleBracket == -1 ? 999 : leftTriangleBracket; + equalsSign = equalsSign == -1 ? 999 : equalsSign; + return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket + ? equalsSign + : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket + ? leftTriangleBracket + : rightTriangleBracket); } static List splitPipShowLines(String pipShowOutput) { - return Arrays.stream(pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())).collect(Collectors.toList()); + return Arrays.stream( + pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())) + .collect(Collectors.toList()); } } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java index 213abee4..ff693425 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java @@ -15,19 +15,14 @@ */ package com.redhat.exhort.utils; -import com.redhat.exhort.tools.Operations; - -import java.io.IOException; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -public class PythonControllerRealEnv extends PythonControllerBase{ - public PythonControllerRealEnv(String pathToPythonBin,String pathToPip) { +public class PythonControllerRealEnv extends PythonControllerBase { + public PythonControllerRealEnv(String pathToPythonBin, String pathToPip) { Path pipPath = Path.of(pathToPip); this.pipBinaryDir = pipPath.getParent(); - if(this.pipBinaryDir == null) - { + if (this.pipBinaryDir == null) { this.pipBinaryDir = pipPath; } this.pythonEnvironmentDir = Path.of(System.getProperty("user.dir")); @@ -35,18 +30,15 @@ public PythonControllerRealEnv(String pathToPythonBin,String pathToPip) { } @Override - public void prepareEnvironment(String pathToPythonBin) - { + public void prepareEnvironment(String pathToPythonBin) { String envBinDir = pipBinaryDir.toString(); - if(envBinDir.contains(FileSystems.getDefault().getSeparator())) { + if (envBinDir.contains(FileSystems.getDefault().getSeparator())) { if (pathToPythonBin.contains("python3")) { this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); } else { this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); } - } - else - { + } else { this.pipBinaryLocation = envBinDir; } } @@ -62,15 +54,13 @@ public boolean isRealEnv() { } @Override - public boolean isVirtualEnv() - { + public boolean isVirtualEnv() { return false; } @Override public void cleanEnvironment(boolean cleanEnvironment) { - //noop as this is real environment, + // noop as this is real environment, } - } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java index b062940d..740e4506 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java @@ -16,31 +16,32 @@ package com.redhat.exhort.utils; import com.redhat.exhort.tools.Operations; - import java.nio.file.Path; -public class PythonControllerTestEnv extends PythonControllerRealEnv{ -// private System.Logger log = System.getLogger("name"); - public PythonControllerTestEnv(String pathToPythonBin,String pathToPip) { - super(pathToPythonBin,pathToPip); +public class PythonControllerTestEnv extends PythonControllerRealEnv { + // private System.Logger log = System.getLogger("name"); + public PythonControllerTestEnv(String pathToPythonBin, String pathToPip) { + super(pathToPythonBin, pathToPip); } @Override - public void prepareEnvironment(String pathToPythonBin) - { + public void prepareEnvironment(String pathToPythonBin) { super.prepareEnvironment(pathToPythonBin); - String output = Operations.runProcessGetOutput(Path.of("."), new String[]{this.pathToPythonBin, "-m", "pip", "install", "--upgrade", "pip"}); -// log.log(System.Logger.Level.INFO,"Output from upgrading pip = " + System.lineSeparator() + output); + String output = + Operations.runProcessGetOutput( + Path.of("."), + new String[] {this.pathToPythonBin, "-m", "pip", "install", "--upgrade", "pip"}); + // log.log(System.Logger.Level.INFO,"Output from upgrading pip = " + System.lineSeparator() + + // output); } @Override public boolean automaticallyInstallPackageOnEnvironment() { return true; } + @Override - public boolean isVirtualEnv() - { + public boolean isVirtualEnv() { return false; } - } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java index 15429028..7c8db244 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java @@ -16,43 +16,42 @@ package com.redhat.exhort.utils; import com.redhat.exhort.tools.Operations; - import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; -public class PythonControllerVirtualEnv extends PythonControllerBase{ +public class PythonControllerVirtualEnv extends PythonControllerBase { -// private System.Logger log = System.getLogger("name"); + // private System.Logger log = System.getLogger("name"); public PythonControllerVirtualEnv(String pathToPythonBin) { - this.pipBinaryDir = Path.of(FileSystems.getDefault().getSeparator(), "tmp","exhort_env","bin"); - this.pythonEnvironmentDir = Path.of(FileSystems.getDefault().getSeparator(),"tmp","exhort_env"); + this.pipBinaryDir = + Path.of(FileSystems.getDefault().getSeparator(), "tmp", "exhort_env", "bin"); + this.pythonEnvironmentDir = + Path.of(FileSystems.getDefault().getSeparator(), "tmp", "exhort_env"); this.pathToPythonBin = pathToPythonBin; } @Override - public void prepareEnvironment(String pathToPythonBin) - { + public void prepareEnvironment(String pathToPythonBin) { try { - if(!Files.exists(pythonEnvironmentDir)) { + if (!Files.exists(pythonEnvironmentDir)) { Files.createDirectory(pythonEnvironmentDir); } } catch (IOException e) { throw new RuntimeException(e); } - String output = Operations.runProcessGetOutput(Path.of("."), new String[]{pathToPythonBin, "-m", "venv", pythonEnvironmentDir.toString()}); + String output = + Operations.runProcessGetOutput( + Path.of("."), + new String[] {pathToPythonBin, "-m", "venv", pythonEnvironmentDir.toString()}); String envBinDir = pipBinaryDir.toString(); - if(pathToPythonBin.contains("python3")) - { - this.pipBinaryLocation = Path.of(envBinDir,"pip3").toString(); - } - else - { - this.pipBinaryLocation = Path.of(envBinDir,"pip").toString(); + if (pathToPythonBin.contains("python3")) { + this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); + } else { + this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); } - } @Override @@ -66,39 +65,37 @@ public boolean isRealEnv() { } @Override - public boolean isVirtualEnv() - { + public boolean isVirtualEnv() { return true; } @Override - public void cleanEnvironment(boolean deleteEnvironment) - { - if(deleteEnvironment) - { + public void cleanEnvironment(boolean deleteEnvironment) { + if (deleteEnvironment) { try { - Files - .walk(pythonEnvironmentDir) - .sorted(Comparator.reverseOrder()) - .forEach( file -> { - try { - Files.delete(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + Files.walk(pythonEnvironmentDir) + .sorted(Comparator.reverseOrder()) + .forEach( + file -> { + try { + Files.delete(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } catch (IOException e) { throw new RuntimeException(e); } - } - else { + } else { Path envRequirements = Path.of(pythonEnvironmentDir.toString(), "requirements.txt"); try { Files.deleteIfExists(envRequirements); - String freezeOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); + String freezeOutput = + Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); Files.createFile(envRequirements); - Files.write(envRequirements,freezeOutput.getBytes()); - Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "uninstall","-y","-r","requirements.txt"); + Files.write(envRequirements, freezeOutput.getBytes()); + Operations.runProcessGetOutput( + pythonEnvironmentDir, pipBinaryLocation, "uninstall", "-y", "-r", "requirements.txt"); } catch (IOException e) { throw new RuntimeException(e); diff --git a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java index 01049ad9..867c0130 100644 --- a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java +++ b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java @@ -16,7 +16,6 @@ package com.redhat.exhort.vcs; import com.redhat.exhort.tools.Operations; - import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -26,123 +25,128 @@ public class GitVersionControlSystemImpl implements VersionControlSystem { private String gitBinary; - public GitVersionControlSystemImpl() - { + + public GitVersionControlSystemImpl() { gitBinary = Operations.getCustomPathOrElse("git"); } + @Override public TagInfo getLatestTag(Path repoLocation) { TagInfo tagInfo = new TagInfo(); - //get current commit hash digest - String commitHash = Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "HEAD").trim(); - if(Pattern.matches("^[a-f0-9]+",commitHash)) { + // get current commit hash digest + String commitHash = + Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "HEAD").trim(); + if (Pattern.matches("^[a-f0-9]+", commitHash)) { tagInfo.setCurrentCommitDigest(commitHash); - //get current commit timestamp. - String timeStampFromGit = Operations.runProcessGetOutput(repoLocation, gitBinary, "show", "HEAD", "--format=%cI", "--date", "local", "--quiet"); - LocalDateTime commitTimestamp = LocalDateTime.parse(timeStampFromGit.trim(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + // get current commit timestamp. + String timeStampFromGit = + Operations.runProcessGetOutput( + repoLocation, + gitBinary, + "show", + "HEAD", + "--format=%cI", + "--date", + "local", + "--quiet"); + LocalDateTime commitTimestamp = + LocalDateTime.parse(timeStampFromGit.trim(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); tagInfo.setCommitTimestamp(commitTimestamp); - // go get last annotated tag - String resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "describe", "--abbrev=12").trim(); + String resultFromInvocation = + Operations.runProcessGetOutput(repoLocation, gitBinary, "describe", "--abbrev=12").trim(); // if there are only unannotated tag, fetch last one. if (resultFromInvocation.contains("there were unannotated tags")) { - //fetch last unannotated tag - resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "describe", "--tags", "--abbrev=12").trim(); + // fetch last unannotated tag + resultFromInvocation = + Operations.runProcessGetOutput( + repoLocation, gitBinary, "describe", "--tags", "--abbrev=12") + .trim(); fetchLatestTag(tagInfo, resultFromInvocation); } else { if (resultFromInvocation.startsWith("fatal: No names found")) { tagInfo.setCurrentCommitPointedByTag(false); tagInfo.setTagName(""); } - //fetch last annotated tag + // fetch last annotated tag else { fetchLatestTag(tagInfo, resultFromInvocation); - } } } // empty git repo with no commits - else - { + else { tagInfo.setTagName(""); tagInfo.setCurrentCommitPointedByTag(false); - tagInfo.setCommitTimestamp(LocalDateTime.parse(LocalDateTime.MIN.toString(),DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + tagInfo.setCommitTimestamp( + LocalDateTime.parse(LocalDateTime.MIN.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)); tagInfo.setCurrentCommitDigest(""); } return tagInfo; } - @Override public boolean isDirectoryRepo(Path repoLocation) { - String resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "--is-inside-work-tree"); + String resultFromInvocation = + Operations.runProcessGetOutput( + repoLocation, gitBinary, "rev-parse", "--is-inside-work-tree"); return resultFromInvocation.trim().equals("true"); } @Override public String getNextTagVersion(TagInfo tagInfo) { - String result=""; - //if tag version ends with a digit, then increment it by one, and append to the end -0. - if(Pattern.matches(".*[0-9]$",tagInfo.getTagName())) - { - int length = tagInfo.getTagName().toCharArray().length; - Integer lastDigit= Integer.parseInt(tagInfo.getTagName().substring(length - 1, length)); - lastDigit++; - result = String.format("%s%s-0",tagInfo.getTagName().substring(0,length-1),lastDigit.toString()); - } - else - { - //if tag version ends with some suffix starting with '.' or '-', then just append to the end -0. - if(Pattern.matches(".*-[a-zA-Z0-9]+$|.*\\.[a-zA-Z0-9]+$",tagInfo.getTagName())) - { - result = String.format("%s-0",tagInfo.getTagName()); - } - - } - return result; + String result = ""; + // if tag version ends with a digit, then increment it by one, and append to the end -0. + if (Pattern.matches(".*[0-9]$", tagInfo.getTagName())) { + int length = tagInfo.getTagName().toCharArray().length; + Integer lastDigit = Integer.parseInt(tagInfo.getTagName().substring(length - 1, length)); + lastDigit++; + result = + String.format( + "%s%s-0", tagInfo.getTagName().substring(0, length - 1), lastDigit.toString()); + } else { + // if tag version ends with some suffix starting with '.' or '-', then just append to the end + // -0. + if (Pattern.matches(".*-[a-zA-Z0-9]+$|.*\\.[a-zA-Z0-9]+$", tagInfo.getTagName())) { + result = String.format("%s-0", tagInfo.getTagName()); + } + } + return result; } public String getPseudoVersion(TagInfo tagInfo, String newTagVersion) { - String stringTS = tagInfo.getCommitTimestamp().toString().replaceAll("[:-]|T",""); - String commitHash12 = tagInfo.getCurrentCommitDigest().substring(0,12); - return String.format("%s.%s-%s",newTagVersion,stringTS,commitHash12); - + String stringTS = tagInfo.getCommitTimestamp().toString().replaceAll("[:-]|T", ""); + String commitHash12 = tagInfo.getCurrentCommitDigest().substring(0, 12); + return String.format("%s.%s-%s", newTagVersion, stringTS, commitHash12); } + private static void fetchLatestTag(TagInfo tagInfo, String resultFromInvocation) { String[] parts = resultFromInvocation.split("-"); - if(parts.length > 1) - { + if (parts.length > 1) { analyzeGitDescribeResult(tagInfo, parts); - } - else - { + } else { tagInfo.setCurrentCommitPointedByTag(true); tagInfo.setTagName(parts[0]); } } private static void analyzeGitDescribeResult(TagInfo tagInfo, String[] parts) { - if(Pattern.matches("g[0-9a-f]{12}", parts[parts.length-1]) - && Pattern.matches("[1-9]*", parts[parts.length-2])) - { + if (Pattern.matches("g[0-9a-f]{12}", parts[parts.length - 1]) + && Pattern.matches("[1-9]*", parts[parts.length - 2])) { String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-" , tagNameParts)); - tagInfo.setCurrentCommitDigest(parts[parts.length-1].replace("g","")); + tagInfo.setTagName(String.join("-", tagNameParts)); + tagInfo.setCurrentCommitDigest(parts[parts.length - 1].replace("g", "")); tagInfo.setCurrentCommitPointedByTag(false); - } - - else - { + } else { String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-" , tagNameParts)); + tagInfo.setTagName(String.join("-", tagNameParts)); tagInfo.setCurrentCommitPointedByTag(true); } } - } diff --git a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java index 49cc3f55..1a09850d 100644 --- a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java +++ b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java @@ -19,33 +19,30 @@ public interface VersionControlSystem { /** - * This method gets the latest tag in a repo, information whether current commit pointed by - * a tag or not, and a short hash digest of the current commit + * This method gets the latest tag in a repo, information whether current commit pointed by a tag + * or not, and a short hash digest of the current commit + * * @param repoLocation - the repo directory path with inner .git directory * @return {@link TagInfo} containing the latest tag */ TagInfo getLatestTag(Path repoLocation); /** - * * @param repoLocation - the directory path to be checked whether it's a git repo or not * @return {boolean} - returns true if the directory is a repo. */ boolean isDirectoryRepo(Path repoLocation); /** - * * @param tagInfo - object that contains the tag info in order to calculate next version * @return A String containing the next version */ String getNextTagVersion(TagInfo tagInfo); /** - * * @param tagInfo - object that contains the tag info and current commit data * @param newTagVersion * @return */ - String getPseudoVersion(TagInfo tagInfo , String newTagVersion); - + String getPseudoVersion(TagInfo tagInfo, String newTagVersion); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1c7ffa7d..91a436a9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,5 @@ module com.redhat.exhort { requires java.net.http; - requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.core; requires transitive com.fasterxml.jackson.databind; @@ -11,8 +10,10 @@ requires transitive packageurl.java; requires org.tomlj; - opens com.redhat.exhort.api to com.fasterxml.jackson.databind; - opens com.redhat.exhort.providers to com.fasterxml.jackson.databind; + opens com.redhat.exhort.api to + com.fasterxml.jackson.databind; + opens com.redhat.exhort.providers to + com.fasterxml.jackson.databind; exports com.redhat.exhort; exports com.redhat.exhort.api; @@ -21,10 +22,16 @@ exports com.redhat.exhort.sbom; exports com.redhat.exhort.tools; - opens com.redhat.exhort.sbom to com.fasterxml.jackson.databind, packageurl.java; - opens com.redhat.exhort.api.serialization to com.fasterxml.jackson.databind; - exports com.redhat.exhort.providers; + opens com.redhat.exhort.sbom to + com.fasterxml.jackson.databind, + packageurl.java; + opens com.redhat.exhort.api.serialization to + com.fasterxml.jackson.databind; + + exports com.redhat.exhort.providers; exports com.redhat.exhort.logging; exports com.redhat.exhort.image; - opens com.redhat.exhort.image to com.fasterxml.jackson.databind; + + opens com.redhat.exhort.image to + com.fasterxml.jackson.databind; } diff --git a/src/test/java/com/redhat/exhort/ExhortTest.java b/src/test/java/com/redhat/exhort/ExhortTest.java index 74efe977..dca86b7c 100644 --- a/src/test/java/com/redhat/exhort/ExhortTest.java +++ b/src/test/java/com/redhat/exhort/ExhortTest.java @@ -36,8 +36,10 @@ protected String getStringFromFile(String... list) { return new String(bytes); } - public static InputStream getResourceAsStreamDecision(Class theClass, String[] list) throws IOException { - InputStream resourceAsStreamFromModule = theClass.getModule().getResourceAsStream(String.join("/", list)); + public static InputStream getResourceAsStreamDecision( + Class theClass, String[] list) throws IOException { + InputStream resourceAsStreamFromModule = + theClass.getModule().getResourceAsStream(String.join("/", list)); if (Objects.isNull(resourceAsStreamFromModule)) { return theClass.getClassLoader().getResourceAsStream(String.join("/", list)); } @@ -50,12 +52,11 @@ protected String getFileFromResource(String fileName, String... pathList) { var tmpDir = Files.createTempDirectory("exhort_test_"); tmpFile = Files.createFile(tmpDir.resolve(fileName)); try (var is = getResourceAsStreamDecision(this.getClass(), pathList)) { - if(Objects.nonNull(is)) { + if (Objects.nonNull(is)) { Files.write(tmpFile, is.readAllBytes()); - } - else - { - InputStream resourceIs = getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); + } else { + InputStream resourceIs = + getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); Files.write(tmpFile, resourceIs.readAllBytes()); resourceIs.close(); } @@ -67,17 +68,17 @@ protected String getFileFromResource(String fileName, String... pathList) { } return tmpFile.toString(); } -protected String getFileFromString(String fileName, String content) { + + protected String getFileFromString(String fileName, String content) { Path tmpFile; try { var tmpDir = Files.createTempDirectory("exhort_test_"); tmpFile = Files.createFile(tmpDir.resolve(fileName)); - Files.write(tmpFile, content.getBytes()); + Files.write(tmpFile, content.getBytes()); } catch (IOException e) { throw new RuntimeException(e); } return tmpFile.toString(); } - } diff --git a/src/test/java/com/redhat/exhort/image/ImageRefTest.java b/src/test/java/com/redhat/exhort/image/ImageRefTest.java index af4ba6be..ba670fa6 100644 --- a/src/test/java/com/redhat/exhort/image/ImageRefTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageRefTest.java @@ -15,41 +15,45 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class ImageRefTest extends ExhortTest { @Test void test_imageRef() throws MalformedPackageURLException { - var image = "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"; + var image = + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"; var platform = "linux/arm/v7"; var imageRef = new ImageRef(image, platform); assertEquals(new Image(image), imageRef.getImage()); assertEquals(new Platform(platform), imageRef.getPlatform()); - assertEquals("ImageRef{image='" + image + '\'' + ", platform='" + platform + '\'' + '}', imageRef.toString()); - - var purl = new PackageURL("pkg:oci/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?" + - "repository_url=test.io/test-namespace/test-repository&tag=test-tag&os=linux&arch=arm&variant=v7"); + assertEquals( + "ImageRef{image='" + image + '\'' + ", platform='" + platform + '\'' + '}', + imageRef.toString()); + + var purl = + new PackageURL( + "pkg:oci/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?" + + "repository_url=test.io/test-namespace/test-repository&tag=test-tag&os=linux&arch=arm&variant=v7"); assertEquals(purl, imageRef.getPackageURL()); var imageRefPurl = new ImageRef(purl); @@ -61,37 +65,48 @@ void test_imageRef() throws MalformedPackageURLException { @Test void test_check_image_digest() throws IOException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); var imageName = "test.io/test/test-app:test-version"; - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", imageName)}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", "inspect", "--raw", String.format("docker://%s", imageName) + }), + isNull())) + .thenReturn(output); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"docker", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("OSType: linux\nArchitecture: amd64", "", 0)); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"docker", "info"}), isNull())) + .thenReturn( + new Operations.ProcessExecOutput("OSType: linux\nArchitecture: amd64", "", 0)); var imageRef = new ImageRef(imageName, null); imageRef.checkImageDigest(); - var expectedImageRef = new ImageRef(imageName + - "@sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0", "linux/amd64"); + var expectedImageRef = + new ImageRef( + imageName + + "@sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0", + "linux/amd64"); assertEquals(expectedImageRef, imageRef); } - - } } diff --git a/src/test/java/com/redhat/exhort/image/ImageTest.java b/src/test/java/com/redhat/exhort/image/ImageTest.java index b1e537e7..c4733feb 100644 --- a/src/test/java/com/redhat/exhort/image/ImageTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageTest.java @@ -15,379 +15,370 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.stream.Stream; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.*; - class ImageTest { static Stream imageTestSources() { return Stream.of( - Arguments.of( - Named.of("full name, host port", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional-host:2500", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test-host:5000", - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional-host:2500/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("full name, registry", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-namespace/test-repository", - "optional.io/test-namespace/test-repository", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without namepsace", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-repository", - "test.io/test-repository", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace, digest", - "test-repository:test-tag"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "test-tag", - null, - "test-repository:test-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:test-tag", - "optional.io/test-repository:test-tag", - null, - "test-repository", - "optional.io/test-repository:test-tag", - "test-repository:test-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("without registry, namespace, tag", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - null, - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace, tag, digest", - "test-repository"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "latest", - null, - "test-repository:latest", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:latest", - "optional.io/test-repository:latest", - null, - "test-repository", - "optional.io/test-repository:latest", - "test-repository:latest@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("given tag, full name, host port", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional-host:2500", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test-host:5000", - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional-host:2500/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, full name, registry", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-namespace/test-repository", - "optional.io/test-namespace/test-repository", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without namepsace", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-repository", - "test.io/test-repository", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, digest", - "test-repository:test-tag"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - null, - "test-repository:alt-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag", - "optional.io/test-repository:alt-tag", - null, - "test-repository", - "optional.io/test-repository:alt-tag", - "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, tag", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, tag, digest", - "test-repository"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - null, - "test-repository:alt-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag", - "optional.io/test-repository:alt-tag", - null, - "test-repository", - "optional.io/test-repository:alt-tag", - "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ) - ); + Arguments.of( + Named.of( + "full name, host port", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional-host:2500", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test-host:5000", + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional-host:2500/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "full name, registry", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without registry", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-namespace/test-repository", + "optional.io/test-namespace/test-repository", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without namepsace", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-repository", + "test.io/test-repository", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without registry, namespace", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("without registry, namespace, digest", "test-repository:test-tag"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "test-tag", + null, + "test-repository:test-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:test-tag", + "optional.io/test-repository:test-tag", + null, + "test-repository", + "optional.io/test-repository:test-tag", + "test-repository:test-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "without registry, namespace, tag", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + null, + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("without registry, namespace, tag, digest", "test-repository"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "latest", + null, + "test-repository:latest", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:latest", + "optional.io/test-repository:latest", + null, + "test-repository", + "optional.io/test-repository:latest", + "test-repository:latest@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "given tag, full name, host port", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional-host:2500", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test-host:5000", + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional-host:2500/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, full name, registry", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without registry", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-namespace/test-repository", + "optional.io/test-namespace/test-repository", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without namepsace", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-repository", + "test.io/test-repository", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without registry, namespace", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("given tag, without registry, namespace, digest", "test-repository:test-tag"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + null, + "test-repository:alt-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag", + "optional.io/test-repository:alt-tag", + null, + "test-repository", + "optional.io/test-repository:alt-tag", + "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "given tag, without registry, namespace, tag", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("given tag, without registry, namespace, tag, digest", "test-repository"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + null, + "test-repository:alt-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag", + "optional.io/test-repository:alt-tag", + null, + "test-repository", + "optional.io/test-repository:alt-tag", + "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0")); } @ParameterizedTest(name = "{0}") @MethodSource("imageTestSources") - void test_image_no_tag(String fullName, - String givenTag, - String optionalRegistry, - String optionalDigest, - String expectedRegistry, - String expectedRepository, - String expectedTag, - String expectedDigest, - String expectedString, - boolean expectedHasRegistry, - String expectedNameWithoutTag, - String expectedNameWithoutTagOptionalRegistry, - String expectedFullName, - String expectedFullNameOptionalRegistry, - String expectedUser, - String expectedSimpleName, - String expectedNameWithOptionalRepository, - String expectedFullNameOptionalDigest) { + void test_image_no_tag( + String fullName, + String givenTag, + String optionalRegistry, + String optionalDigest, + String expectedRegistry, + String expectedRepository, + String expectedTag, + String expectedDigest, + String expectedString, + boolean expectedHasRegistry, + String expectedNameWithoutTag, + String expectedNameWithoutTagOptionalRegistry, + String expectedFullName, + String expectedFullNameOptionalRegistry, + String expectedUser, + String expectedSimpleName, + String expectedNameWithOptionalRepository, + String expectedFullNameOptionalDigest) { var image = givenTag == null ? new Image(fullName) : new Image(fullName, givenTag); @@ -403,7 +394,8 @@ void test_image_no_tag(String fullName, assertEquals(expectedFullNameOptionalRegistry, image.getFullName(optionalRegistry)); assertEquals(expectedUser, image.getUser()); assertEquals(expectedSimpleName, image.getSimpleName()); - assertEquals(expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); + assertEquals( + expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); assertEquals(expectedFullName, image.getNameWithOptionalRepository(null)); image.setDigest(optionalDigest); @@ -434,19 +426,28 @@ void test_hashCode() { void test_image_null() { var expectedMessage = "Image name must not be null"; - var exception1 = assertThrows(NullPointerException.class, () -> { - new Image(null); - }); + var exception1 = + assertThrows( + NullPointerException.class, + () -> { + new Image(null); + }); assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(NullPointerException.class, () -> { - new Image(null, "test"); - }); + var exception2 = + assertThrows( + NullPointerException.class, + () -> { + new Image(null, "test"); + }); assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(NullPointerException.class, () -> { - Image.validate(null); - }); + var exception3 = + assertThrows( + NullPointerException.class, + () -> { + Image.validate(null); + }); assertEquals(expectedMessage, exception3.getMessage()); } @@ -455,46 +456,73 @@ void test_image_invalid() { var imageName = ""; var expectedMessage = imageName + " is not a proper image name ([registry/][repo][:port]"; - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName); - }); + var exception1 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName); + }); assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "test"); - }); + var exception2 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName, "test"); + }); assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); + var exception3 = + assertThrows( + IllegalArgumentException.class, + () -> { + Image.validate(imageName); + }); assertEquals(expectedMessage, exception3.getMessage()); } @Test void test_all_invalid() { - var imageName = "%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF"; - var expectedMessage = "Given Docker name '%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' is invalid:\n" + - " * registry part '%&^.%*' doesn't match allowed pattern '^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*(?::[0-9]+)?$'\n" + - " * image part '&(&(&' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?'\n" + - " * user part '*(*&(' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?'\n" + - " * tag part '&^*&' doesn't match allowed pattern '^[\\w][\\w.-]{0,127}$'\n" + - " * digest part 'sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' doesn't match allowed pattern '^sha256:[a-z0-9]{32,}$'\n" + - "See http://bit.ly/docker_image_fmt for more details"; + var imageName = + "%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF"; + var expectedMessage = + "Given Docker name" + + " '%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF'" + + " is invalid:\n" + + " * registry part '%&^.%*' doesn't match allowed pattern" + + " '^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*(?::[0-9]+)?$'\n" + + " * image part '&(&(&' doesn't match allowed pattern" + + " '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?'\n" + + " * user part '*(*&(' doesn't match allowed pattern" + + " '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?'\n" + + " * tag part '&^*&' doesn't match allowed pattern '^[\\w][\\w.-]{0,127}$'\n" + + " * digest part" + + " 'sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' doesn't" + + " match allowed pattern '^sha256:[a-z0-9]{32,}$'\n" + + "See http://bit.ly/docker_image_fmt for more details"; - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName); - }); + var exception1 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName); + }); assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "&^*&"); - }); + var exception2 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName, "&^*&"); + }); assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); + var exception3 = + assertThrows( + IllegalArgumentException.class, + () -> { + Image.validate(imageName); + }); assertEquals(expectedMessage, exception3.getMessage()); } } diff --git a/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java b/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java index c2bf5a0c..708a8cdf 100644 --- a/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java @@ -15,6 +15,23 @@ */ package com.redhat.exhort.image; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_ARCH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_OS; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_PLATFORM; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_SERVICE_ENDPOINT; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_VARIANT; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SKOPEO_CONFIG_PATH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_CONFIG_PATH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_IMAGE_SOURCE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -22,16 +39,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junitpioneer.jupiter.ClearEnvironmentVariable; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -43,27 +50,20 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_ARCH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_OS; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_PLATFORM; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_SERVICE_ENDPOINT; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_VARIANT; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SKOPEO_CONFIG_PATH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_CONFIG_PATH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_IMAGE_SOURCE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class ImageUtilsTest extends ExhortTest { - static final String mockImageName = "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165"; + static final String mockImageName = + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165"; static final String mockImagePlatform = "linux/amd64"; static final ImageRef mockImageRef = new ImageRef(mockImageName, mockImagePlatform); static final String mockSyftPath = "test-path/syft"; @@ -81,96 +81,38 @@ class ImageUtilsTest extends ExhortTest { static Stream dockerArchSources() { return Stream.of( - Arguments.of( - Named.of("amd64", "amd64"), "amd64" - ), - Arguments.of( - Named.of("x86_64", "x86_64"), "amd64" - ), - Arguments.of( - Named.of("armv5tl", "armv5tl"), "arm" - ), - Arguments.of( - Named.of("armv5tel", "armv5tel"), "arm" - ), - Arguments.of( - Named.of("armv5tejl", "armv5tejl"), "arm" - ), - Arguments.of( - Named.of("armv6l", "armv6l"), "arm" - ), - Arguments.of( - Named.of("armv7l", "armv7l"), "arm" - ), - Arguments.of( - Named.of("armv7ml", "armv7ml"), "arm" - ), - Arguments.of( - Named.of("arm64", "arm64"), "arm64" - ), - Arguments.of( - Named.of("aarch64", "aarch64"), "arm64" - ), - Arguments.of( - Named.of("i386", "i386"), "386" - ), - Arguments.of( - Named.of("i486", "i486"), "386" - ), - Arguments.of( - Named.of("i586", "i586"), "386" - ), - Arguments.of( - Named.of("i686", "i686"), "386" - ), - Arguments.of( - Named.of("mips64le", "mips64le"), "mips64le" - ), - Arguments.of( - Named.of("ppc64le", "ppc64le"), "ppc64le" - ), - Arguments.of( - Named.of("riscv64", "riscv64"), "riscv64" - ), - Arguments.of( - Named.of("s390x", "s390x"), "s390x" - ), - Arguments.of( - Named.of("empty", ""), "" - ) - ); + Arguments.of(Named.of("amd64", "amd64"), "amd64"), + Arguments.of(Named.of("x86_64", "x86_64"), "amd64"), + Arguments.of(Named.of("armv5tl", "armv5tl"), "arm"), + Arguments.of(Named.of("armv5tel", "armv5tel"), "arm"), + Arguments.of(Named.of("armv5tejl", "armv5tejl"), "arm"), + Arguments.of(Named.of("armv6l", "armv6l"), "arm"), + Arguments.of(Named.of("armv7l", "armv7l"), "arm"), + Arguments.of(Named.of("armv7ml", "armv7ml"), "arm"), + Arguments.of(Named.of("arm64", "arm64"), "arm64"), + Arguments.of(Named.of("aarch64", "aarch64"), "arm64"), + Arguments.of(Named.of("i386", "i386"), "386"), + Arguments.of(Named.of("i486", "i486"), "386"), + Arguments.of(Named.of("i586", "i586"), "386"), + Arguments.of(Named.of("i686", "i686"), "386"), + Arguments.of(Named.of("mips64le", "mips64le"), "mips64le"), + Arguments.of(Named.of("ppc64le", "ppc64le"), "ppc64le"), + Arguments.of(Named.of("riscv64", "riscv64"), "riscv64"), + Arguments.of(Named.of("s390x", "s390x"), "s390x"), + Arguments.of(Named.of("empty", ""), "")); } static Stream dockerVariantSources() { return Stream.of( - Arguments.of( - Named.of("armv5tl", "armv5tl"), "v5" - ), - Arguments.of( - Named.of("armv5tel", "armv5tel"), "v5" - ), - Arguments.of( - Named.of("armv5tejl", "armv5tejl"), "v5" - ), - Arguments.of( - Named.of("armv6l", "armv6l"), "v6" - ), - Arguments.of( - Named.of("armv7l", "armv7l"), "v7" - ), - Arguments.of( - Named.of("armv7ml", "armv7ml"), "v7" - ), - Arguments.of( - Named.of("arm64", "arm64"), "v8" - ), - Arguments.of( - Named.of("aarch64", "aarch64"), "v8" - ), - Arguments.of( - Named.of("empty", ""), "" - ) - ); + Arguments.of(Named.of("armv5tl", "armv5tl"), "v5"), + Arguments.of(Named.of("armv5tel", "armv5tel"), "v5"), + Arguments.of(Named.of("armv5tejl", "armv5tejl"), "v5"), + Arguments.of(Named.of("armv6l", "armv6l"), "v6"), + Arguments.of(Named.of("armv7l", "armv7l"), "v7"), + Arguments.of(Named.of("armv7ml", "armv7ml"), "v7"), + Arguments.of(Named.of("arm64", "arm64"), "v8"), + Arguments.of(Named.of("aarch64", "aarch64"), "v8"), + Arguments.of(Named.of("empty", ""), "")); } @Test @@ -182,24 +124,40 @@ static Stream dockerVariantSources() { @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) void test_generate_image_sbom() throws IOException, MalformedPackageURLException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", mockImageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + mockImageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); var sbom = ImageUtils.generateImageSBOM(mockImageRef); var mapper = new ObjectMapper(); var node = mapper.readTree(json); - ((ObjectNode) node.get("metadata").get("component")).set("purl", new TextNode(mockImageRef.getPackageURL().canonicalize())); + ((ObjectNode) node.get("metadata").get("component")) + .set("purl", new TextNode(mockImageRef.getPackageURL().canonicalize())); assertEquals(node, sbom); } @@ -211,32 +169,60 @@ void test_generate_image_sbom() throws IOException, MalformedPackageURLException @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) void test_get_image_digests_single() throws IOException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var isRaw = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single_raw.json"}); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single.json"})) { - var jsonRaw = new BufferedReader(new InputStreamReader(isRaw, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var isRaw = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single_raw.json"}); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single.json"})) { + var jsonRaw = + new BufferedReader(new InputStreamReader(isRaw, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var outputRaw = new Operations.ProcessExecOutput(jsonRaw, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(outputRaw); - - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "--raw", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(outputRaw); + + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); var digests = ImageUtils.getImageDigests(mockImageRef); - var expectedDigests = Collections.singletonMap(Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + var expectedDigests = + Collections.singletonMap( + Platform.EMPTY_PLATFORM, + "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); assertEquals(expectedDigests, digests); } @@ -248,26 +234,46 @@ void test_get_image_digests_single() throws IOException { @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) void test_get_image_digests_multiple() throws IOException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "--raw", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); var digests = ImageUtils.getImageDigests(mockImageRef); var expectedDigests = new HashMap<>(); - expectedDigests.put(new Platform("linux", "amd64", null), "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); - expectedDigests.put(new Platform("linux", "arm64", null), "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); - expectedDigests.put(new Platform("linux", "ppc64le", null), "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); - expectedDigests.put(new Platform("linux", "s390x", null), "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); + expectedDigests.put( + new Platform("linux", "amd64", null), + "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); + expectedDigests.put( + new Platform("linux", "arm64", null), + "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); + expectedDigests.put( + new Platform("linux", "ppc64le", null), + "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); + expectedDigests.put( + new Platform("linux", "s390x", null), + "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); assertEquals(expectedDigests, digests); } @@ -303,13 +309,13 @@ void test_get_image_platform_no_default() { @ClearEnvironmentVariable(key = EXHORT_IMAGE_VARIANT) void test_get_image_platform_no_default_no_variant() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"podman", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("", "", 0)); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"podman", "info"}), isNull())) + .thenReturn(new Operations.ProcessExecOutput("", "", 0)); var platform = ImageUtils.getImagePlatform(); assertNull(platform); @@ -325,13 +331,14 @@ void test_get_image_platform_no_default_no_variant() { @ClearEnvironmentVariable(key = "PATH") void test_get_image_platform_no_defaults() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"podman", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("os: linux\narch: arm64\nvariant=v8", "", 0)); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"podman", "info"}), isNull())) + .thenReturn( + new Operations.ProcessExecOutput("os: linux\narch: arm64\nvariant=v8", "", 0)); var platform = ImageUtils.getImagePlatform(); assertEquals(new Platform("linux", "arm64", "v8"), platform); @@ -349,21 +356,35 @@ void test_exec_syft() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn(mockSyftPath); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSyftPath, mockImageRef.getImage().getFullName(), - "--from", mockSyftSource, "-c", mockSyftConfig, "-s", "all-layers", - "-o", "cyclonedx-json", "-q"}), - eq(new String[]{"PATH=" + "test-path/" + File.pathSeparator + "test-path/"}))) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn(mockSyftPath); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); + + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + mockSyftPath, + mockImageRef.getImage().getFullName(), + "--from", + mockSyftSource, + "-c", + mockSyftConfig, + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + eq( + new String[] { + "PATH=" + "test-path/" + File.pathSeparator + "test-path/" + }))) + .thenReturn(output); assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); } @@ -380,20 +401,28 @@ void test_exec_syft_no_config_no_source() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", mockImageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); + + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + mockImageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); } @@ -442,13 +471,13 @@ void test_host_info_docker() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("info0: test\n info: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.hostInfo("docker", "info")).isEqualTo("test-output"); } @@ -460,17 +489,20 @@ void test_host_info_no_docker_path() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"docker", "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"docker", "info"}), isNull())) + .thenReturn(output); - var exception = assertThrows(RuntimeException.class, () -> { - ImageUtils.hostInfo("docker", "info"); - }); + var exception = + assertThrows( + RuntimeException.class, + () -> { + ImageUtils.hostInfo("docker", "info"); + }); assertEquals("test-error", exception.getMessage()); } } @@ -482,13 +514,13 @@ void test_docker_get_os() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("OSType: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.dockerGetOs()).isEqualTo("test-output"); } @@ -502,13 +534,13 @@ void test_docker_get_arch(String sysArch, String arch) { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.dockerGetArch()).isEqualTo(arch); } @@ -522,13 +554,13 @@ void test_docker_get_variant(String sysArch, String variant) { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.dockerGetVariant()).isEqualTo(variant); } @@ -541,13 +573,13 @@ void test_host_info_podman() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("info: test-output\nabcdesss", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.hostInfo("podman", "info")).isEqualTo("test-output"); } @@ -560,13 +592,13 @@ void test_podman_get_os() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("os: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.podmanGetOs()).isEqualTo("test-output"); } @@ -579,13 +611,13 @@ void test_podman_get_arch() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("arch: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.podmanGetArch()).isEqualTo("test-output"); } @@ -598,13 +630,13 @@ void test_podman_get_variant() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("variant: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); assertThat(ImageUtils.podmanGetVariant()).isEqualTo("test-output"); } @@ -627,15 +659,25 @@ void test_exec_skopeo_inspect_raw() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", "--authfile", mockSkopeoConfig, - "--daemon-host", mockSkopeoDaemon, "--raw", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + mockSkopeoPath, + "inspect", + "--authfile", + mockSkopeoConfig, + "--daemon-host", + mockSkopeoDaemon, + "--raw", + String.format("docker-daemon:%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } @@ -649,15 +691,23 @@ void test_exec_skopeo_inspect_raw_no_config() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", - "--daemon-host", mockSkopeoDaemon, "--raw", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + mockSkopeoPath, + "inspect", + "--daemon-host", + mockSkopeoDaemon, + "--raw", + String.format("docker-daemon:%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } @@ -671,14 +721,23 @@ void test_exec_skopeo_inspect_raw_no_daemon() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", "--authfile", mockSkopeoConfig, - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "--authfile", + mockSkopeoConfig, + "--raw", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } @@ -692,14 +751,21 @@ void test_exec_skopeo_inspect_raw_no_config_no_daemon() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "--raw", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } @@ -713,15 +779,25 @@ void test_exec_skopeo_inspect() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", "--authfile", mockSkopeoConfig, - "--daemon-host", mockSkopeoDaemon, "", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + mockSkopeoPath, + "inspect", + "--authfile", + mockSkopeoConfig, + "--daemon-host", + mockSkopeoDaemon, + "", + String.format("docker-daemon:%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } @@ -735,15 +811,23 @@ void test_exec_skopeo_inspect_no_config() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", - "--daemon-host", mockSkopeoDaemon, "", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + mockSkopeoPath, + "inspect", + "--daemon-host", + mockSkopeoDaemon, + "", + String.format("docker-daemon:%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } @@ -757,14 +841,23 @@ void test_exec_skopeo_inspect_no_daemon() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", "--authfile", mockSkopeoConfig, - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "--authfile", + mockSkopeoConfig, + "", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } @@ -778,14 +871,21 @@ void test_exec_skopeo_inspect_no_config_no_daemon() { try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } @@ -793,16 +893,26 @@ void test_exec_skopeo_inspect_no_config_no_daemon() { @Test void test_get_multi_image_digests() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { var mapper = new ObjectMapper(); var node = mapper.readTree(is); var digests = ImageUtils.getMultiImageDigests(node); Map expectedDigests = new HashMap<>(); - expectedDigests.put(new Platform("linux", "amd64", null), "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); - expectedDigests.put(new Platform("linux", "arm64", null), "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); - expectedDigests.put(new Platform("linux", "ppc64le", null), "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); - expectedDigests.put(new Platform("linux", "s390x", null), "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); + expectedDigests.put( + new Platform("linux", "amd64", null), + "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); + expectedDigests.put( + new Platform("linux", "arm64", null), + "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); + expectedDigests.put( + new Platform("linux", "ppc64le", null), + "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); + expectedDigests.put( + new Platform("linux", "s390x", null), + "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); assertEquals(expectedDigests, digests); } @@ -820,7 +930,9 @@ void test_get_multi_image_digests_empty() { @Test void test_filter_mediaType() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { var mapper = new ObjectMapper(); var node = mapper.readTree(is); @@ -830,7 +942,9 @@ void test_filter_mediaType() throws IOException { @Test void test_filter_digest() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { var mapper = new ObjectMapper(); var node = mapper.readTree(is); @@ -840,7 +954,9 @@ void test_filter_digest() throws IOException { @Test void test_filter_platform() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { var mapper = new ObjectMapper(); var node = mapper.readTree(is); @@ -851,21 +967,36 @@ void test_filter_platform() throws IOException { @Test void test_get_single_image_digest() throws IOException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single.json"})) { + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); var digests = ImageUtils.getSingleImageDigest(mockImageRef); - Map expectedDigests = Collections.singletonMap(Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + Map expectedDigests = + Collections.singletonMap( + Platform.EMPTY_PLATFORM, + "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); assertEquals(expectedDigests, digests); } @@ -878,14 +1009,21 @@ void test_get_single_image_digest_empty() throws JsonProcessingException { var node = new TextNode("root"); var output = new Operations.ProcessExecOutput(mapper.writeValueAsString(node), "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "skopeo", + "inspect", + "", + String.format("docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); var digests = ImageUtils.getSingleImageDigest(mockImageRef); Map expectedDigests = Collections.emptyMap(); diff --git a/src/test/java/com/redhat/exhort/image/PlatformTest.java b/src/test/java/com/redhat/exhort/image/PlatformTest.java index e21667f9..16803b3b 100644 --- a/src/test/java/com/redhat/exhort/image/PlatformTest.java +++ b/src/test/java/com/redhat/exhort/image/PlatformTest.java @@ -15,156 +15,84 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.stream.Stream; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.*; - class PlatformTest { static Stream PlatformSources() { return Stream.of( - Arguments.of( - Named.of( - "amd64", "amd64" - ), - "linux", - "amd64", - null, - "linux/amd64", - false - ), - Arguments.of( - Named.of( - "linux/amd64", "linux/amd64" - ), - "linux", - "amd64", - null, - "linux/amd64", - false - ), - Arguments.of( - Named.of( - "linux/arm/v5", "linux/arm/v5" - ), - "linux", - "arm", - "v5", - "linux/arm/v5", - true - ), - Arguments.of( - Named.of( - "linux/arm/v6", "linux/arm/v6" - ), - "linux", - "arm", - "v6", - "linux/arm/v6", - true - ), - Arguments.of( - Named.of( - "linux/arm/v7", "linux/arm/v7" - ), - "linux", - "arm", - "v7", - "linux/arm/v7", - true - ), - Arguments.of( - Named.of( - "linux/arm64", "linux/arm64" - ), - "linux", - "arm64", - "v8", - "linux/arm64/v8", - false - ), - Arguments.of( - Named.of( - "linux/arm64/v8", "linux/arm64/v8" - ), - "linux", - "arm64", - "v8", - "linux/arm64/v8", - false - ), - Arguments.of( - Named.of( - "linux/386", "linux/386" - ), - "linux", - "386", - null, - "linux/386", - false - ), - Arguments.of( - Named.of( - "linux/mips64le", "linux/mips64le" - ), - "linux", - "mips64le", - null, - "linux/mips64le", - false - ), - Arguments.of( - Named.of( - "linux/ppc64le", "linux/ppc64le" - ), - "linux", - "ppc64le", - null, - "linux/ppc64le", - false - ), - Arguments.of( - Named.of( - "linux/riscv64", "linux/riscv64" - ), - "linux", - "riscv64", - null, - "linux/riscv64", - false - ), - Arguments.of( - Named.of( - "linux/s390x", "linux/s390x" - ), - "linux", - "s390x", - null, - "linux/s390x", - false - ), - Arguments.of( - Named.of( - "windows/arm64", "windows/arm64" - ), - "windows", - "arm64", - null, - "windows/arm64", - false - ) - ); + Arguments.of(Named.of("amd64", "amd64"), "linux", "amd64", null, "linux/amd64", false), + Arguments.of( + Named.of("linux/amd64", "linux/amd64"), "linux", "amd64", null, "linux/amd64", false), + Arguments.of( + Named.of("linux/arm/v5", "linux/arm/v5"), "linux", "arm", "v5", "linux/arm/v5", true), + Arguments.of( + Named.of("linux/arm/v6", "linux/arm/v6"), "linux", "arm", "v6", "linux/arm/v6", true), + Arguments.of( + Named.of("linux/arm/v7", "linux/arm/v7"), "linux", "arm", "v7", "linux/arm/v7", true), + Arguments.of( + Named.of("linux/arm64", "linux/arm64"), + "linux", + "arm64", + "v8", + "linux/arm64/v8", + false), + Arguments.of( + Named.of("linux/arm64/v8", "linux/arm64/v8"), + "linux", + "arm64", + "v8", + "linux/arm64/v8", + false), + Arguments.of(Named.of("linux/386", "linux/386"), "linux", "386", null, "linux/386", false), + Arguments.of( + Named.of("linux/mips64le", "linux/mips64le"), + "linux", + "mips64le", + null, + "linux/mips64le", + false), + Arguments.of( + Named.of("linux/ppc64le", "linux/ppc64le"), + "linux", + "ppc64le", + null, + "linux/ppc64le", + false), + Arguments.of( + Named.of("linux/riscv64", "linux/riscv64"), + "linux", + "riscv64", + null, + "linux/riscv64", + false), + Arguments.of( + Named.of("linux/s390x", "linux/s390x"), "linux", "s390x", null, "linux/s390x", false), + Arguments.of( + Named.of("windows/arm64", "windows/arm64"), + "windows", + "arm64", + null, + "windows/arm64", + false)); } @ParameterizedTest(name = "{0}") @MethodSource("PlatformSources") - void test_platform(String platform, String os, String arch, String variant, String platformStr, boolean variantRequired) { + void test_platform( + String platform, + String os, + String arch, + String variant, + String platformStr, + boolean variantRequired) { var p = new Platform(platform); assertEquals(os, p.getOs()); @@ -180,34 +108,52 @@ void test_platform(String platform, String os, String arch, String variant, Stri @Test void test_platform_invalid() { - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Platform(null); - }); + var exception1 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform(null); + }); assertEquals("Invalid platform: null", exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux/arm/v8/a"); - }); + var exception2 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform("linux/arm/v8/a"); + }); assertEquals("Invalid platform: linux/arm/v8/a", exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux/abc"); - }); + var exception3 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform("linux/abc"); + }); assertEquals("Image platform is not supported: linux/abc", exception3.getMessage()); - var exception4 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("", null, ""); - }); + var exception4 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform("", null, ""); + }); assertEquals("Invalid platform arch: null", exception4.getMessage()); - var exception5 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux", "arm", "v8"); - }); + var exception5 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform("linux", "arm", "v8"); + }); assertEquals("Image platform is not supported: linux/arm/v8", exception5.getMessage()); - var exception6 = assertThrows(IllegalArgumentException.class, () -> { - new Platform(null, "arm", null); - }); + var exception6 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Platform(null, "arm", null); + }); assertEquals("Image platform is not supported: null/arm/null", exception6.getMessage()); } } diff --git a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java index 149dd225..c35a9644 100644 --- a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java +++ b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java @@ -15,6 +15,13 @@ */ package com.redhat.exhort.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mockStatic; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,18 +33,6 @@ import com.redhat.exhort.providers.HelperExtension; import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.jupiter.MockitoExtension; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -54,13 +49,17 @@ import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mockStatic; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; @Tag("IntegrationTest") @ExtendWith(HelperExtension.class) @@ -68,16 +67,27 @@ class ExhortApiIT extends ExhortTest { private static Api api; - private static Map ecoSystemsManifestNames; + private static Map ecoSystemsManifestNames; private MockedStatic mockedOperations; + @BeforeAll static void beforeAll() { api = new ExhortApi(); - System.setProperty("RHDA_SOURCE","exhort-java-api-it"); - System.setProperty("EXHORT_DEV_MODE","false"); - ecoSystemsManifestNames = Map.of("golang", "go.mod","maven","pom.xml","npm","package.json","pypi","requirements.txt", "gradle", "build.gradle"); - + System.setProperty("RHDA_SOURCE", "exhort-java-api-it"); + System.setProperty("EXHORT_DEV_MODE", "false"); + ecoSystemsManifestNames = + Map.of( + "golang", + "go.mod", + "maven", + "pom.xml", + "npm", + "package.json", + "pypi", + "requirements.txt", + "gradle", + "build.gradle"); } @Tag("IntegrationTest") @@ -87,53 +97,74 @@ static void afterAll() { System.clearProperty("EXHORT_DEV_MODE"); api = null; } + @Tag("IntegrationTest") @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + String pathToManifest = + getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. + // Github action runner with all maven and java versions seems to enter infinite loop in + // integration tests of + // MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. // locally it's not recreated with same versions mockMavenDependencyTree(packageManager); AnalysisReport analysisReportResult = api.stackAnalysis(pathToManifest).get(); - handleJsonResponse(analysisReportResult,true); + handleJsonResponse(analysisReportResult, true); releaseStaticMock(packageManager); } private void releaseStaticMock(Ecosystem.Type packageManager) { - if(packageManager.equals(Ecosystem.Type.MAVEN)) { + if (packageManager.equals(Ecosystem.Type.MAVEN)) { this.mockedOperations.close(); } } - @Tag("IntegrationTest") @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis_Mixed(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis_Mixed(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + String pathToManifest = + getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. + // Github action runner with all maven and java versions seems to enter infinite loop in + // integration tests of + // MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. // locally it's not recreated with same versions mockMavenDependencyTree(packageManager); AnalysisReport analysisReportJson = api.stackAnalysisMixed(pathToManifest).get().json; String analysisReportHtml = new String(api.stackAnalysisMixed(pathToManifest).get().html); - handleJsonResponse(analysisReportJson,true); + handleJsonResponse(analysisReportJson, true); handleHtmlResponse(analysisReportHtml); releaseStaticMock(packageManager); } @Tag("IntegrationTest") @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis_Html(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis_Html(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + String pathToManifest = + getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when running dependency maven plugin to produce verbose text dependenct tree format. + // Github action runner with all maven and java versions seems to enter infinite loop in + // integration tests of + // MAVEN when running dependency maven plugin to produce verbose text dependenct tree format. // locally it's not recreated with same versions mockMavenDependencyTree(packageManager); String analysisReportHtml = new String(api.stackAnalysisHtml(pathToManifest).get()); @@ -141,28 +172,35 @@ void Integration_Test_End_To_End_Stack_Analysis_Html(Ecosystem.Type packageManag handleHtmlResponse(analysisReportHtml); } - @Tag("IntegrationTest") @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON" }) - void Integration_Test_End_To_End_Component_Analysis(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON"}) + void Integration_Test_End_To_End_Component_Analysis(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - byte[] manifestContent = getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName).getBytes(); + byte[] manifestContent = + getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName) + .getBytes(); preparePythonEnvironment(packageManager); - AnalysisReport analysisReportResult = api.componentAnalysis(manifestFileName,manifestContent).get(); - handleJsonResponse(analysisReportResult,false); + AnalysisReport analysisReportResult = + api.componentAnalysis(manifestFileName, manifestContent).get(); + handleJsonResponse(analysisReportResult, false); } @Tag("IntegrationTest") @Test void Integration_Test_End_To_End_Image_Analysis() throws IOException { - var result = testImageAnalysis(i -> { - try { - return api.imageAnalysis(i).get(); - } catch (InterruptedException | ExecutionException | IOException e) { - throw new RuntimeException(e); - } - }); + var result = + testImageAnalysis( + i -> { + try { + return api.imageAnalysis(i).get(); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new RuntimeException(e); + } + }); assertEquals(1, result.size()); handleJsonResponse(new ArrayList<>(result.values()).get(0), false); @@ -171,69 +209,92 @@ void Integration_Test_End_To_End_Image_Analysis() throws IOException { @Tag("IntegrationTest") @Test void Integration_Test_End_To_End_Image_Analysis_Html() throws IOException { - var result = testImageAnalysis(i -> { - try { - return api.imageAnalysisHtml(i).get(); - } catch (InterruptedException | ExecutionException | IOException e) { - throw new RuntimeException(e); - } - }); + var result = + testImageAnalysis( + i -> { + try { + return api.imageAnalysisHtml(i).get(); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new RuntimeException(e); + } + }); handleHtmlResponseForImage(new String(result)); } - private static T testImageAnalysis(Function, T> imageAnalysisFunction) throws IOException { + private static T testImageAnalysis(Function, T> imageAnalysisFunction) + throws IOException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(ExhortApiIT.class, new String[]{"msc", "image", "image_sbom.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var sbomIS = + getResourceAsStreamDecision( + ExhortApiIT.class, new String[] {"msc", "image", "image_sbom.json"})) { + + var imageRef = + new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = + new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); return imageAnalysisFunction.apply(Set.of(imageRef)); } } private static void preparePythonEnvironment(Ecosystem.Type packageManager) { - if(packageManager.equals(Ecosystem.Type.PYTHON)) { - System.setProperty("EXHORT_PYTHON_VIRTUAL_ENV","true"); - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - } - else { + if (packageManager.equals(Ecosystem.Type.PYTHON)) { + System.setProperty("EXHORT_PYTHON_VIRTUAL_ENV", "true"); + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + } else { System.clearProperty("EXHORT_PYTHON_VIRTUAL_ENV"); System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); System.clearProperty("MATCH_MANIFEST_VERSIONS"); } } - private static void handleJsonResponse(AnalysisReport analysisReportResult, boolean positiveNumberOfTransitives) { - analysisReportResult.getProviders().entrySet().stream().forEach(provider -> { assertTrue(provider.getValue().getStatus().getOk()); - assertTrue(provider.getValue().getStatus().getCode() == HttpURLConnection.HTTP_OK); - }); + private static void handleJsonResponse( + AnalysisReport analysisReportResult, boolean positiveNumberOfTransitives) { analysisReportResult.getProviders().entrySet().stream() - .map(Map.Entry::getValue) - .map(ProviderReport::getSources) - .map(Map::entrySet) - .flatMap(Collection::stream) - .map(Map.Entry::getValue) - .forEach( source -> assertTrue(source.getSummary().getTotal() > 0 )); - - if(positiveNumberOfTransitives) { + .forEach( + provider -> { + assertTrue(provider.getValue().getStatus().getOk()); + assertTrue(provider.getValue().getStatus().getCode() == HttpURLConnection.HTTP_OK); + }); + analysisReportResult.getProviders().entrySet().stream() + .map(Map.Entry::getValue) + .map(ProviderReport::getSources) + .map(Map::entrySet) + .flatMap(Collection::stream) + .map(Map.Entry::getValue) + .forEach(source -> assertTrue(source.getSummary().getTotal() > 0)); + + if (positiveNumberOfTransitives) { assertTrue(analysisReportResult.getScanned().getTransitive() > 0); - } - else { - assertEquals(0,analysisReportResult.getScanned().getTransitive()); + } else { + assertEquals(0, analysisReportResult.getScanned().getTransitive()); } } @@ -242,10 +303,10 @@ private void handleHtmlResponse(String analysisReportHtml) throws JsonProcessing assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); int jsonStart = analysisReportHtml.indexOf("\"report\":"); int jsonEnd = analysisReportHtml.indexOf("}}}}}"); - if(jsonEnd == -1) { + if (jsonEnd == -1) { jsonEnd = analysisReportHtml.indexOf("}}}}"); } - String embeddedJson = analysisReportHtml.substring(jsonStart + 9 ,jsonEnd + 5); + String embeddedJson = analysisReportHtml.substring(jsonStart + 9, jsonEnd + 5); JsonNode jsonInHtml = om.readTree(embeddedJson); JsonNode scannedNode = jsonInHtml.get("scanned"); assertTrue(scannedNode.get("total").asInt(0) > 0); @@ -253,15 +314,15 @@ private void handleHtmlResponse(String analysisReportHtml) throws JsonProcessing JsonNode status = jsonInHtml.get("providers").get("osv-nvd").get("status"); assertTrue(status.get("code").asInt(0) == 200); assertTrue(status.get("ok").asBoolean(false)); - } - private void handleHtmlResponseForImage(String analysisReportHtml) throws JsonProcessingException { + private void handleHtmlResponseForImage(String analysisReportHtml) + throws JsonProcessingException { ObjectMapper om = new ObjectMapper(); assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); int jsonStart = analysisReportHtml.indexOf("\"report\":"); int jsonEnd = analysisReportHtml.indexOf("}}}}}}"); - String embeddedJson = analysisReportHtml.substring(jsonStart + 9 ,jsonEnd + 6); + String embeddedJson = analysisReportHtml.substring(jsonStart + 9, jsonEnd + 6); JsonNode jsonInHtml = om.readTree(embeddedJson); JsonNode scannedNode = jsonInHtml.findValue("scanned"); assertTrue(scannedNode.get("total").asInt(0) > 0); @@ -272,30 +333,38 @@ private void handleHtmlResponseForImage(String analysisReportHtml) throws JsonPr } private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOException { - if(packageManager.equals(Ecosystem.Type.MAVEN)) { + if (packageManager.equals(Ecosystem.Type.MAVEN)) { mockedOperations = mockStatic(Operations.class); String depTree; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "it","maven", "depTree.txt"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "it", "maven", "depTree.txt"})) { depTree = new String(is.readAllBytes()); } - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile");}); + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock( + depTree, invocationOnMock, "-DoutputFile"); + }); } } - public static String getOutputFileAndOverwriteItWithMock(String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) throws IOException { + public static String getOutputFileAndOverwriteItWithMock( + String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) + throws IOException { String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; - Optional outputFileArg = Arrays.stream(rawArguments).filter(arg -> arg!= null && arg.startsWith(parameterPrefix)).findFirst(); - String outputFilePath=null; - if(outputFileArg.isPresent()) - { + Optional outputFileArg = + Arrays.stream(rawArguments) + .filter(arg -> arg != null && arg.startsWith(parameterPrefix)) + .findFirst(); + String outputFilePath = null; + if (outputFileArg.isPresent()) { String outputFile = outputFileArg.get(); outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); Files.writeString(Path.of(outputFilePath), outputFileContent); } return outputFilePath; } - } - - - diff --git a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java index 79c3059c..35bd45e6 100644 --- a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java +++ b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java @@ -30,6 +30,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.Provider; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; +import com.redhat.exhort.tools.Ecosystem; +import com.redhat.exhort.tools.Operations; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -46,7 +60,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -59,35 +72,19 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.github.packageurl.MalformedPackageURLException; -import com.github.packageurl.PackageURL; -import com.redhat.exhort.Api; -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.Provider; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; -import com.redhat.exhort.tools.Ecosystem; -import com.redhat.exhort.tools.Operations; - @ExtendWith(MockitoExtension.class) -@ClearEnvironmentVariable(key="EXHORT_SNYK_TOKEN") -@ClearEnvironmentVariable(key="EXHORT_DEV_MODE") -@ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") -@ClearEnvironmentVariable(key="RHDA_TOKEN") -@ClearEnvironmentVariable(key="RHDA_SOURCE") +@ClearEnvironmentVariable(key = "EXHORT_SNYK_TOKEN") +@ClearEnvironmentVariable(key = "EXHORT_DEV_MODE") +@ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") +@ClearEnvironmentVariable(key = "RHDA_TOKEN") +@ClearEnvironmentVariable(key = "RHDA_SOURCE") @SuppressWarnings("unchecked") class Exhort_Api_Test extends ExhortTest { - @Mock - Provider mockProvider; - @Mock - HttpClient mockHttpClient; - @InjectMocks - ExhortApi exhortApiSut; + @Mock Provider mockProvider; + + @Mock HttpClient mockHttpClient; + + @InjectMocks ExhortApi exhortApiSut; @AfterEach void cleanup() { @@ -95,36 +92,41 @@ void cleanup() { } @Test - @SetEnvironmentVariable(key="EXHORT_SNYK_TOKEN", value="snyk-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_TOKEN", value="rhda-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_SOURCE", value="rhda-source-from-env-var") + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") void stackAnalysisHtml_with_pom_xml_should_return_html_report_from_the_backend() throws IOException, ExecutionException, InterruptedException { // create a temporary pom.xml file var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { Files.write(tmpFile, is.readAllBytes()); } // stub the mocked provider with a fake content object given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); // create an argument matcher to make sure we mock the response to for right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("text/html") && - // snyk token is set using the environment variable (annotation) - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") && - - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("text/html") + && + // snyk token is set using the environment variable (annotation) + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") + && r.method().equals("POST"); // load dummy html and set as the expected analysis byte[] expectedHtml; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.html"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.html"})) { expectedHtml = is.readAllBytes(); } @@ -134,13 +136,13 @@ void stackAnalysisHtml_with_pom_xml_should_return_html_report_from_the_backend() given(mockHttpResponse.statusCode()).willReturn(200); // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { + try (var ecosystemTool = mockStatic(Ecosystem.class)) { // stub static getProvider utility function to return our mock provider ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a html stack analysis report var htmlTxt = exhortApiSut.stackAnalysisHtml(tmpFile.toString()); @@ -152,41 +154,49 @@ void stackAnalysisHtml_with_pom_xml_should_return_html_report_from_the_backend() } @Test -// System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); -// System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); - @SetEnvironmentVariable(key="EXHORT_SNYK_TOKEN", value="snyk-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_TOKEN", value="rhda-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_SOURCE", value="rhda-source-from-env-var") + // System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); + // System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { + throws IOException, ExecutionException, InterruptedException { // create a temporary pom.xml file var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { Files.write(tmpFile, is.readAllBytes()); } // stub the mocked provider with a fake content object given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); // we expect this to be ignored because tokens from env vars takes precedence System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using the environment variable (annotation) - ignored the one set in properties - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using the environment variable (annotation) - ignored the one + // set in + // properties + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") + && r.method().equals("POST"); // load dummy json and set as the expected analysis var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); AnalysisReport expectedAnalysis; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { expectedAnalysis = mapper.readValue(is, AnalysisReport.class); } @@ -196,13 +206,13 @@ void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() given(mockHttpResponse.statusCode()).willReturn(200); // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { + try (var ecosystemTool = mockStatic(Ecosystem.class)) { // stub static getProvider utility function to return our mock provider ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a json stack analysis report var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString()); @@ -215,16 +225,18 @@ void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() @Test void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { + throws IOException, ExecutionException, InterruptedException { // load pom.xml byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { targetPom = is.readAllBytes(); } // stub the mocked provider with a fake content object given(mockProvider.provideComponent(targetPom)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); // we expect this to picked up because no env var to take precedence System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); @@ -232,20 +244,25 @@ void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using properties which is picked up because no env var specified - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-property") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-property") && - r.headers().firstValue("rhda-operation-type").get().equals("Component Analysis") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using properties which is picked up because no env var + // specified + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-property") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-property") + && r.headers().firstValue("rhda-operation-type").get().equals("Component Analysis") + && r.method().equals("POST"); // load dummy json and set as the expected analysis var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); AnalysisReport expectedReport; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { expectedReport = mapper.readValue(is, AnalysisReport.class); } @@ -261,7 +278,7 @@ void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a json stack analysis report var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); @@ -271,40 +288,50 @@ void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() } @Test - void stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { + void + stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { // load dummy json and set as the expected analysis var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); AnalysisReport expectedJson; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { expectedJson = mapper.readValue(is, AnalysisReport.class); } // load dummy html and set as the expected analysis byte[] expectedHtml; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.html"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.html"})) { expectedHtml = is.readAllBytes(); } // create a temporary pom.xml file var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { Files.write(tmpFile, is.readAllBytes()); } // stub the mocked provider with a fake content object given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("multipart/mixed") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("multipart/mixed") + && r.method().equals("POST"); // load dummy mixed and set as the expected analysis byte[] mixedResponse; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.mixed"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.mixed"})) { mixedResponse = is.readAllBytes(); } @@ -314,13 +341,13 @@ void stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_objec given(mockHttpResponse.statusCode()).willReturn(200); // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { + try (var ecosystemTool = mockStatic(Ecosystem.class)) { // stub static getProvider utility function to return our mock provider ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a json stack analysis mixed report var responseAnalysis = exhortApiSut.stackAnalysisMixed(tmpFile.toString()).get(); @@ -334,32 +361,39 @@ void stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_objec @Test void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { + throws IOException, ExecutionException, InterruptedException { // load pom.xml var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { Files.write(tmpFile, is.readAllBytes()); } // stub the mocked provider with a fake content object given(mockProvider.provideComponent(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); // we expect this to picked up because no env var to take precedence System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using properties which is picked up because no env var specified - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using properties which is picked up because no env var + // specified + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") + && r.method().equals("POST"); // load dummy json and set as the expected analysis var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); AnalysisReport expectedReport; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { expectedReport = mapper.readValue(is, AnalysisReport.class); } @@ -375,71 +409,72 @@ void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_b // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a json stack analysis report var responseAnalysis = exhortApiSut.componentAnalysis(tmpFile.toString()); // verify we got the correct analysis report then(responseAnalysis.get()).isEqualTo(expectedReport); - //cleanup + // cleanup Files.deleteIfExists(tmpFile); } } - @AfterEach void afterEach() { System.clearProperty("EXHORT_DEV_MODE"); System.clearProperty("DEV_EXHORT_BACKEND_URL"); System.clearProperty("RHDA_TOKEN"); System.clearProperty("RHDA_SOURCE"); - } @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") void check_Exhort_Url_When_DEV_Mode_true_Both() { - System.setProperty("EXHORT_DEV_MODE","true"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); + System.setProperty("EXHORT_DEV_MODE", "true"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); } -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") + + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") void check_Exhort_Url_When_env_DEV_Mode_true_property_DEV_Mode_false() { - System.setProperty("EXHORT_DEV_MODE","false"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); + System.setProperty("EXHORT_DEV_MODE", "false"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); } -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") - void check_Exhort_Url_When_env_DEV_Mode_true_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { - String dummyUrl = "http://dummy-url"; - System.setProperty("DEV_EXHORT_BACKEND_URL", dummyUrl); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(dummyUrl); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void + check_Exhort_Url_When_env_DEV_Mode_true_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { + String dummyUrl = "http://dummy-url"; + System.setProperty("DEV_EXHORT_BACKEND_URL", dummyUrl); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(dummyUrl); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="false") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") -void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void + check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { System.setProperty("EXHORT_DEV_MODE", "false"); ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } - @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value= "false") - void check_Exhort_Url_When_env_DEV_Mode_false_And_Property_Dev_Mode_true_Default_Exhort_URL_Selected() { + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + void + check_Exhort_Url_When_env_DEV_Mode_false_And_Property_Dev_Mode_true_Default_Exhort_URL_Selected() { System.setProperty("EXHORT_DEV_MODE", "true"); ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); @@ -447,34 +482,40 @@ void check_Exhort_Url_When_env_DEV_Mode_false_And_Property_Dev_Mode_true_Default } @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="false") - @SetEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL", value="http://dummy-route") - void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_Exhort_URL_Selected_Anyway() { + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + @SetEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL", value = "http://dummy-route") + void + check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_Exhort_URL_Selected_Anyway() { System.setProperty("EXHORT_DEV_MODE", "true"); - System.setProperty("DEV_EXHORT_BACKEND_URL","http://dummy-route2"); + System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); then(exhortApi.getEndpoint()).isNotEqualTo(System.getenv("DEV_EXHORT_BACKEND_URL")); then(exhortApi.getEndpoint()).isNotEqualTo(System.getProperty("DEV_EXHORT_BACKEND_URL")); - } + @Test - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_false_Then_Default_Exhort_URL_Selected() { + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_false_Then_Default_Exhort_URL_Selected() { System.setProperty("EXHORT_DEV_MODE", "false"); ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } + @Test - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_Then_Default_DEV_Exhort_URL_Selected() { + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_Then_Default_DEV_Exhort_URL_Selected() { System.setProperty("EXHORT_DEV_MODE", "true"); ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } + @Test - @SetEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL", value="http://dummy-route") - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_and_Env_DEV_Exhort_Backend_Url_Set_Then_DEV_ENV_Exhort_URL_Selected() { + @SetEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL", value = "http://dummy-route") + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_and_Env_DEV_Exhort_Backend_Url_Set_Then_DEV_ENV_Exhort_URL_Selected() { System.setProperty("EXHORT_DEV_MODE", "true"); System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); ExhortApi exhortApi = new ExhortApi(); @@ -488,58 +529,105 @@ void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_tru void check_Exhort_Url_When_Nothing_Set_Then_Default_Exhort_URL_Selected() { ExhortApi exhortApi = new ExhortApi(); then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - } @Test @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void test_image_analysis() throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { + void test_image_analysis() + throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"}); - var reportIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var sbomIS = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"}); + var reportIS = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + + var imageRef = + new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = + new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var jsonReport = + new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var httpResponse = mock(HttpResponse.class); when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(jsonReport); - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.uri() + .equals( + URI.create( + String.format( + "%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers() + .firstValue("Accept") + .get() + .equals(Api.MediaType.APPLICATION_JSON.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); + .thenReturn(CompletableFuture.completedFuture(httpResponse)); var result = exhortApiSut.imageAnalysis(Set.of(imageRef)); var reports = result.get(); assertEquals(2, reports.size()); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); } } @@ -547,43 +635,75 @@ void test_image_analysis() throws IOException, ExecutionException, InterruptedEx @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void imageAnalysisHtml() throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { + void imageAnalysisHtml() + throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"}); - var reportIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var sbomIS = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"}); + var reportIS = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + + var imageRef = + new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = + new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var jsonReport = + new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var httpResponse = mock(HttpResponse.class); when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(jsonReport); - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.TEXT_HTML.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); + ArgumentMatcher matchesRequest = + r -> + r.uri() + .equals( + URI.create( + String.format( + "%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers() + .firstValue("Accept") + .get() + .equals(Api.MediaType.TEXT_HTML.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); + .thenReturn(CompletableFuture.completedFuture(httpResponse)); var result = exhortApiSut.imageAnalysisHtml(Set.of(imageRef)); assertEquals(jsonReport, result.get()); @@ -594,24 +714,38 @@ void imageAnalysisHtml() throws IOException, ExecutionException, InterruptedExce @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void test_perform_batch_analysis() throws IOException, MalformedPackageURLException, ExecutionException, InterruptedException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { + void test_perform_batch_analysis() + throws IOException, MalformedPackageURLException, ExecutionException, InterruptedException { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { var sbomsGenerator = mock(Supplier.class); var responseBodyHandler = mock(HttpResponse.BodyHandler.class); var responseGenerator = mock(Function.class); var exceptionResponseGenerator = mock(Supplier.class); - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); + ArgumentMatcher matchesRequest = + r -> + r.uri() + .equals( + URI.create( + String.format( + "%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers() + .firstValue("Accept") + .get() + .equals(Api.MediaType.APPLICATION_JSON.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); + + var imageRef = + new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); var sboms = new HashMap(); sboms.put(imageRef.getPackageURL().canonicalize(), new ObjectMapper().readTree(is)); @@ -619,20 +753,21 @@ void test_perform_batch_analysis() throws IOException, MalformedPackageURLExcept when(httpResponse.statusCode()).thenReturn(200); when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); + .thenReturn(CompletableFuture.completedFuture(httpResponse)); when(sbomsGenerator.get()).thenReturn(sboms); var expectedResult = "test-result"; when(responseGenerator.apply(eq(httpResponse))).thenReturn(expectedResult); - var result = exhortApiSut.performBatchAnalysis( - sbomsGenerator, - Api.MediaType.APPLICATION_JSON, - responseBodyHandler, - responseGenerator, - exceptionResponseGenerator, - "Image Analysis"); + var result = + exhortApiSut.performBatchAnalysis( + sbomsGenerator, + Api.MediaType.APPLICATION_JSON, + responseBodyHandler, + responseGenerator, + exceptionResponseGenerator, + "Image Analysis"); assertEquals(expectedResult, result.get()); } @@ -641,26 +776,45 @@ void test_perform_batch_analysis() throws IOException, MalformedPackageURLExcept @Test void test_get_batch_image_sboms() throws IOException, MalformedPackageURLException { try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { + var imageRef = + new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var output = new Operations.ProcessExecOutput(json, "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), + aryEq( + new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); var sboms = exhortApiSut.getBatchImageSboms(Set.of(imageRef)); var mapper = new ObjectMapper(); var node = mapper.readTree(json); - ((ObjectNode) node.get("metadata").get("component")).set("purl", new TextNode(imageRef.getPackageURL().canonicalize())); + ((ObjectNode) node.get("metadata").get("component")) + .set("purl", new TextNode(imageRef.getPackageURL().canonicalize())); var map = new HashMap(); map.put(imageRef.getPackageURL().canonicalize(), node); @@ -671,8 +825,13 @@ void test_get_batch_image_sboms() throws IOException, MalformedPackageURLExcepti @Test void test_get_batch_image_analysis_reports() throws IOException, MalformedPackageURLException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + var json = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); var httpResponse = mock(HttpResponse.class); when(httpResponse.statusCode()).thenReturn(200); @@ -680,10 +839,26 @@ void test_get_batch_image_analysis_reports() throws IOException, MalformedPackag var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); assertEquals(2, reports.size()); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); } } @@ -703,8 +878,8 @@ void test_get_batch_analysis_reports_from_response() { var responseGenerator = mock(Function.class); - exhortApiSut.getBatchAnalysisReportsFromResponse(httpResponse, responseGenerator, - "test-operation", "testReport", "testTraceId"); + exhortApiSut.getBatchAnalysisReportsFromResponse( + httpResponse, responseGenerator, "test-operation", "testReport", "testTraceId"); verify(responseGenerator).apply(eq(httpResponse)); } diff --git a/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java b/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java index eeccea8a..31833a0b 100644 --- a/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java +++ b/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java @@ -15,18 +15,15 @@ */ package com.redhat.exhort.providers; -import com.redhat.exhort.tools.Operations; -import org.apache.commons.io.FileSystemUtils; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.regex.Pattern; - import org.apache.commons.io.FileUtils; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; @Tag("gitTest") class GoModulesMainModuleVersionTest { @@ -35,15 +32,16 @@ class GoModulesMainModuleVersionTest { private Path testGitRepo; private GoModulesProvider goModulesProvider; - @BeforeEach void setUp() { try { this.goModulesProvider = new GoModulesProvider(); this.testGitRepo = Files.createTempDirectory("exhort_tmp"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "init"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "config","user.email","tester@exhort-java-api.com"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "config","user.name","exhort-java-api-tester"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "init"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "config", "user.email", "tester@exhort-java-api.com"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "config", "user.name", "exhort-java-api-tester"); this.noGitRepo = Files.createTempDirectory("exhort_tmp"); } catch (IOException e) { throw new RuntimeException(e); @@ -61,59 +59,53 @@ void tearDown() { } } - - - @Test void determine_Main_Module_Version_NoRepo() { goModulesProvider.determineMainModuleVersion(noGitRepo); - assertEquals(goModulesProvider.defaultMainVersion,goModulesProvider.getMainModuleVersion()); + assertEquals(goModulesProvider.defaultMainVersion, goModulesProvider.getMainModuleVersion()); } @Test void determine_Main_Module_Version_GitRepo() { goModulesProvider.determineMainModuleVersion(testGitRepo); assertEquals(goModulesProvider.defaultMainVersion, goModulesProvider.getMainModuleVersion()); - } @Test void determine_Main_Module_Version_GitRepo_commit_is_tag() { - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); goModulesProvider.determineMainModuleVersion(testGitRepo); assertEquals("v1.0.0", goModulesProvider.getMainModuleVersion()); - } @Test void determine_Main_Module_Version_GitRepo_commit_is_annotated_tag() { - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "-a", "-m" ,"annotatedTag", "v1.0.0a"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "tag", "-a", "-m", "annotatedTag", "v1.0.0a"); goModulesProvider.determineMainModuleVersion(testGitRepo); assertEquals("v1.0.0a", goModulesProvider.getMainModuleVersion()); - } - @Test void determine_Main_Module_Version_GitRepo_commit_is_after_tag() { - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample2\"", "--allow-empty"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "commit", "-m \"sample2\"", "--allow-empty"); goModulesProvider.determineMainModuleVersion(testGitRepo); - assertTrue(Pattern.matches("v1.0.1-0.[0-9]{14}-[a-f0-9]{12}",goModulesProvider.getMainModuleVersion())); - + assertTrue( + Pattern.matches( + "v1.0.1-0.[0-9]{14}-[a-f0-9]{12}", goModulesProvider.getMainModuleVersion())); } - - - - - } diff --git a/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java index 542fc01e..8142736b 100644 --- a/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java @@ -15,23 +15,22 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; @ExtendWith(HelperExtension.class) class Golang_Modules_Provider_Test extends ExhortTest { @@ -41,13 +40,12 @@ class Golang_Modules_Provider_Test extends ExhortTest { // - expected_sbom.json: the SBOM expected to be provided static Stream testFolders() { return Stream.of( - "go_mod_light_no_ignore", - "go_mod_no_ignore", - "go_mod_with_ignore", - "go_mod_with_all_ignore", - "go_mod_with_one_ignored_prefix_go", - "go_mod_no_path" - ); + "go_mod_light_no_ignore", + "go_mod_no_ignore", + "go_mod_with_ignore", + "go_mod_with_all_ignore", + "go_mod_with_one_ignored_prefix_go", + "go_mod_no_path"); } @ParameterizedTest @@ -56,12 +54,19 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc // create temp file hosting our sut package.json var tmpGoModulesDir = Files.createTempDirectory("exhort_test_"); var tmpGolangFile = Files.createFile(tmpGoModulesDir.resolve("go.mod")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "go.mod"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "golang", testFolder, "go.mod"})) { Files.write(tmpGolangFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "expected_sbom_stack_analysis.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] { + "tst_manifests", "golang", testFolder, "expected_sbom_stack_analysis.json" + })) { expectedSbom = new String(is.readAllBytes()); } // when providing stack content for our pom @@ -70,8 +75,7 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc Files.deleteIfExists(tmpGolangFile); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } @ParameterizedTest @@ -79,87 +83,121 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { // load the pom target pom file byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "go.mod"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "golang", testFolder, "go.mod"})) { targetPom = is.readAllBytes(); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "expected_sbom_component_analysis.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] { + "tst_manifests", "golang", testFolder, "expected_sbom_component_analysis.json" + })) { expectedSbom = new String(is.readAllBytes()); } // when providing component content for our pom var content = new GoModulesProvider().provideComponent(targetPom); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - @Test void Test_The_ProvideComponent_Path_Should_Throw_Exception() { GoModulesProvider goModulesProvider = new GoModulesProvider(); - assertThatIllegalArgumentException().isThrownBy(() -> { - goModulesProvider.provideComponent(Path.of(".")); - }).withMessage("provideComponent with file system path for GoModules package manager not implemented yet"); - - + assertThatIllegalArgumentException() + .isThrownBy( + () -> { + goModulesProvider.provideComponent(Path.of(".")); + }) + .withMessage( + "provideComponent with file system path for GoModules package manager not implemented" + + " yet"); } @ParameterizedTest - @ValueSource(booleans = { true,false }) + @ValueSource(booleans = {true, false}) void Test_Golang_Modules_with_Match_Manifest_Version(boolean MatchManifestVersionsEnabled) { String goModPath = getFileFromResource("go.mod", "msc", "golang", "go.mod"); GoModulesProvider goModulesProvider = new GoModulesProvider(); - if(MatchManifestVersionsEnabled) - { - System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), true), "Expected getDependenciesSbom/2 to throw RuntimeException, due to version mismatch, but it didn't."); - assertTrue(runtimeException.getMessage().contains("Can't continue with analysis - versions mismatch for dependency name=github.com/google/uuid, manifest version=v1.1.0, installed Version=v1.1.1")); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } - else - { - String sbomString = assertDoesNotThrow(() -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), false).getAsJsonString()); - String actualSbomWithTSStripped = dropIgnoredKeepFormat(sbomString); - assertEquals(getStringFromFile("msc","golang","expected_sbom_ca.json").trim(), actualSbomWithTSStripped); - - System.out.println(sbomString); - } + if (MatchManifestVersionsEnabled) { + System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); + RuntimeException runtimeException = + assertThrows( + RuntimeException.class, + () -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), true), + "Expected getDependenciesSbom/2 to throw RuntimeException, due to version mismatch," + + " but it didn't."); + assertTrue( + runtimeException + .getMessage() + .contains( + "Can't continue with analysis - versions mismatch for dependency" + + " name=github.com/google/uuid, manifest version=v1.1.0, installed" + + " Version=v1.1.1")); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } else { + String sbomString = + assertDoesNotThrow( + () -> + goModulesProvider + .getDependenciesSbom(Path.of(goModPath), false) + .getAsJsonString()); + String actualSbomWithTSStripped = dropIgnoredKeepFormat(sbomString); + assertEquals( + getStringFromFile("msc", "golang", "expected_sbom_ca.json").trim(), + actualSbomWithTSStripped); + + System.out.println(sbomString); + } } @Test void Test_Golang_MvS_Logic_Enabled() throws IOException { ObjectMapper om = new ObjectMapper(); System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED", "true"); - String goModPath = getFileFromResource("go.mod", "msc", "golang","mvs_logic", "go.mod"); + String goModPath = getFileFromResource("go.mod", "msc", "golang", "mvs_logic", "go.mod"); GoModulesProvider goModulesProvider = new GoModulesProvider(); - String resultSbom = dropIgnoredKeepFormat(goModulesProvider.getDependenciesSbom(Path.of(goModPath),true).getAsJsonString()); - String expectedSbom = getStringFromFile("msc", "golang", "mvs_logic", "expected_sbom_stack_analysis.json").trim(); - - assertEquals(expectedSbom,resultSbom); - - // check that only one version of package golang/go.opencensus.io is in sbom for EXHORT_GO_MVS_LOGIC_ENABLED=true - assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())).filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")).count() == 1); + String resultSbom = + dropIgnoredKeepFormat( + goModulesProvider.getDependenciesSbom(Path.of(goModPath), true).getAsJsonString()); + String expectedSbom = + getStringFromFile("msc", "golang", "mvs_logic", "expected_sbom_stack_analysis.json").trim(); + + assertEquals(expectedSbom, resultSbom); + + // check that only one version of package golang/go.opencensus.io is in sbom for + // EXHORT_GO_MVS_LOGIC_ENABLED=true + assertTrue( + Arrays.stream(resultSbom.split(System.lineSeparator())) + .filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")) + .count() + == 1); System.clearProperty("EXHORT_GO_MVS_LOGIC_ENABLED"); - resultSbom = dropIgnoredKeepFormat(goModulesProvider.getDependenciesSbom(Path.of(goModPath),true).getAsJsonString()); - // check that there is more than one version of package golang/go.opencensus.io in sbom for EXHORT_GO_MVS_LOGIC_ENABLED=false - assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())).filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")).count() > 1); - + resultSbom = + dropIgnoredKeepFormat( + goModulesProvider.getDependenciesSbom(Path.of(goModPath), true).getAsJsonString()); + // check that there is more than one version of package golang/go.opencensus.io in sbom for + // EXHORT_GO_MVS_LOGIC_ENABLED=false + assertTrue( + Arrays.stream(resultSbom.split(System.lineSeparator())) + .filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")) + .count() + > 1); } - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); } - private String dropIgnoredKeepFormat(String s) { + + private String dropIgnoredKeepFormat(String s) { return s.replaceAll("\"timestamp\" : \"[a-zA-Z0-9\\-\\:]+\",\n ", ""); } - } diff --git a/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java index 595d33ed..6c5c2f28 100644 --- a/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java @@ -15,46 +15,40 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mockStatic; + import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentMatcher; import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Optional; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mockStatic; - @ExtendWith(HelperExtension.class) @ExtendWith(MockitoExtension.class) class Gradle_Provider_Test extends ExhortTest { -// private static System.Logger log = System.getLogger("Gradle_Provider_Test"); + // private static System.Logger log = System.getLogger("Gradle_Provider_Test"); // test folder are located at src/test/resources/tst_manifests // each folder should contain: // - build.gradle: the target manifest for testing // - expected_sbom.json: the SBOM expected to be provided static Stream testFolders() { return Stream.of( - "deps_with_ignore_full_specification", - "deps_with_ignore_named_params", - "deps_with_ignore_notations", - "deps_with_no_ignore_common_paths" - ); + "deps_with_ignore_full_specification", + "deps_with_ignore_named_params", + "deps_with_ignore_notations", + "deps_with_no_ignore_common_paths"); } @ParameterizedTest @@ -63,30 +57,56 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc // create temp file hosting our sut build.gradle var tmpGradleDir = Files.createTempDirectory("exhort_test_"); var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { Files.write(tmpGradleFile, is.readAllBytes()); } var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "settings.gradle"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "settings.gradle"))) { Files.write(settingsFile, is.readAllBytes()); } var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join( + "/", "tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { Files.write(libsVersionFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "expected_stack_sbom.json"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join( + "/", "tst_manifests", "gradle", testFolder, "expected_stack_sbom.json"))) { expectedSbom = new String(is.readAllBytes()); } String depTree; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "depTree.txt"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "depTree.txt"))) { depTree = new String(is.readAllBytes()); } String gradleProperties; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle.properties"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "gradle.properties"))) { gradleProperties = new String(is.readAllBytes()); } @@ -95,8 +115,18 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc ArgumentMatcher dependencies = string -> string.equals("dependencies"); ArgumentMatcher properties = string -> string.equals("properties"); mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle),argThat(dependencies))).thenReturn(depTree); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))).thenReturn(gradleProperties); + mockedOperations + .when( + () -> + Operations.runProcessGetOutput( + any(Path.class), argThat(gradle), argThat(dependencies))) + .thenReturn(depTree); + mockedOperations + .when( + () -> + Operations.runProcessGetOutput( + any(Path.class), argThat(gradle), argThat(properties))) + .thenReturn(gradleProperties); // when providing stack content for our pom var content = new GradleProvider().provideStack(tmpGradleFile); @@ -105,9 +135,7 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc // verify expected SBOM is returned mockedOperations.close(); assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } @ParameterizedTest @@ -115,46 +143,83 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { // load the pom target pom file byte[] targetGradleBuild; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { targetGradleBuild = is.readAllBytes(); } GradleProvider gradleProvider = new GradleProvider(); - assertThatIllegalArgumentException().isThrownBy(() -> { - gradleProvider.provideComponent(targetGradleBuild); - }).withMessage("Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); - + assertThatIllegalArgumentException() + .isThrownBy( + () -> { + gradleProvider.provideComponent(targetGradleBuild); + }) + .withMessage( + "Gradle Package Manager requires the full package directory, not just the manifest" + + " content, to generate the dependency tree. Please provide the complete package" + + " directory path."); } + @ParameterizedTest @MethodSource("testFolders") - void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { + void test_the_provideComponent_With_Path(String testFolder) + throws IOException, InterruptedException { // create temp file hosting our sut build.gradle var tmpGradleDir = Files.createTempDirectory("exhort_test_"); var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { Files.write(tmpGradleFile, is.readAllBytes()); } var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "settings.gradle"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "settings.gradle"))) { Files.write(settingsFile, is.readAllBytes()); } var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join( + "/", "tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { Files.write(libsVersionFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "expected_component_sbom.json"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join( + "/", "tst_manifests", "gradle", testFolder, "expected_component_sbom.json"))) { expectedSbom = new String(is.readAllBytes()); } String depTree; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "depTree.txt"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "depTree.txt"))) { depTree = new String(is.readAllBytes()); } String gradleProperties; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle.properties"))) { + try (var is = + getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "gradle.properties"))) { gradleProperties = new String(is.readAllBytes()); } @@ -163,8 +228,18 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException, ArgumentMatcher dependencies = string -> string.equals("dependencies"); ArgumentMatcher properties = string -> string.equals("properties"); mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle),argThat(dependencies))).thenReturn(depTree); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))).thenReturn(gradleProperties); + mockedOperations + .when( + () -> + Operations.runProcessGetOutput( + any(Path.class), argThat(gradle), argThat(dependencies))) + .thenReturn(depTree); + mockedOperations + .when( + () -> + Operations.runProcessGetOutput( + any(Path.class), argThat(gradle), argThat(properties))) + .thenReturn(gradleProperties); // when providing component content for our pom var content = new GradleProvider().provideComponent(tmpGradleFile); @@ -173,11 +248,10 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException, // verify expected SBOM is returned mockedOperations.close(); assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); } } diff --git a/src/test/java/com/redhat/exhort/providers/HelperExtension.java b/src/test/java/com/redhat/exhort/providers/HelperExtension.java index a33de94d..5464dbe8 100644 --- a/src/test/java/com/redhat/exhort/providers/HelperExtension.java +++ b/src/test/java/com/redhat/exhort/providers/HelperExtension.java @@ -15,46 +15,46 @@ */ package com.redhat.exhort.providers; -import com.redhat.exhort.tools.Operations; -import com.redhat.exhort.utils.PythonControllerBase; -import com.redhat.exhort.utils.PythonControllerTestEnv; -import org.junit.jupiter.api.extension.*; - -import java.io.IOException; -import java.nio.file.Files; import java.util.List; +import org.junit.jupiter.api.extension.*; - -public class HelperExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { +public class HelperExtension + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { private System.Logger log = System.getLogger(this.getClass().getName()); -// public PythonEnvironmentExtension(List requirementsFiles) { -// this.requirementsFiles = requirementsFiles; -// } + // public PythonEnvironmentExtension(List requirementsFiles) { + // this.requirementsFiles = requirementsFiles; + // } private List requirementsFiles; + @Override public void afterAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Finished all tests!!"); - + log.log(System.Logger.Level.INFO, "Finished all tests!!"); } @Override public void afterEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Finished Test Method: %s_%s", extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); + log.log( + System.Logger.Level.INFO, + String.format( + "Finished Test Method: %s_%s", + extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); } @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Before all tests"); + log.log(System.Logger.Level.INFO, "Before all tests"); } @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Started Test Method: %s_%s", extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); + log.log( + System.Logger.Level.INFO, + String.format( + "Started Test Method: %s_%s", + extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); } - - } diff --git a/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java b/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java index 1fb06664..cb38b864 100644 --- a/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java @@ -15,15 +15,14 @@ */ package com.redhat.exhort.providers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collections; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.SetEnvironmentVariable; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - public class Java_Envs_Test { @Test diff --git a/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java index 2fbde9b0..3e1615fa 100644 --- a/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java @@ -16,25 +16,21 @@ package com.redhat.exhort.providers; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Optional; import java.util.stream.Stream; - -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mockStatic; - - -import com.redhat.exhort.Api; import org.mockito.MockedStatic; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -43,26 +39,22 @@ @ExtendWith(MockitoExtension.class) public class Java_Maven_Provider_Test extends ExhortTest { - -// private static System.Logger log = System.getLogger("Java_Maven_Provider_Test"); + // private static System.Logger log = System.getLogger("Java_Maven_Provider_Test"); // test folder are located at src/test/resources/tst_manifests // each folder should contain: // - pom.xml: the target manifest for testing // - expected_sbom.json: the SBOM expected to be provided static Stream testFolders() { return Stream.of( - "pom_deps_with_no_ignore_provided_scope", - "deps_no_trivial_with_ignore", - "deps_with_ignore_on_artifact", - "deps_with_ignore_on_dependency", - "deps_with_ignore_on_group", - "deps_with_ignore_on_version", - "deps_with_ignore_on_wrong", - "deps_with_no_ignore", - "pom_deps_with_no_ignore_common_paths" - - - ); + "pom_deps_with_no_ignore_provided_scope", + "deps_no_trivial_with_ignore", + "deps_with_ignore_on_artifact", + "deps_with_ignore_on_dependency", + "deps_with_ignore_on_group", + "deps_with_ignore_on_version", + "deps_with_ignore_on_wrong", + "deps_with_no_ignore", + "pom_deps_with_no_ignore_common_paths"); } @ParameterizedTest @@ -70,25 +62,34 @@ static Stream testFolders() { void test_the_provideStack(String testFolder) throws IOException, InterruptedException { // create temp file hosting our sut pom.xml var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getResourceAsStreamDecision(getClass(), new String []{ "tst_manifests", "maven", testFolder, "pom.xml"})) { + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { Files.write(tmpPomFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_stack_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), + new String[] {"tst_manifests", "maven", testFolder, "expected_stack_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String depTree; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "depTree.txt"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "depTree.txt"})) { depTree = new String(is.readAllBytes()); } MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock,"-DoutputFile"); - }); - + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"); + }); // when providing stack content for our pom var content = new JavaMavenProvider().provideStack(tmpPomFile); @@ -97,17 +98,19 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc // verify expected SBOM is returned mockedOperations.close(); assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - public static String getOutputFileAndOverwriteItWithMock(String outputFileContent, InvocationOnMock invocationOnMock,String parameterPrefix) throws IOException { + public static String getOutputFileAndOverwriteItWithMock( + String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) + throws IOException { String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; - Optional outputFileArg = Arrays.stream(rawArguments).filter(arg -> arg!= null && arg.startsWith(parameterPrefix)).findFirst(); - String outputFilePath=null; - if(outputFileArg.isPresent()) - { + Optional outputFileArg = + Arrays.stream(rawArguments) + .filter(arg -> arg != null && arg.startsWith(parameterPrefix)) + .findFirst(); + String outputFilePath = null; + if (outputFileArg.isPresent()) { String outputFile = outputFileArg.get(); outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); Files.writeString(Path.of(outputFilePath), outputFileContent); @@ -120,70 +123,90 @@ public static String getOutputFileAndOverwriteItWithMock(String outputFileConten void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { // load the pom target pom file byte[] targetPom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { targetPom = is.readAllBytes(); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), + new String[] {"tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String effectivePom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "effectivePom.xml"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "effectivePom.xml"})) { effectivePom = new String(is.readAllBytes()); } MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock,"-Doutput"); - }); + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock( + effectivePom, invocationOnMock, "-Doutput"); + }); // when providing component content for our pom var content = new JavaMavenProvider().provideComponent(targetPom); mockedOperations.close(); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } + @ParameterizedTest @MethodSource("testFolders") - void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { + void test_the_provideComponent_With_Path(String testFolder) + throws IOException, InterruptedException { // load the pom target pom file // create temp file hosting our sut pom.xml var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); - try (var is = getResourceAsStreamDecision(getClass(),new String [] { "tst_manifests", "maven", testFolder, "pom.xml"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { Files.write(tmpPomFile, is.readAllBytes()); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), + new String[] {"tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String effectivePom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "effectivePom.xml"})) { + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "effectivePom.xml"})) { effectivePom = new String(is.readAllBytes()); } MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock,"-Doutput"); - }); + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock( + effectivePom, invocationOnMock, "-Doutput"); + }); // when providing component content for our pom var content = new JavaMavenProvider().provideComponent(tmpPomFile); // verify expected SBOM is returned mockedOperations.close(); assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); } } diff --git a/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java b/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java index 8c35032a..4410f03c 100644 --- a/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java @@ -15,25 +15,26 @@ */ package com.redhat.exhort.providers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.File; +import java.util.Collections; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.junitpioneer.jupiter.SetSystemProperty; -import java.io.File; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - public class Javascript_Envs_Test { @Test @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") @SetEnvironmentVariable(key = "PATH", value = "test-path") void test_javascript_get_envs() { var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertEquals(Collections.singletonMap("PATH", "test-path" + File.pathSeparator + "test-node-home"), envs); + assertEquals( + Collections.singletonMap("PATH", "test-path" + File.pathSeparator + "test-node-home"), + envs); } @Test diff --git a/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java index f573aa2f..2a1e51e0 100644 --- a/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java @@ -19,18 +19,16 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; - -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - -import com.redhat.exhort.Api; import org.mockito.*; @ExtendWith(HelperExtension.class) @@ -40,15 +38,9 @@ class Javascript_Npm_Provider_Test extends ExhortTest { // - package.json: the target manifest for testing // - expected_sbom.json: the SBOM expected to be provided static Stream testFolders() { - return Stream.of( - "deps_with_ignore", - "deps_with_no_ignore" - ); + return Stream.of("deps_with_ignore", "deps_with_no_ignore"); } - - - @ParameterizedTest @MethodSource("testFolders") void test_the_provideStack(String testFolder) throws IOException, InterruptedException { @@ -56,26 +48,39 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); var tmpLockFile = Files.createFile(tmpNpmFolder.resolve("package-lock.json")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { Files.write(tmpNpmFile, is.readAllBytes()); } - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package-lock.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "package-lock.json"})) { Files.write(tmpLockFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_stack_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "expected_stack_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String npmListingStack; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-stack.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "npm-ls-stack.json"})) { npmListingStack = new String(is.readAllBytes()); } MockedStatic mockedOperations = mockStatic(Operations.class); - //Operations.runProcess(contains("npm i"),any()) + // Operations.runProcess(contains("npm i"),any()) ArgumentMatcher matchPath = path -> path == null; - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingStack); + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingStack); // when providing stack content for our pom var content = new JavaScriptNpmProvider().provideStack(tmpNpmFile); // cleanup @@ -85,8 +90,7 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc mockedOperations.close(); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } @ParameterizedTest @@ -94,43 +98,57 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { // load the pom target pom file byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { targetPom = is.readAllBytes(); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String npmListingComponent; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { npmListingComponent = new String(is.readAllBytes()); } -// MockedStatic javaFiles = mockStatic(Files.class); - //Operations.runProcess(contains("npm i"),any()) -// mockedOperations.when(() -> Operations.runProcessGetOutput(eq(null),any())).thenReturn(npmListingComponent); - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer((invocationOnMock) -> { - String[] commandParts = (String [])invocationOnMock.getRawArguments()[0]; - int lastElementIsDir = commandParts.length - 1; - String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; - Files.createFile(Path.of(packageLockJson)); - return packageLockJson ; - }); + // MockedStatic javaFiles = mockStatic(Files.class); + // Operations.runProcess(contains("npm i"),any()) + // mockedOperations.when(() -> + // Operations.runProcessGetOutput(eq(null),any())).thenReturn(npmListingComponent); + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + (invocationOnMock) -> { + String[] commandParts = (String[]) invocationOnMock.getRawArguments()[0]; + int lastElementIsDir = commandParts.length - 1; + String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; + Files.createFile(Path.of(packageLockJson)); + return packageLockJson; + }); ArgumentMatcher matchPath = path -> path == null; - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingComponent); + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingComponent); // when providing component content for our pom var content = new JavaScriptNpmProvider().provideComponent(targetPom); mockedOperations.close(); -// javaFiles.close(); + // javaFiles.close(); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } -@ParameterizedTest + + @ParameterizedTest @MethodSource("testFolders") void test_the_provideComponent_with_Path(String testFolder) throws Exception { // load the pom target pom file @@ -138,37 +156,49 @@ void test_the_provideComponent_with_Path(String testFolder) throws Exception { // create temp file hosting our sut package.json var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { - Files.write(tmpNpmFile, is.readAllBytes()); - } - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { + Files.write(tmpNpmFile, is.readAllBytes()); + } + String expectedSbom = ""; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } - String npmListingComponent; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { - npmListingComponent = new String(is.readAllBytes()); - } - ArgumentMatcher matchPath = path -> path == null; + String npmListingComponent; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { + npmListingComponent = new String(is.readAllBytes()); + } + ArgumentMatcher matchPath = path -> path == null; MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer((invocationOnMock) -> { - String[] commandParts = (String [])invocationOnMock.getRawArguments()[0]; - int lastElementIsDir = commandParts.length - 1; - String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; - Files.createFile(Path.of(packageLockJson)); - return packageLockJson ; - }); - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingComponent); + mockedOperations + .when(() -> Operations.runProcess(any(), any())) + .thenAnswer( + (invocationOnMock) -> { + String[] commandParts = (String[]) invocationOnMock.getRawArguments()[0]; + int lastElementIsDir = commandParts.length - 1; + String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; + Files.createFile(Path.of(packageLockJson)); + return packageLockJson; + }); + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingComponent); // when providing component content for our pom var content = new JavaScriptNpmProvider().provideComponent(tmpNpmFile); mockedOperations.close(); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); } } diff --git a/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java b/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java index 34a7fe35..f94d773d 100644 --- a/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java +++ b/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java @@ -17,81 +17,92 @@ import com.redhat.exhort.tools.Operations; import com.redhat.exhort.utils.PythonControllerBase; -import com.redhat.exhort.utils.PythonControllerVirtualEnv; import com.redhat.exhort.utils.PythonControllerTestEnv; -import org.junit.jupiter.api.extension.*; - -import java.io.IOException; -import java.nio.file.Files; import java.util.List; +import org.junit.jupiter.api.extension.*; - -public class PythonEnvironmentExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver, BeforeTestExecutionCallback { - - - private PythonControllerBase pythonController = new PythonControllerTestEnv(Operations.getCustomPathOrElse("python3"),Operations.getCustomPathOrElse("pip3")); +public class PythonEnvironmentExtension + implements BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback, + AfterEachCallback, + ParameterResolver, + BeforeTestExecutionCallback { + + private PythonControllerBase pythonController = + new PythonControllerTestEnv( + Operations.getCustomPathOrElse("python3"), Operations.getCustomPathOrElse("pip3")); private System.Logger log = System.getLogger(this.getClass().getName()); -// public PythonEnvironmentExtension(List requirementsFiles) { -// this.requirementsFiles = requirementsFiles; -// } + // public PythonEnvironmentExtension(List requirementsFiles) { + // this.requirementsFiles = requirementsFiles; + // } private List requirementsFiles; + @Override public void afterAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Finished all python tests and about to clean environment"); + log.log(System.Logger.Level.INFO, "Finished all python tests and about to clean environment"); pythonController.cleanEnvironment(true); } @Override public void afterEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Finished Test Method: %s", extensionContext.getRequiredTestMethod())); + log.log( + System.Logger.Level.INFO, + String.format("Finished Test Method: %s", extensionContext.getRequiredTestMethod())); } @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Preparing python environment for tests"); + log.log(System.Logger.Level.INFO, "Preparing python environment for tests"); String python3 = Operations.getCustomPathOrElse("python3"); String pip3 = Operations.getCustomPathOrElse("pip3"); - this.pythonController = new PythonControllerTestEnv(python3,pip3); - log.log(System.Logger.Level.INFO,"Finished Preparing environment for testing"); -// var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); -// var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); -// Python_Provider_Test.testFolders().forEach( test -> { -// try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "pip", test, "requirements.txt"))) { -// Files.write(tmpPythonFile, is.readAllBytes()); -// pythonController.installPackage(tmpPythonFile.toAbsolutePath().toString()); -// -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// }); -// log.log(System.Logger.Level.INFO,"Finished Installing all requirements.txt files"); -// Files.deleteIfExists(tmpPythonFile); -// Files.deleteIfExists(tmpPythonModuleDir); - + this.pythonController = new PythonControllerTestEnv(python3, pip3); + log.log(System.Logger.Level.INFO, "Finished Preparing environment for testing"); + // var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); + // var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); + // Python_Provider_Test.testFolders().forEach( test -> { + // try (var is = + // getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "pip", + // test, "requirements.txt"))) { + // Files.write(tmpPythonFile, is.readAllBytes()); + // pythonController.installPackage(tmpPythonFile.toAbsolutePath().toString()); + // + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // }); + // log.log(System.Logger.Level.INFO,"Finished Installing all requirements.txt files"); + // Files.deleteIfExists(tmpPythonFile); + // Files.deleteIfExists(tmpPythonModuleDir); } @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("About to Start Test Method: %s", extensionContext.getRequiredTestMethod())); + log.log( + System.Logger.Level.INFO, + String.format("About to Start Test Method: %s", extensionContext.getRequiredTestMethod())); } @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return parameterContext.getParameter().getType() - .equals(PythonControllerBase.class) || parameterContext.getParameter().getType() - .equals(PythonControllerTestEnv.class); + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType().equals(PythonControllerBase.class) + || parameterContext.getParameter().getType().equals(PythonControllerTestEnv.class); } @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { return this.pythonController; } @Override public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { -// Method requiredTestMethod = extensionContext.getRequiredTestInstances(); + // Method requiredTestMethod = extensionContext.getRequiredTestInstances(); } } diff --git a/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java index 1e3070aa..9ca72a0d 100644 --- a/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java @@ -15,37 +15,33 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.utils.PythonControllerBase; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @ExtendWith(PythonEnvironmentExtension.class) class Python_Provider_Test extends ExhortTest { static Stream testFolders() { - return Stream.of( -"pip_requirements_txt_no_ignore", - "pip_requirements_txt_ignore" - - ); + return Stream.of("pip_requirements_txt_no_ignore", "pip_requirements_txt_ignore"); } -// @RegisterExtension -// private PythonEnvironmentExtension pythonEnvironmentExtension = new PythonEnvironmentExtension(); + // @RegisterExtension + // private PythonEnvironmentExtension pythonEnvironmentExtension = new + // PythonEnvironmentExtension(); public Python_Provider_Test(PythonControllerBase pythonController) { this.pythonController = pythonController; @@ -55,19 +51,26 @@ public Python_Provider_Test(PythonControllerBase pythonController) { private PythonControllerBase pythonController; private PythonPipProvider pythonPipProvider; - @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN",matches = "true") + + @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN", matches = "true") @ParameterizedTest @MethodSource("testFolders") void test_the_provideStack(String testFolder) throws IOException, InterruptedException { // create temp file hosting our sut package.json var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { Files.write(tmpPythonFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } // when providing stack content for our pom @@ -77,109 +80,123 @@ void test_the_provideStack(String testFolder) throws IOException, InterruptedExc Files.deleteIfExists(tmpPythonModuleDir); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN",matches = "true") + @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN", matches = "true") @ParameterizedTest @MethodSource("testFolders") void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { // load the pom target pom file byte[] targetRequirementsTxt; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { targetRequirementsTxt = is.readAllBytes(); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } // when providing component content for our pom var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - @ParameterizedTest @MethodSource("testFolders") - void test_the_provideStack_with_properties(String testFolder) throws IOException, InterruptedException { + void test_the_provideStack_with_properties(String testFolder) + throws IOException, InterruptedException { // create temp file hosting our sut package.json var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { Files.write(tmpPythonFile, is.readAllBytes()); } // load expected SBOM String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } // when providing stack content for our pom var content = this.pythonPipProvider.provideStack(tmpPythonFile); - String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); - String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); - String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); - String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); - System.setProperty("EXHORT_PIP_SHOW",base64PipShow); - System.setProperty("EXHORT_PIP_FREEZE",base64PipFreeze); + String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); + String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); + String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); + String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); + System.setProperty("EXHORT_PIP_SHOW", base64PipShow); + System.setProperty("EXHORT_PIP_FREEZE", base64PipFreeze); // cleanup Files.deleteIfExists(tmpPythonFile); Files.deleteIfExists(tmpPythonModuleDir); - System.clearProperty("EXHORT_PIP_SHOW"); - System.clearProperty("EXHORT_PIP_FREEZE"); + System.clearProperty("EXHORT_PIP_SHOW"); + System.clearProperty("EXHORT_PIP_FREEZE"); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } @ParameterizedTest @MethodSource("testFolders") - void test_the_provideComponent_with_properties(String testFolder) throws IOException, InterruptedException { + void test_the_provideComponent_with_properties(String testFolder) + throws IOException, InterruptedException { // load the pom target pom file byte[] targetRequirementsTxt; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { targetRequirementsTxt = is.readAllBytes(); } // load expected SBOM String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { expectedSbom = new String(is.readAllBytes()); } String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); - System.setProperty("EXHORT_PIP_SHOW",base64PipShow); - System.setProperty("EXHORT_PIP_FREEZE",base64PipFreeze); + System.setProperty("EXHORT_PIP_SHOW", base64PipShow); + System.setProperty("EXHORT_PIP_FREEZE", base64PipFreeze); // when providing component content for our pom - var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); + var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); // verify expected SBOM is returned assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); System.clearProperty("EXHORT_PIP_SHOW"); System.clearProperty("EXHORT_PIP_FREEZE"); - } - @Test void Test_The_ProvideComponent_Path_Should_Throw_Exception() { - assertThatIllegalArgumentException().isThrownBy(() -> { - this.pythonPipProvider.provideComponent(Path.of(".")); - }).withMessage("provideComponent with file system path for Python pip package manager is not supported"); - - + assertThatIllegalArgumentException() + .isThrownBy( + () -> { + this.pythonPipProvider.provideComponent(Path.of(".")); + }) + .withMessage( + "provideComponent with file system path for Python pip package manager is not" + + " supported"); } private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); } } diff --git a/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java b/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java index 545ebee3..4f728773 100644 --- a/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java @@ -18,18 +18,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import com.redhat.exhort.providers.JavaMavenProvider; import java.nio.file.Paths; - import org.junit.jupiter.api.Test; -import com.redhat.exhort.providers.JavaMavenProvider; - class Ecosystem_Test { @Test void get_a_provider_for_an_unknown_package_file_should_throw_an_exception() { var manifestPath = Paths.get("/not/a/supported/mani.fest"); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> Ecosystem.getProvider(manifestPath)); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> Ecosystem.getProvider(manifestPath)); } @Test @@ -37,5 +36,4 @@ void get_a_provider_for_a_pom_xml_file_should_return_java_maven_manifest() { var manifestPath = Paths.get("/supported/manifest/pom.xml"); assertThat(Ecosystem.getProvider(manifestPath)).isInstanceOf(JavaMavenProvider.class); } - } diff --git a/src/test/java/com/redhat/exhort/tools/OperationsTest.java b/src/test/java/com/redhat/exhort/tools/OperationsTest.java index 9428b296..8c011c05 100644 --- a/src/test/java/com/redhat/exhort/tools/OperationsTest.java +++ b/src/test/java/com/redhat/exhort/tools/OperationsTest.java @@ -15,13 +15,12 @@ */ package com.redhat.exhort.tools; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + class OperationsTest { @Test @@ -36,11 +35,19 @@ void when_running_process_for_non_existing_command_should_throw_runtime_exceptio @Test void when_running_process_get_full_output_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcessGetFullOutput(null, new String[]{"ls", "."}, null)); + assertThatNoException() + .isThrownBy(() -> Operations.runProcessGetFullOutput(null, new String[] {"ls", "."}, null)); } @Test - void when_running_process_get_full_output_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcessGetFullOutput(Path.of("."), new String[]{"unknown", "--command"}, new String[]{"PATH=123"})); + void + when_running_process_get_full_output_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException() + .isThrownBy( + () -> + Operations.runProcessGetFullOutput( + Path.of("."), + new String[] {"unknown", "--command"}, + new String[] {"PATH=123"})); } } diff --git a/src/test/java/com/redhat/exhort/tools/Operations_Test.java b/src/test/java/com/redhat/exhort/tools/Operations_Test.java index 2c10837b..14f4b6e9 100644 --- a/src/test/java/com/redhat/exhort/tools/Operations_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Operations_Test.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.SetEnvironmentVariable; @@ -41,7 +40,7 @@ void when_running_process_for_non_existing_command_should_throw_runtime_exceptio } @Nested - @ClearEnvironmentVariable(key="EXHORT_MADE_UP_CMD_PATH") + @ClearEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH") class Test_getCustomPathOrElse { @AfterEach void cleanup() { @@ -49,16 +48,18 @@ void cleanup() { } @Test - @SetEnvironmentVariable(key="EXHORT_MADE_UP_CMD_PATH", value="/path/to/env/made_up_cmd") + @SetEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH", value = "/path/to/env/made_up_cmd") void when_custom_path_exists_in_env_vars_and_properties_should_return_from_env_vars() { System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); - assertThat(Operations.getCustomPathOrElse("made-up cmd")).isEqualTo("/path/to/env/made_up_cmd"); + assertThat(Operations.getCustomPathOrElse("made-up cmd")) + .isEqualTo("/path/to/env/made_up_cmd"); } @Test void when_custom_path_not_in_env_var_but_exists_in_properties_should_return_from_properties() { System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); - assertThat(Operations.getCustomPathOrElse("made-up_cmd")).isEqualTo("/path/to/property/made_up_cmd"); + assertThat(Operations.getCustomPathOrElse("made-up_cmd")) + .isEqualTo("/path/to/property/made_up_cmd"); } @Test diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java index efe03ddf..ea8ad87d 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java @@ -13,1992 +13,2033 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.utils; -import com.redhat.exhort.ExhortTest; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; +import static org.junit.jupiter.api.Assertions.assertEquals; +import com.redhat.exhort.ExhortTest; import java.util.Arrays; import java.util.LinkedList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; class PythonControllerBaseTest extends ExhortTest { - static ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { - @Override - public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); - } - // in var args, must override type default method' void.class in argumentMatcher interface in order to let custom ArgumentMatcher work correctly. - @Override - public Class type() - { - return String[].class; - } + static ArgumentMatcher matchCommandPipFreeze = + new ArgumentMatcher() { + @Override + public boolean matches(String[] command) { + return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); + } - }; + // in var args, must override type default method' void.class in argumentMatcher interface + // in order to let + // custom ArgumentMatcher work correctly. + @Override + public Class type() { + return String[].class; + } + }; - static ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { - @Override - public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("show")); - } + static ArgumentMatcher matchCommandPipShow = + new ArgumentMatcher() { + @Override + public boolean matches(String[] command) { + return Arrays.stream(command).anyMatch(word -> word.contains("show")); + } - @Override - public Class type() - { - return String[].class; - } + @Override + public Class type() { + return String[].class; + } + }; - }; @Test void when_spliting_pip_show_dep_with_license() { List results = PythonControllerBase.splitPipShowLines(PIP_SHOW_LINES); assertEquals(EXPECTED_PIP_SHOW_RESULTS, results); } - - private static final String PIP_SHOW_LINES; static { - - PIP_SHOW_LINES = "Name: altgraph\n" + - "Version: 0.17.2\n" + - "Summary: Python graph (network) package\n" + - "Home-page: https://altgraph.readthedocs.io\n" + - "Author: Ronald Oussoren\n" + - "Author-email: ronaldoussoren@mac.com\n" + - "License: MIT\n" + - "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + - "Requires: \n" + - "Required-by: macholib\n" + - "---\n" + - "Name: scipy\n" + - "Version: 1.11.3\n" + - "Summary: Fundamental algorithms for scientific computing in Python\n" + - "Home-page: https://scipy.org/\n" + - "Author: \n" + - "Author-email: \n" + - "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions\n" + - " are met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above\n" + - " copyright notice, this list of conditions and the following\n" + - " disclaimer in the documentation and/or other materials provided\n" + - " with the distribution.\n" + - " \n" + - " 3. Neither the name of the copyright holder nor the names of its\n" + - " contributors may be used to endorse or promote products derived\n" + - " from this software without specific prior written permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " ----\n" + - " \n" + - " This binary distribution of SciPy also bundles the following software:\n" + - " \n" + - " \n" + - " Name: OpenBLAS\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled as a dynamically linked library\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 2011-2014, The OpenBLAS Project\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer in\n" + - " the documentation and/or other materials provided with the\n" + - " distribution.\n" + - " 3. Neither the name of the OpenBLAS project nor the names of\n" + - " its contributors may be used to endorse or promote products\n" + - " derived from this software without specific prior written\n" + - " permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + - " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + - " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + - " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + - " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + - " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + - " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + - " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + - " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + - " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: LAPACK\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled in OpenBLAS\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + - " of Tennessee Research Foundation. All rights\n" + - " reserved.\n" + - " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + - " rights reserved.\n" + - " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + - " reserved.\n" + - " \n" + - " $COPYRIGHT$\n" + - " \n" + - " Additional copyrights may follow\n" + - " \n" + - " $HEADER$\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " - Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " - Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer listed\n" + - " in this license in the documentation and/or other materials\n" + - " provided with the distribution.\n" + - " \n" + - " - Neither the name of the copyright holders nor the names of its\n" + - " contributors may be used to endorse or promote products derived from\n" + - " this software without specific prior written permission.\n" + - " \n" + - " The copyright holders provide no reassurances that the source code\n" + - " provided does not infringe any patent, copyright, or any other\n" + - " intellectual property rights of third parties. The copyright holders\n" + - " disclaim any liability to any recipient for claims brought against\n" + - " recipient by any third party for infringement of that parties\n" + - " intellectual property rights.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: GCC runtime library\n" + - " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + - " License: GPL-3.0-with-GCC-exception\n" + - " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + - " \n" + - " Libgfortran is free software; you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation; either version 3, or (at your option)\n" + - " any later version.\n" + - " \n" + - " Libgfortran is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " Under Section 7 of GPL version 3, you are granted additional\n" + - " permissions described in the GCC Runtime Library Exception, version\n" + - " 3.1, as published by the Free Software Foundation.\n" + - " \n" + - " You should have received a copy of the GNU General Public License and\n" + - " a copy of the GCC Runtime Library Exception along with this program;\n" + - " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + - " .\n" + - " \n" + - " ----\n" + - " \n" + - " Full text of license texts referred to above follows (that they are\n" + - " listed below does not necessarily imply the conditions apply to the\n" + - " present binary release):\n" + - " \n" + - " ----\n" + - " \n" + - " GCC RUNTIME LIBRARY EXCEPTION\n" + - " \n" + - " Version 3.1, 31 March 2009\n" + - " \n" + - " Copyright (C) 2009 Free Software Foundation, Inc. \n" + - " \n" + - " Everyone is permitted to copy and distribute verbatim copies of this\n" + - " license document, but changing it is not allowed.\n" + - " \n" + - " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + - " permission under section 7 of the GNU General Public License, version\n" + - " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + - " bears a notice placed by the copyright holder of the file stating that\n" + - " the file is governed by GPLv3 along with this Exception.\n" + - " \n" + - " When you use GCC to compile a program, GCC may combine portions of\n" + - " certain GCC header files and runtime libraries with the compiled\n" + - " program. The purpose of this Exception is to allow compilation of\n" + - " non-GPL (including proprietary) programs to use, in this way, the\n" + - " header files and runtime libraries covered by this Exception.\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " A file is an \"Independent Module\" if it either requires the Runtime\n" + - " Library for execution after a Compilation Process, or makes use of an\n" + - " interface provided by the Runtime Library, but is not otherwise based\n" + - " on the Runtime Library.\n" + - " \n" + - " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + - " modifications, governed by version 3 (or a specified later version) of\n" + - " the GNU General Public License (GPL) with the option of using any\n" + - " subsequent versions published by the FSF.\n" + - " \n" + - " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + - " modification and use would permit combination with GCC in accord with\n" + - " the license of GCC.\n" + - " \n" + - " \"Target Code\" refers to output from any compiler for a real or virtual\n" + - " target processor architecture, in executable form or suitable for\n" + - " input to an assembler, loader, linker and/or execution\n" + - " phase. Notwithstanding that, Target Code does not include data in any\n" + - " format that is used as a compiler intermediate representation, or used\n" + - " for producing a compiler intermediate representation.\n" + - " \n" + - " The \"Compilation Process\" transforms code entirely represented in\n" + - " non-intermediate languages designed for human-written code, and/or in\n" + - " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + - " use of source code generators and preprocessors need not be considered\n" + - " part of the Compilation Process, since the Compilation Process can be\n" + - " understood as starting with the output of the generators or\n" + - " preprocessors.\n" + - " \n" + - " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + - " with other GPL-compatible software, or if it is done without using any\n" + - " work based on GCC. For example, using non-GPL-compatible Software to\n" + - " optimize any GCC intermediate representations would not qualify as an\n" + - " Eligible Compilation Process.\n" + - " \n" + - " 1. Grant of Additional Permission.\n" + - " \n" + - " You have permission to propagate a work of Target Code formed by\n" + - " combining the Runtime Library with Independent Modules, even if such\n" + - " propagation would otherwise violate the terms of GPLv3, provided that\n" + - " all Target Code was generated by Eligible Compilation Processes. You\n" + - " may then convey such a combination under terms of your choice,\n" + - " consistent with the licensing of the Independent Modules.\n" + - " \n" + - " 2. No Weakening of GCC Copyleft.\n" + - " \n" + - " The availability of this Exception does not imply any general\n" + - " presumption that third-party software is unaffected by the copyleft\n" + - " requirements of the license of GCC.\n" + - " \n" + - " ----\n" + - " \n" + - " GNU GENERAL PUBLIC LICENSE\n" + - " Version 3, 29 June 2007\n" + - " \n" + - " Copyright (C) 2007 Free Software Foundation, Inc. \n" + - " Everyone is permitted to copy and distribute verbatim copies\n" + - " of this license document, but changing it is not allowed.\n" + - " \n" + - " Preamble\n" + - " \n" + - " The GNU General Public License is a free, copyleft license for\n" + - " software and other kinds of works.\n" + - " \n" + - " The licenses for most software and other practical works are designed\n" + - " to take away your freedom to share and change the works. By contrast,\n" + - " the GNU General Public License is intended to guarantee your freedom to\n" + - " share and change all versions of a program--to make sure it remains free\n" + - " software for all its users. We, the Free Software Foundation, use the\n" + - " GNU General Public License for most of our software; it applies also to\n" + - " any other work released this way by its authors. You can apply it to\n" + - " your programs, too.\n" + - " \n" + - " When we speak of free software, we are referring to freedom, not\n" + - " price. Our General Public Licenses are designed to make sure that you\n" + - " have the freedom to distribute copies of free software (and charge for\n" + - " them if you wish), that you receive source code or can get it if you\n" + - " want it, that you can change the software or use pieces of it in new\n" + - " free programs, and that you know you can do these things.\n" + - " \n" + - " To protect your rights, we need to prevent others from denying you\n" + - " these rights or asking you to surrender the rights. Therefore, you have\n" + - " certain responsibilities if you distribute copies of the software, or if\n" + - " you modify it: responsibilities to respect the freedom of others.\n" + - " \n" + - " For example, if you distribute copies of such a program, whether\n" + - " gratis or for a fee, you must pass on to the recipients the same\n" + - " freedoms that you received. You must make sure that they, too, receive\n" + - " or can get the source code. And you must show them these terms so they\n" + - " know their rights.\n" + - " \n" + - " Developers that use the GNU GPL protect your rights with two steps:\n" + - " (1) assert copyright on the software, and (2) offer you this License\n" + - " giving you legal permission to copy, distribute and/or modify it.\n" + - " \n" + - " For the developers' and authors' protection, the GPL clearly explains\n" + - " that there is no warranty for this free software. For both users' and\n" + - " authors' sake, the GPL requires that modified versions be marked as\n" + - " changed, so that their problems will not be attributed erroneously to\n" + - " authors of previous versions.\n" + - " \n" + - " Some devices are designed to deny users access to install or run\n" + - " modified versions of the software inside them, although the manufacturer\n" + - " can do so. This is fundamentally incompatible with the aim of\n" + - " protecting users' freedom to change the software. The systematic\n" + - " pattern of such abuse occurs in the area of products for individuals to\n" + - " use, which is precisely where it is most unacceptable. Therefore, we\n" + - " have designed this version of the GPL to prohibit the practice for those\n" + - " products. If such problems arise substantially in other domains, we\n" + - " stand ready to extend this provision to those domains in future versions\n" + - " of the GPL, as needed to protect the freedom of users.\n" + - " \n" + - " Finally, every program is threatened constantly by software patents.\n" + - " States should not allow patents to restrict development and use of\n" + - " software on general-purpose computers, but in those that do, we wish to\n" + - " avoid the special danger that patents applied to a free program could\n" + - " make it effectively proprietary. To prevent this, the GPL assures that\n" + - " patents cannot be used to render the program non-free.\n" + - " \n" + - " The precise terms and conditions for copying, distribution and\n" + - " modification follow.\n" + - " \n" + - " TERMS AND CONDITIONS\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " \"This License\" refers to version 3 of the GNU General Public License.\n" + - " \n" + - " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + - " works, such as semiconductor masks.\n" + - " \n" + - " \"The Program\" refers to any copyrightable work licensed under this\n" + - " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + - " \"recipients\" may be individuals or organizations.\n" + - " \n" + - " To \"modify\" a work means to copy from or adapt all or part of the work\n" + - " in a fashion requiring copyright permission, other than the making of an\n" + - " exact copy. The resulting work is called a \"modified version\" of the\n" + - " earlier work or a work \"based on\" the earlier work.\n" + - " \n" + - " A \"covered work\" means either the unmodified Program or a work based\n" + - " on the Program.\n" + - " \n" + - " To \"propagate\" a work means to do anything with it that, without\n" + - " permission, would make you directly or secondarily liable for\n" + - " infringement under applicable copyright law, except executing it on a\n" + - " computer or modifying a private copy. Propagation includes copying,\n" + - " distribution (with or without modification), making available to the\n" + - " public, and in some countries other activities as well.\n" + - " \n" + - " To \"convey\" a work means any kind of propagation that enables other\n" + - " parties to make or receive copies. Mere interaction with a user through\n" + - " a computer network, with no transfer of a copy, is not conveying.\n" + - " \n" + - " An interactive user interface displays \"Appropriate Legal Notices\"\n" + - " to the extent that it includes a convenient and prominently visible\n" + - " feature that (1) displays an appropriate copyright notice, and (2)\n" + - " tells the user that there is no warranty for the work (except to the\n" + - " extent that warranties are provided), that licensees may convey the\n" + - " work under this License, and how to view a copy of this License. If\n" + - " the interface presents a list of user commands or options, such as a\n" + - " menu, a prominent item in the list meets this criterion.\n" + - " \n" + - " 1. Source Code.\n" + - " \n" + - " The \"source code\" for a work means the preferred form of the work\n" + - " for making modifications to it. \"Object code\" means any non-source\n" + - " form of a work.\n" + - " \n" + - " A \"Standard Interface\" means an interface that either is an official\n" + - " standard defined by a recognized standards body, or, in the case of\n" + - " interfaces specified for a particular programming language, one that\n" + - " is widely used among developers working in that language.\n" + - " \n" + - " The \"System Libraries\" of an executable work include anything, other\n" + - " than the work as a whole, that (a) is included in the normal form of\n" + - " packaging a Major Component, but which is not part of that Major\n" + - " Component, and (b) serves only to enable use of the work with that\n" + - " Major Component, or to implement a Standard Interface for which an\n" + - " implementation is available to the public in source code form. A\n" + - " \"Major Component\", in this context, means a major essential component\n" + - " (kernel, window system, and so on) of the specific operating system\n" + - " (if any) on which the executable work runs, or a compiler used to\n" + - " produce the work, or an object code interpreter used to run it.\n" + - " \n" + - " The \"Corresponding Source\" for a work in object code form means all\n" + - " the source code needed to generate, install, and (for an executable\n" + - " work) run the object code and to modify the work, including scripts to\n" + - " control those activities. However, it does not include the work's\n" + - " System Libraries, or general-purpose tools or generally available free\n" + - " programs which are used unmodified in performing those activities but\n" + - " which are not part of the work. For example, Corresponding Source\n" + - " includes interface definition files associated with source files for\n" + - " the work, and the source code for shared libraries and dynamically\n" + - " linked subprograms that the work is specifically designed to require,\n" + - " such as by intimate data communication or control flow between those\n" + - " subprograms and other parts of the work.\n" + - " \n" + - " The Corresponding Source need not include anything that users\n" + - " can regenerate automatically from other parts of the Corresponding\n" + - " Source.\n" + - " \n" + - " The Corresponding Source for a work in source code form is that\n" + - " same work.\n" + - " \n" + - " 2. Basic Permissions.\n" + - " \n" + - " All rights granted under this License are granted for the term of\n" + - " copyright on the Program, and are irrevocable provided the stated\n" + - " conditions are met. This License explicitly affirms your unlimited\n" + - " permission to run the unmodified Program. The output from running a\n" + - " covered work is covered by this License only if the output, given its\n" + - " content, constitutes a covered work. This License acknowledges your\n" + - " rights of fair use or other equivalent, as provided by copyright law.\n" + - " \n" + - " You may make, run and propagate covered works that you do not\n" + - " convey, without conditions so long as your license otherwise remains\n" + - " in force. You may convey covered works to others for the sole purpose\n" + - " of having them make modifications exclusively for you, or provide you\n" + - " with facilities for running those works, provided that you comply with\n" + - " the terms of this License in conveying all material for which you do\n" + - " not control copyright. Those thus making or running the covered works\n" + - " for you must do so exclusively on your behalf, under your direction\n" + - " and control, on terms that prohibit them from making any copies of\n" + - " your copyrighted material outside their relationship with you.\n" + - " \n" + - " Conveying under any other circumstances is permitted solely under\n" + - " the conditions stated below. Sublicensing is not allowed; section 10\n" + - " makes it unnecessary.\n" + - " \n" + - " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + - " \n" + - " No covered work shall be deemed part of an effective technological\n" + - " measure under any applicable law fulfilling obligations under article\n" + - " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + - " similar laws prohibiting or restricting circumvention of such\n" + - " measures.\n" + - " \n" + - " When you convey a covered work, you waive any legal power to forbid\n" + - " circumvention of technological measures to the extent such circumvention\n" + - " is effected by exercising rights under this License with respect to\n" + - " the covered work, and you disclaim any intention to limit operation or\n" + - " modification of the work as a means of enforcing, against the work's\n" + - " users, your or third parties' legal rights to forbid circumvention of\n" + - " technological measures.\n" + - " \n" + - " 4. Conveying Verbatim Copies.\n" + - " \n" + - " You may convey verbatim copies of the Program's source code as you\n" + - " receive it, in any medium, provided that you conspicuously and\n" + - " appropriately publish on each copy an appropriate copyright notice;\n" + - " keep intact all notices stating that this License and any\n" + - " non-permissive terms added in accord with section 7 apply to the code;\n" + - " keep intact all notices of the absence of any warranty; and give all\n" + - " recipients a copy of this License along with the Program.\n" + - " \n" + - " You may charge any price or no price for each copy that you convey,\n" + - " and you may offer support or warranty protection for a fee.\n" + - " \n" + - " 5. Conveying Modified Source Versions.\n" + - " \n" + - " You may convey a work based on the Program, or the modifications to\n" + - " produce it from the Program, in the form of source code under the\n" + - " terms of section 4, provided that you also meet all of these conditions:\n" + - " \n" + - " a) The work must carry prominent notices stating that you modified\n" + - " it, and giving a relevant date.\n" + - " \n" + - " b) The work must carry prominent notices stating that it is\n" + - " released under this License and any conditions added under section\n" + - " 7. This requirement modifies the requirement in section 4 to\n" + - " \"keep intact all notices\".\n" + - " \n" + - " c) You must license the entire work, as a whole, under this\n" + - " License to anyone who comes into possession of a copy. This\n" + - " License will therefore apply, along with any applicable section 7\n" + - " additional terms, to the whole of the work, and all its parts,\n" + - " regardless of how they are packaged. This License gives no\n" + - " permission to license the work in any other way, but it does not\n" + - " invalidate such permission if you have separately received it.\n" + - " \n" + - " d) If the work has interactive user interfaces, each must display\n" + - " Appropriate Legal Notices; however, if the Program has interactive\n" + - " interfaces that do not display Appropriate Legal Notices, your\n" + - " work need not make them do so.\n" + - " \n" + - " A compilation of a covered work with other separate and independent\n" + - " works, which are not by their nature extensions of the covered work,\n" + - " and which are not combined with it such as to form a larger program,\n" + - " in or on a volume of a storage or distribution medium, is called an\n" + - " \"aggregate\" if the compilation and its resulting copyright are not\n" + - " used to limit the access or legal rights of the compilation's users\n" + - " beyond what the individual works permit. Inclusion of a covered work\n" + - " in an aggregate does not cause this License to apply to the other\n" + - " parts of the aggregate.\n" + - " \n" + - " 6. Conveying Non-Source Forms.\n" + - " \n" + - " You may convey a covered work in object code form under the terms\n" + - " of sections 4 and 5, provided that you also convey the\n" + - " machine-readable Corresponding Source under the terms of this License,\n" + - " in one of these ways:\n" + - " \n" + - " a) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by the\n" + - " Corresponding Source fixed on a durable physical medium\n" + - " customarily used for software interchange.\n" + - " \n" + - " b) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by a\n" + - " written offer, valid for at least three years and valid for as\n" + - " long as you offer spare parts or customer support for that product\n" + - " model, to give anyone who possesses the object code either (1) a\n" + - " copy of the Corresponding Source for all the software in the\n" + - " product that is covered by this License, on a durable physical\n" + - " medium customarily used for software interchange, for a price no\n" + - " more than your reasonable cost of physically performing this\n" + - " conveying of source, or (2) access to copy the\n" + - " Corresponding Source from a network server at no charge.\n" + - " \n" + - " c) Convey individual copies of the object code with a copy of the\n" + - " written offer to provide the Corresponding Source. This\n" + - " alternative is allowed only occasionally and noncommercially, and\n" + - " only if you received the object code with such an offer, in accord\n" + - " with subsection 6b.\n" + - " \n" + - " d) Convey the object code by offering access from a designated\n" + - " place (gratis or for a charge), and offer equivalent access to the\n" + - " Corresponding Source in the same way through the same place at no\n" + - " further charge. You need not require recipients to copy the\n" + - " Corresponding Source along with the object code. If the place to\n" + - " copy the object code is a network server, the Corresponding Source\n" + - " may be on a different server (operated by you or a third party)\n" + - " that supports equivalent copying facilities, provided you maintain\n" + - " clear directions next to the object code saying where to find the\n" + - " Corresponding Source. Regardless of what server hosts the\n" + - " Corresponding Source, you remain obligated to ensure that it is\n" + - " available for as long as needed to satisfy these requirements.\n" + - " \n" + - " e) Convey the object code using peer-to-peer transmission, provided\n" + - " you inform other peers where the object code and Corresponding\n" + - " Source of the work are being offered to the general public at no\n" + - " charge under subsection 6d.\n" + - " \n" + - " A separable portion of the object code, whose source code is excluded\n" + - " from the Corresponding Source as a System Library, need not be\n" + - " included in conveying the object code work.\n" + - " \n" + - " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + - " tangible personal property which is normally used for personal, family,\n" + - " or household purposes, or (2) anything designed or sold for incorporation\n" + - " into a dwelling. In determining whether a product is a consumer product,\n" + - " doubtful cases shall be resolved in favor of coverage. For a particular\n" + - " product received by a particular user, \"normally used\" refers to a\n" + - " typical or common use of that class of product, regardless of the status\n" + - " of the particular user or of the way in which the particular user\n" + - " actually uses, or expects or is expected to use, the product. A product\n" + - " is a consumer product regardless of whether the product has substantial\n" + - " commercial, industrial or non-consumer uses, unless such uses represent\n" + - " the only significant mode of use of the product.\n" + - " \n" + - " \"Installation Information\" for a User Product means any methods,\n" + - " procedures, authorization keys, or other information required to install\n" + - " and execute modified versions of a covered work in that User Product from\n" + - " a modified version of its Corresponding Source. The information must\n" + - " suffice to ensure that the continued functioning of the modified object\n" + - " code is in no case prevented or interfered with solely because\n" + - " modification has been made.\n" + - " \n" + - " If you convey an object code work under this section in, or with, or\n" + - " specifically for use in, a User Product, and the conveying occurs as\n" + - " part of a transaction in which the right of possession and use of the\n" + - " User Product is transferred to the recipient in perpetuity or for a\n" + - " fixed term (regardless of how the transaction is characterized), the\n" + - " Corresponding Source conveyed under this section must be accompanied\n" + - " by the Installation Information. But this requirement does not apply\n" + - " if neither you nor any third party retains the ability to install\n" + - " modified object code on the User Product (for example, the work has\n" + - " been installed in ROM).\n" + - " \n" + - " The requirement to provide Installation Information does not include a\n" + - " requirement to continue to provide support service, warranty, or updates\n" + - " for a work that has been modified or installed by the recipient, or for\n" + - " the User Product in which it has been modified or installed. Access to a\n" + - " network may be denied when the modification itself materially and\n" + - " adversely affects the operation of the network or violates the rules and\n" + - " protocols for communication across the network.\n" + - " \n" + - " Corresponding Source conveyed, and Installation Information provided,\n" + - " in accord with this section must be in a format that is publicly\n" + - " documented (and with an implementation available to the public in\n" + - " source code form), and must require no special password or key for\n" + - " unpacking, reading or copying.\n" + - " \n" + - " 7. Additional Terms.\n" + - " \n" + - " \"Additional permissions\" are terms that supplement the terms of this\n" + - " License by making exceptions from one or more of its conditions.\n" + - " Additional permissions that are applicable to the entire Program shall\n" + - " be treated as though they were included in this License, to the extent\n" + - " that they are valid under applicable law. If additional permissions\n" + - " apply only to part of the Program, that part may be used separately\n" + - " under those permissions, but the entire Program remains governed by\n" + - " this License without regard to the additional permissions.\n" + - " \n" + - " When you convey a copy of a covered work, you may at your option\n" + - " remove any additional permissions from that copy, or from any part of\n" + - " it. (Additional permissions may be written to require their own\n" + - " removal in certain cases when you modify the work.) You may place\n" + - " additional permissions on material, added by you to a covered work,\n" + - " for which you have or can give appropriate copyright permission.\n" + - " \n" + - " Notwithstanding any other provision of this License, for material you\n" + - " add to a covered work, you may (if authorized by the copyright holders of\n" + - " that material) supplement the terms of this License with terms:\n" + - " \n" + - " a) Disclaiming warranty or limiting liability differently from the\n" + - " terms of sections 15 and 16 of this License; or\n" + - " \n" + - " b) Requiring preservation of specified reasonable legal notices or\n" + - " author attributions in that material or in the Appropriate Legal\n" + - " Notices displayed by works containing it; or\n" + - " \n" + - " c) Prohibiting misrepresentation of the origin of that material, or\n" + - " requiring that modified versions of such material be marked in\n" + - " reasonable ways as different from the original version; or\n" + - " \n" + - " d) Limiting the use for publicity purposes of names of licensors or\n" + - " authors of the material; or\n" + - " \n" + - " e) Declining to grant rights under trademark law for use of some\n" + - " trade names, trademarks, or service marks; or\n" + - " \n" + - " f) Requiring indemnification of licensors and authors of that\n" + - " material by anyone who conveys the material (or modified versions of\n" + - " it) with contractual assumptions of liability to the recipient, for\n" + - " any liability that these contractual assumptions directly impose on\n" + - " those licensors and authors.\n" + - " \n" + - " All other non-permissive additional terms are considered \"further\n" + - " restrictions\" within the meaning of section 10. If the Program as you\n" + - " received it, or any part of it, contains a notice stating that it is\n" + - " governed by this License along with a term that is a further\n" + - " restriction, you may remove that term. If a license document contains\n" + - " a further restriction but permits relicensing or conveying under this\n" + - " License, you may add to a covered work material governed by the terms\n" + - " of that license document, provided that the further restriction does\n" + - " not survive such relicensing or conveying.\n" + - " \n" + - " If you add terms to a covered work in accord with this section, you\n" + - " must place, in the relevant source files, a statement of the\n" + - " additional terms that apply to those files, or a notice indicating\n" + - " where to find the applicable terms.\n" + - " \n" + - " Additional terms, permissive or non-permissive, may be stated in the\n" + - " form of a separately written license, or stated as exceptions;\n" + - " the above requirements apply either way.\n" + - " \n" + - " 8. Termination.\n" + - " \n" + - " You may not propagate or modify a covered work except as expressly\n" + - " provided under this License. Any attempt otherwise to propagate or\n" + - " modify it is void, and will automatically terminate your rights under\n" + - " this License (including any patent licenses granted under the third\n" + - " paragraph of section 11).\n" + - " \n" + - " However, if you cease all violation of this License, then your\n" + - " license from a particular copyright holder is reinstated (a)\n" + - " provisionally, unless and until the copyright holder explicitly and\n" + - " finally terminates your license, and (b) permanently, if the copyright\n" + - " holder fails to notify you of the violation by some reasonable means\n" + - " prior to 60 days after the cessation.\n" + - " \n" + - " Moreover, your license from a particular copyright holder is\n" + - " reinstated permanently if the copyright holder notifies you of the\n" + - " violation by some reasonable means, this is the first time you have\n" + - " received notice of violation of this License (for any work) from that\n" + - " copyright holder, and you cure the violation prior to 30 days after\n" + - " your receipt of the notice.\n" + - " \n" + - " Termination of your rights under this section does not terminate the\n" + - " licenses of parties who have received copies or rights from you under\n" + - " this License. If your rights have been terminated and not permanently\n" + - " reinstated, you do not qualify to receive new licenses for the same\n" + - " material under section 10.\n" + - " \n" + - " 9. Acceptance Not Required for Having Copies.\n" + - " \n" + - " You are not required to accept this License in order to receive or\n" + - " run a copy of the Program. Ancillary propagation of a covered work\n" + - " occurring solely as a consequence of using peer-to-peer transmission\n" + - " to receive a copy likewise does not require acceptance. However,\n" + - " nothing other than this License grants you permission to propagate or\n" + - " modify any covered work. These actions infringe copyright if you do\n" + - " not accept this License. Therefore, by modifying or propagating a\n" + - " covered work, you indicate your acceptance of this License to do so.\n" + - " \n" + - " 10. Automatic Licensing of Downstream Recipients.\n" + - " \n" + - " Each time you convey a covered work, the recipient automatically\n" + - " receives a license from the original licensors, to run, modify and\n" + - " propagate that work, subject to this License. You are not responsible\n" + - " for enforcing compliance by third parties with this License.\n" + - " \n" + - " An \"entity transaction\" is a transaction transferring control of an\n" + - " organization, or substantially all assets of one, or subdividing an\n" + - " organization, or merging organizations. If propagation of a covered\n" + - " work results from an entity transaction, each party to that\n" + - " transaction who receives a copy of the work also receives whatever\n" + - " licenses to the work the party's predecessor in interest had or could\n" + - " give under the previous paragraph, plus a right to possession of the\n" + - " Corresponding Source of the work from the predecessor in interest, if\n" + - " the predecessor has it or can get it with reasonable efforts.\n" + - " \n" + - " You may not impose any further restrictions on the exercise of the\n" + - " rights granted or affirmed under this License. For example, you may\n" + - " not impose a license fee, royalty, or other charge for exercise of\n" + - " rights granted under this License, and you may not initiate litigation\n" + - " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + - " any patent claim is infringed by making, using, selling, offering for\n" + - " sale, or importing the Program or any portion of it.\n" + - " \n" + - " 11. Patents.\n" + - " \n" + - " A \"contributor\" is a copyright holder who authorizes use under this\n" + - " License of the Program or a work on which the Program is based. The\n" + - " work thus licensed is called the contributor's \"contributor version\".\n" + - " \n" + - " A contributor's \"essential patent claims\" are all patent claims\n" + - " owned or controlled by the contributor, whether already acquired or\n" + - " hereafter acquired, that would be infringed by some manner, permitted\n" + - " by this License, of making, using, or selling its contributor version,\n" + - " but do not include claims that would be infringed only as a\n" + - " consequence of further modification of the contributor version. For\n" + - " purposes of this definition, \"control\" includes the right to grant\n" + - " patent sublicenses in a manner consistent with the requirements of\n" + - " this License.\n" + - " \n" + - " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + - " patent license under the contributor's essential patent claims, to\n" + - " make, use, sell, offer for sale, import and otherwise run, modify and\n" + - " propagate the contents of its contributor version.\n" + - " \n" + - " In the following three paragraphs, a \"patent license\" is any express\n" + - " agreement or commitment, however denominated, not to enforce a patent\n" + - " (such as an express permission to practice a patent or covenant not to\n" + - " sue for patent infringement). To \"grant\" such a patent license to a\n" + - " party means to make such an agreement or commitment not to enforce a\n" + - " patent against the party.\n" + - " \n" + - " If you convey a covered work, knowingly relying on a patent license,\n" + - " and the Corresponding Source of the work is not available for anyone\n" + - " to copy, free of charge and under the terms of this License, through a\n" + - " publicly available network server or other readily accessible means,\n" + - " then you must either (1) cause the Corresponding Source to be so\n" + - " available, or (2) arrange to deprive yourself of the benefit of the\n" + - " patent license for this particular work, or (3) arrange, in a manner\n" + - " consistent with the requirements of this License, to extend the patent\n" + - " license to downstream recipients. \"Knowingly relying\" means you have\n" + - " actual knowledge that, but for the patent license, your conveying the\n" + - " covered work in a country, or your recipient's use of the covered work\n" + - " in a country, would infringe one or more identifiable patents in that\n" + - " country that you have reason to believe are valid.\n" + - " \n" + - " If, pursuant to or in connection with a single transaction or\n" + - " arrangement, you convey, or propagate by procuring conveyance of, a\n" + - " covered work, and grant a patent license to some of the parties\n" + - " receiving the covered work authorizing them to use, propagate, modify\n" + - " or convey a specific copy of the covered work, then the patent license\n" + - " you grant is automatically extended to all recipients of the covered\n" + - " work and works based on it.\n" + - " \n" + - " A patent license is \"discriminatory\" if it does not include within\n" + - " the scope of its coverage, prohibits the exercise of, or is\n" + - " conditioned on the non-exercise of one or more of the rights that are\n" + - " specifically granted under this License. You may not convey a covered\n" + - " work if you are a party to an arrangement with a third party that is\n" + - " in the business of distributing software, under which you make payment\n" + - " to the third party based on the extent of your activity of conveying\n" + - " the work, and under which the third party grants, to any of the\n" + - " parties who would receive the covered work from you, a discriminatory\n" + - " patent license (a) in connection with copies of the covered work\n" + - " conveyed by you (or copies made from those copies), or (b) primarily\n" + - " for and in connection with specific products or compilations that\n" + - " contain the covered work, unless you entered into that arrangement,\n" + - " or that patent license was granted, prior to 28 March 2007.\n" + - " \n" + - " Nothing in this License shall be construed as excluding or limiting\n" + - " any implied license or other defenses to infringement that may\n" + - " otherwise be available to you under applicable patent law.\n" + - " \n" + - " 12. No Surrender of Others' Freedom.\n" + - " \n" + - " If conditions are imposed on you (whether by court order, agreement or\n" + - " otherwise) that contradict the conditions of this License, they do not\n" + - " excuse you from the conditions of this License. If you cannot convey a\n" + - " covered work so as to satisfy simultaneously your obligations under this\n" + - " License and any other pertinent obligations, then as a consequence you may\n" + - " not convey it at all. For example, if you agree to terms that obligate you\n" + - " to collect a royalty for further conveying from those to whom you convey\n" + - " the Program, the only way you could satisfy both those terms and this\n" + - " License would be to refrain entirely from conveying the Program.\n" + - " \n" + - " 13. Use with the GNU Affero General Public License.\n" + - " \n" + - " Notwithstanding any other provision of this License, you have\n" + - " permission to link or combine any covered work with a work licensed\n" + - " under version 3 of the GNU Affero General Public License into a single\n" + - " combined work, and to convey the resulting work. The terms of this\n" + - " License will continue to apply to the part which is the covered work,\n" + - " but the special requirements of the GNU Affero General Public License,\n" + - " section 13, concerning interaction through a network will apply to the\n" + - " combination as such.\n" + - " \n" + - " 14. Revised Versions of this License.\n" + - " \n" + - " The Free Software Foundation may publish revised and/or new versions of\n" + - " the GNU General Public License from time to time. Such new versions will\n" + - " be similar in spirit to the present version, but may differ in detail to\n" + - " address new problems or concerns.\n" + - " \n" + - " Each version is given a distinguishing version number. If the\n" + - " Program specifies that a certain numbered version of the GNU General\n" + - " Public License \"or any later version\" applies to it, you have the\n" + - " option of following the terms and conditions either of that numbered\n" + - " version or of any later version published by the Free Software\n" + - " Foundation. If the Program does not specify a version number of the\n" + - " GNU General Public License, you may choose any version ever published\n" + - " by the Free Software Foundation.\n" + - " \n" + - " If the Program specifies that a proxy can decide which future\n" + - " versions of the GNU General Public License can be used, that proxy's\n" + - " public statement of acceptance of a version permanently authorizes you\n" + - " to choose that version for the Program.\n" + - " \n" + - " Later license versions may give you additional or different\n" + - " permissions. However, no additional obligations are imposed on any\n" + - " author or copyright holder as a result of your choosing to follow a\n" + - " later version.\n" + - " \n" + - " 15. Disclaimer of Warranty.\n" + - " \n" + - " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + - " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + - " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + - " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + - " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + - " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + - " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + - " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + - " \n" + - " 16. Limitation of Liability.\n" + - " \n" + - " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + - " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + - " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + - " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + - " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + - " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + - " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + - " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + - " SUCH DAMAGES.\n" + - " \n" + - " 17. Interpretation of Sections 15 and 16.\n" + - " \n" + - " If the disclaimer of warranty and limitation of liability provided\n" + - " above cannot be given local legal effect according to their terms,\n" + - " reviewing courts shall apply local law that most closely approximates\n" + - " an absolute waiver of all civil liability in connection with the\n" + - " Program, unless a warranty or assumption of liability accompanies a\n" + - " copy of the Program in return for a fee.\n" + - " \n" + - " END OF TERMS AND CONDITIONS\n" + - " \n" + - " How to Apply These Terms to Your New Programs\n" + - " \n" + - " If you develop a new program, and you want it to be of the greatest\n" + - " possible use to the public, the best way to achieve this is to make it\n" + - " free software which everyone can redistribute and change under these terms.\n" + - " \n" + - " To do so, attach the following notices to the program. It is safest\n" + - " to attach them to the start of each source file to most effectively\n" + - " state the exclusion of warranty; and each file should have at least\n" + - " the \"copyright\" line and a pointer to where the full notice is found.\n" + - " \n" + - " \n" + - " Copyright (C) \n" + - " \n" + - " This program is free software: you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation, either version 3 of the License, or\n" + - " (at your option) any later version.\n" + - " \n" + - " This program is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " You should have received a copy of the GNU General Public License\n" + - " along with this program. If not, see .\n" + - " \n" + - " Also add information on how to contact you by electronic and paper mail.\n" + - " \n" + - " If the program does terminal interaction, make it output a short\n" + - " notice like this when it starts in an interactive mode:\n" + - " \n" + - " Copyright (C) \n" + - " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + - " This is free software, and you are welcome to redistribute it\n" + - " under certain conditions; type `show c' for details.\n" + - " \n" + - " The hypothetical commands `show w' and `show c' should show the appropriate\n" + - " parts of the General Public License. Of course, your program's commands\n" + - " might be different; for a GUI interface, you would use an \"about box\".\n" + - " \n" + - " You should also get your employer (if you work as a programmer) or school,\n" + - " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + - " For more information on this, and how to apply and follow the GNU GPL, see\n" + - " .\n" + - " \n" + - " The GNU General Public License does not permit incorporating your program\n" + - " into proprietary programs. If your program is a subroutine library, you\n" + - " may consider it more useful to permit linking proprietary applications with\n" + - " the library. If this is what you want to do, use the GNU Lesser General\n" + - " Public License instead of this License. But first, please read\n" + - " .\n" + - " \n" + - " \n" + - " Name: libquadmath\n" + - " Files: scipy/.dylibs/libquadmath*.so\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + - " License: LGPL-2.1-or-later\n" + - " \n" + - " GCC Quad-Precision Math Library\n" + - " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + - " Written by Francois-Xavier Coudert \n" + - " \n" + - " This file is part of the libquadmath library.\n" + - " Libquadmath is free software; you can redistribute it and/or\n" + - " modify it under the terms of the GNU Library General Public\n" + - " License as published by the Free Software Foundation; either\n" + - " version 2.1 of the License, or (at your option) any later version.\n" + - " \n" + - " Libquadmath is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + - " Lesser General Public License for more details.\n" + - " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: numpy\n" + - "Required-by: gensim\n" + - "---\n" + - "Name: six\n" + - "Version: 1.16.0\n" + - "Summary: Python 2 and 3 compatibility utilities\n" + - "Home-page: https://github.com/benjaminp/six\n" + - "Author: Benjamin Peterson\n" + - "Author-email: benjamin@python.org\n" + - "License: MIT\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: \n" + - "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"; + PIP_SHOW_LINES = + "Name: altgraph\n" + + "Version: 0.17.2\n" + + "Summary: Python graph (network) package\n" + + "Home-page: https://altgraph.readthedocs.io\n" + + "Author: Ronald Oussoren\n" + + "Author-email: ronaldoussoren@mac.com\n" + + "License: MIT\n" + + "Location:" + + " /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + + "Requires: \n" + + "Required-by: macholib\n" + + "---\n" + + "Name: scipy\n" + + "Version: 1.11.3\n" + + "Summary: Fundamental algorithms for scientific computing in Python\n" + + "Home-page: https://scipy.org/\n" + + "Author: \n" + + "Author-email: \n" + + "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions\n" + + " are met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above\n" + + " copyright notice, this list of conditions and the following\n" + + " disclaimer in the documentation and/or other materials provided\n" + + " with the distribution.\n" + + " \n" + + " 3. Neither the name of the copyright holder nor the names of its\n" + + " contributors may be used to endorse or promote products derived\n" + + " from this software without specific prior written permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " ----\n" + + " \n" + + " This binary distribution of SciPy also bundles the following software:\n" + + " \n" + + " \n" + + " Name: OpenBLAS\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled as a dynamically linked library\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 2011-2014, The OpenBLAS Project\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer in\n" + + " the documentation and/or other materials provided with the\n" + + " distribution.\n" + + " 3. Neither the name of the OpenBLAS project nor the names of\n" + + " its contributors may be used to endorse or promote products\n" + + " derived from this software without specific prior written\n" + + " permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS" + + " IS\"\n" + + " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO," + + " THE\n" + + " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR" + + " PURPOSE\n" + + " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + + " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR" + + " CONSEQUENTIAL\n" + + " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS" + + " OR\n" + + " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)" + + " HOWEVER\n" + + " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT" + + " LIABILITY,\n" + + " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF" + + " THE\n" + + " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: LAPACK\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled in OpenBLAS\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + + " of Tennessee Research Foundation. All rights\n" + + " reserved.\n" + + " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + + " rights reserved.\n" + + " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + + " reserved.\n" + + " \n" + + " $COPYRIGHT$\n" + + " \n" + + " Additional copyrights may follow\n" + + " \n" + + " $HEADER$\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " - Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " - Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer listed\n" + + " in this license in the documentation and/or other materials\n" + + " provided with the distribution.\n" + + " \n" + + " - Neither the name of the copyright holders nor the names of its\n" + + " contributors may be used to endorse or promote products derived from\n" + + " this software without specific prior written permission.\n" + + " \n" + + " The copyright holders provide no reassurances that the source code\n" + + " provided does not infringe any patent, copyright, or any other\n" + + " intellectual property rights of third parties. The copyright holders\n" + + " disclaim any liability to any recipient for claims brought against\n" + + " recipient by any third party for infringement of that parties\n" + + " intellectual property rights.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: GCC runtime library\n" + + " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + + " License: GPL-3.0-with-GCC-exception\n" + + " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + + " \n" + + " Libgfortran is free software; you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation; either version 3, or (at your option)\n" + + " any later version.\n" + + " \n" + + " Libgfortran is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " Under Section 7 of GPL version 3, you are granted additional\n" + + " permissions described in the GCC Runtime Library Exception, version\n" + + " 3.1, as published by the Free Software Foundation.\n" + + " \n" + + " You should have received a copy of the GNU General Public License and\n" + + " a copy of the GCC Runtime Library Exception along with this program;\n" + + " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + + " .\n" + + " \n" + + " ----\n" + + " \n" + + " Full text of license texts referred to above follows (that they are\n" + + " listed below does not necessarily imply the conditions apply to the\n" + + " present binary release):\n" + + " \n" + + " ----\n" + + " \n" + + " GCC RUNTIME LIBRARY EXCEPTION\n" + + " \n" + + " Version 3.1, 31 March 2009\n" + + " \n" + + " Copyright (C) 2009 Free Software Foundation, Inc. \n" + + " \n" + + " Everyone is permitted to copy and distribute verbatim copies of this\n" + + " license document, but changing it is not allowed.\n" + + " \n" + + " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + + " permission under section 7 of the GNU General Public License, version\n" + + " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + + " bears a notice placed by the copyright holder of the file stating that\n" + + " the file is governed by GPLv3 along with this Exception.\n" + + " \n" + + " When you use GCC to compile a program, GCC may combine portions of\n" + + " certain GCC header files and runtime libraries with the compiled\n" + + " program. The purpose of this Exception is to allow compilation of\n" + + " non-GPL (including proprietary) programs to use, in this way, the\n" + + " header files and runtime libraries covered by this Exception.\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " A file is an \"Independent Module\" if it either requires the Runtime\n" + + " Library for execution after a Compilation Process, or makes use of an\n" + + " interface provided by the Runtime Library, but is not otherwise based\n" + + " on the Runtime Library.\n" + + " \n" + + " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + + " modifications, governed by version 3 (or a specified later version) of\n" + + " the GNU General Public License (GPL) with the option of using any\n" + + " subsequent versions published by the FSF.\n" + + " \n" + + " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + + " modification and use would permit combination with GCC in accord with\n" + + " the license of GCC.\n" + + " \n" + + " \"Target Code\" refers to output from any compiler for a real or virtual\n" + + " target processor architecture, in executable form or suitable for\n" + + " input to an assembler, loader, linker and/or execution\n" + + " phase. Notwithstanding that, Target Code does not include data in any\n" + + " format that is used as a compiler intermediate representation, or used\n" + + " for producing a compiler intermediate representation.\n" + + " \n" + + " The \"Compilation Process\" transforms code entirely represented in\n" + + " non-intermediate languages designed for human-written code, and/or in\n" + + " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + + " use of source code generators and preprocessors need not be considered\n" + + " part of the Compilation Process, since the Compilation Process can be\n" + + " understood as starting with the output of the generators or\n" + + " preprocessors.\n" + + " \n" + + " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + + " with other GPL-compatible software, or if it is done without using any\n" + + " work based on GCC. For example, using non-GPL-compatible Software to\n" + + " optimize any GCC intermediate representations would not qualify as an\n" + + " Eligible Compilation Process.\n" + + " \n" + + " 1. Grant of Additional Permission.\n" + + " \n" + + " You have permission to propagate a work of Target Code formed by\n" + + " combining the Runtime Library with Independent Modules, even if such\n" + + " propagation would otherwise violate the terms of GPLv3, provided that\n" + + " all Target Code was generated by Eligible Compilation Processes. You\n" + + " may then convey such a combination under terms of your choice,\n" + + " consistent with the licensing of the Independent Modules.\n" + + " \n" + + " 2. No Weakening of GCC Copyleft.\n" + + " \n" + + " The availability of this Exception does not imply any general\n" + + " presumption that third-party software is unaffected by the copyleft\n" + + " requirements of the license of GCC.\n" + + " \n" + + " ----\n" + + " \n" + + " GNU GENERAL PUBLIC LICENSE\n" + + " Version 3, 29 June 2007\n" + + " \n" + + " Copyright (C) 2007 Free Software Foundation, Inc. \n" + + " Everyone is permitted to copy and distribute verbatim copies\n" + + " of this license document, but changing it is not allowed.\n" + + " \n" + + " Preamble\n" + + " \n" + + " The GNU General Public License is a free, copyleft license for\n" + + " software and other kinds of works.\n" + + " \n" + + " The licenses for most software and other practical works are designed\n" + + " to take away your freedom to share and change the works. By contrast,\n" + + " the GNU General Public License is intended to guarantee your freedom to\n" + + " share and change all versions of a program--to make sure it remains free\n" + + " software for all its users. We, the Free Software Foundation, use the\n" + + " GNU General Public License for most of our software; it applies also to\n" + + " any other work released this way by its authors. You can apply it to\n" + + " your programs, too.\n" + + " \n" + + " When we speak of free software, we are referring to freedom, not\n" + + " price. Our General Public Licenses are designed to make sure that you\n" + + " have the freedom to distribute copies of free software (and charge for\n" + + " them if you wish), that you receive source code or can get it if you\n" + + " want it, that you can change the software or use pieces of it in new\n" + + " free programs, and that you know you can do these things.\n" + + " \n" + + " To protect your rights, we need to prevent others from denying you\n" + + " these rights or asking you to surrender the rights. Therefore, you have\n" + + " certain responsibilities if you distribute copies of the software, or if\n" + + " you modify it: responsibilities to respect the freedom of others.\n" + + " \n" + + " For example, if you distribute copies of such a program, whether\n" + + " gratis or for a fee, you must pass on to the recipients the same\n" + + " freedoms that you received. You must make sure that they, too, receive\n" + + " or can get the source code. And you must show them these terms so they\n" + + " know their rights.\n" + + " \n" + + " Developers that use the GNU GPL protect your rights with two steps:\n" + + " (1) assert copyright on the software, and (2) offer you this License\n" + + " giving you legal permission to copy, distribute and/or modify it.\n" + + " \n" + + " For the developers' and authors' protection, the GPL clearly explains\n" + + " that there is no warranty for this free software. For both users' and\n" + + " authors' sake, the GPL requires that modified versions be marked as\n" + + " changed, so that their problems will not be attributed erroneously to\n" + + " authors of previous versions.\n" + + " \n" + + " Some devices are designed to deny users access to install or run\n" + + " modified versions of the software inside them, although the manufacturer\n" + + " can do so. This is fundamentally incompatible with the aim of\n" + + " protecting users' freedom to change the software. The systematic\n" + + " pattern of such abuse occurs in the area of products for individuals to\n" + + " use, which is precisely where it is most unacceptable. Therefore, we\n" + + " have designed this version of the GPL to prohibit the practice for those\n" + + " products. If such problems arise substantially in other domains, we\n" + + " stand ready to extend this provision to those domains in future versions\n" + + " of the GPL, as needed to protect the freedom of users.\n" + + " \n" + + " Finally, every program is threatened constantly by software patents.\n" + + " States should not allow patents to restrict development and use of\n" + + " software on general-purpose computers, but in those that do, we wish to\n" + + " avoid the special danger that patents applied to a free program could\n" + + " make it effectively proprietary. To prevent this, the GPL assures that\n" + + " patents cannot be used to render the program non-free.\n" + + " \n" + + " The precise terms and conditions for copying, distribution and\n" + + " modification follow.\n" + + " \n" + + " TERMS AND CONDITIONS\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " \"This License\" refers to version 3 of the GNU General Public License.\n" + + " \n" + + " \"Copyright\" also means copyright-like laws that apply to other kinds" + + " of\n" + + " works, such as semiconductor masks.\n" + + " \n" + + " \"The Program\" refers to any copyrightable work licensed under this\n" + + " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + + " \"recipients\" may be individuals or organizations.\n" + + " \n" + + " To \"modify\" a work means to copy from or adapt all or part of the work\n" + + " in a fashion requiring copyright permission, other than the making of an\n" + + " exact copy. The resulting work is called a \"modified version\" of the\n" + + " earlier work or a work \"based on\" the earlier work.\n" + + " \n" + + " A \"covered work\" means either the unmodified Program or a work based\n" + + " on the Program.\n" + + " \n" + + " To \"propagate\" a work means to do anything with it that, without\n" + + " permission, would make you directly or secondarily liable for\n" + + " infringement under applicable copyright law, except executing it on a\n" + + " computer or modifying a private copy. Propagation includes copying,\n" + + " distribution (with or without modification), making available to the\n" + + " public, and in some countries other activities as well.\n" + + " \n" + + " To \"convey\" a work means any kind of propagation that enables other\n" + + " parties to make or receive copies. Mere interaction with a user through\n" + + " a computer network, with no transfer of a copy, is not conveying.\n" + + " \n" + + " An interactive user interface displays \"Appropriate Legal Notices\"\n" + + " to the extent that it includes a convenient and prominently visible\n" + + " feature that (1) displays an appropriate copyright notice, and (2)\n" + + " tells the user that there is no warranty for the work (except to the\n" + + " extent that warranties are provided), that licensees may convey the\n" + + " work under this License, and how to view a copy of this License. If\n" + + " the interface presents a list of user commands or options, such as a\n" + + " menu, a prominent item in the list meets this criterion.\n" + + " \n" + + " 1. Source Code.\n" + + " \n" + + " The \"source code\" for a work means the preferred form of the work\n" + + " for making modifications to it. \"Object code\" means any non-source\n" + + " form of a work.\n" + + " \n" + + " A \"Standard Interface\" means an interface that either is an official\n" + + " standard defined by a recognized standards body, or, in the case of\n" + + " interfaces specified for a particular programming language, one that\n" + + " is widely used among developers working in that language.\n" + + " \n" + + " The \"System Libraries\" of an executable work include anything, other\n" + + " than the work as a whole, that (a) is included in the normal form of\n" + + " packaging a Major Component, but which is not part of that Major\n" + + " Component, and (b) serves only to enable use of the work with that\n" + + " Major Component, or to implement a Standard Interface for which an\n" + + " implementation is available to the public in source code form. A\n" + + " \"Major Component\", in this context, means a major essential component\n" + + " (kernel, window system, and so on) of the specific operating system\n" + + " (if any) on which the executable work runs, or a compiler used to\n" + + " produce the work, or an object code interpreter used to run it.\n" + + " \n" + + " The \"Corresponding Source\" for a work in object code form means all\n" + + " the source code needed to generate, install, and (for an executable\n" + + " work) run the object code and to modify the work, including scripts to\n" + + " control those activities. However, it does not include the work's\n" + + " System Libraries, or general-purpose tools or generally available free\n" + + " programs which are used unmodified in performing those activities but\n" + + " which are not part of the work. For example, Corresponding Source\n" + + " includes interface definition files associated with source files for\n" + + " the work, and the source code for shared libraries and dynamically\n" + + " linked subprograms that the work is specifically designed to require,\n" + + " such as by intimate data communication or control flow between those\n" + + " subprograms and other parts of the work.\n" + + " \n" + + " The Corresponding Source need not include anything that users\n" + + " can regenerate automatically from other parts of the Corresponding\n" + + " Source.\n" + + " \n" + + " The Corresponding Source for a work in source code form is that\n" + + " same work.\n" + + " \n" + + " 2. Basic Permissions.\n" + + " \n" + + " All rights granted under this License are granted for the term of\n" + + " copyright on the Program, and are irrevocable provided the stated\n" + + " conditions are met. This License explicitly affirms your unlimited\n" + + " permission to run the unmodified Program. The output from running a\n" + + " covered work is covered by this License only if the output, given its\n" + + " content, constitutes a covered work. This License acknowledges your\n" + + " rights of fair use or other equivalent, as provided by copyright law.\n" + + " \n" + + " You may make, run and propagate covered works that you do not\n" + + " convey, without conditions so long as your license otherwise remains\n" + + " in force. You may convey covered works to others for the sole purpose\n" + + " of having them make modifications exclusively for you, or provide you\n" + + " with facilities for running those works, provided that you comply with\n" + + " the terms of this License in conveying all material for which you do\n" + + " not control copyright. Those thus making or running the covered works\n" + + " for you must do so exclusively on your behalf, under your direction\n" + + " and control, on terms that prohibit them from making any copies of\n" + + " your copyrighted material outside their relationship with you.\n" + + " \n" + + " Conveying under any other circumstances is permitted solely under\n" + + " the conditions stated below. Sublicensing is not allowed; section 10\n" + + " makes it unnecessary.\n" + + " \n" + + " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + + " \n" + + " No covered work shall be deemed part of an effective technological\n" + + " measure under any applicable law fulfilling obligations under article\n" + + " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + + " similar laws prohibiting or restricting circumvention of such\n" + + " measures.\n" + + " \n" + + " When you convey a covered work, you waive any legal power to forbid\n" + + " circumvention of technological measures to the extent such circumvention\n" + + " is effected by exercising rights under this License with respect to\n" + + " the covered work, and you disclaim any intention to limit operation or\n" + + " modification of the work as a means of enforcing, against the work's\n" + + " users, your or third parties' legal rights to forbid circumvention of\n" + + " technological measures.\n" + + " \n" + + " 4. Conveying Verbatim Copies.\n" + + " \n" + + " You may convey verbatim copies of the Program's source code as you\n" + + " receive it, in any medium, provided that you conspicuously and\n" + + " appropriately publish on each copy an appropriate copyright notice;\n" + + " keep intact all notices stating that this License and any\n" + + " non-permissive terms added in accord with section 7 apply to the code;\n" + + " keep intact all notices of the absence of any warranty; and give all\n" + + " recipients a copy of this License along with the Program.\n" + + " \n" + + " You may charge any price or no price for each copy that you convey,\n" + + " and you may offer support or warranty protection for a fee.\n" + + " \n" + + " 5. Conveying Modified Source Versions.\n" + + " \n" + + " You may convey a work based on the Program, or the modifications to\n" + + " produce it from the Program, in the form of source code under the\n" + + " terms of section 4, provided that you also meet all of these conditions:\n" + + " \n" + + " a) The work must carry prominent notices stating that you modified\n" + + " it, and giving a relevant date.\n" + + " \n" + + " b) The work must carry prominent notices stating that it is\n" + + " released under this License and any conditions added under section\n" + + " 7. This requirement modifies the requirement in section 4 to\n" + + " \"keep intact all notices\".\n" + + " \n" + + " c) You must license the entire work, as a whole, under this\n" + + " License to anyone who comes into possession of a copy. This\n" + + " License will therefore apply, along with any applicable section 7\n" + + " additional terms, to the whole of the work, and all its parts,\n" + + " regardless of how they are packaged. This License gives no\n" + + " permission to license the work in any other way, but it does not\n" + + " invalidate such permission if you have separately received it.\n" + + " \n" + + " d) If the work has interactive user interfaces, each must display\n" + + " Appropriate Legal Notices; however, if the Program has interactive\n" + + " interfaces that do not display Appropriate Legal Notices, your\n" + + " work need not make them do so.\n" + + " \n" + + " A compilation of a covered work with other separate and independent\n" + + " works, which are not by their nature extensions of the covered work,\n" + + " and which are not combined with it such as to form a larger program,\n" + + " in or on a volume of a storage or distribution medium, is called an\n" + + " \"aggregate\" if the compilation and its resulting copyright are not\n" + + " used to limit the access or legal rights of the compilation's users\n" + + " beyond what the individual works permit. Inclusion of a covered work\n" + + " in an aggregate does not cause this License to apply to the other\n" + + " parts of the aggregate.\n" + + " \n" + + " 6. Conveying Non-Source Forms.\n" + + " \n" + + " You may convey a covered work in object code form under the terms\n" + + " of sections 4 and 5, provided that you also convey the\n" + + " machine-readable Corresponding Source under the terms of this License,\n" + + " in one of these ways:\n" + + " \n" + + " a) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by the\n" + + " Corresponding Source fixed on a durable physical medium\n" + + " customarily used for software interchange.\n" + + " \n" + + " b) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by a\n" + + " written offer, valid for at least three years and valid for as\n" + + " long as you offer spare parts or customer support for that product\n" + + " model, to give anyone who possesses the object code either (1) a\n" + + " copy of the Corresponding Source for all the software in the\n" + + " product that is covered by this License, on a durable physical\n" + + " medium customarily used for software interchange, for a price no\n" + + " more than your reasonable cost of physically performing this\n" + + " conveying of source, or (2) access to copy the\n" + + " Corresponding Source from a network server at no charge.\n" + + " \n" + + " c) Convey individual copies of the object code with a copy of the\n" + + " written offer to provide the Corresponding Source. This\n" + + " alternative is allowed only occasionally and noncommercially, and\n" + + " only if you received the object code with such an offer, in accord\n" + + " with subsection 6b.\n" + + " \n" + + " d) Convey the object code by offering access from a designated\n" + + " place (gratis or for a charge), and offer equivalent access to the\n" + + " Corresponding Source in the same way through the same place at no\n" + + " further charge. You need not require recipients to copy the\n" + + " Corresponding Source along with the object code. If the place to\n" + + " copy the object code is a network server, the Corresponding Source\n" + + " may be on a different server (operated by you or a third party)\n" + + " that supports equivalent copying facilities, provided you maintain\n" + + " clear directions next to the object code saying where to find the\n" + + " Corresponding Source. Regardless of what server hosts the\n" + + " Corresponding Source, you remain obligated to ensure that it is\n" + + " available for as long as needed to satisfy these requirements.\n" + + " \n" + + " e) Convey the object code using peer-to-peer transmission, provided\n" + + " you inform other peers where the object code and Corresponding\n" + + " Source of the work are being offered to the general public at no\n" + + " charge under subsection 6d.\n" + + " \n" + + " A separable portion of the object code, whose source code is excluded\n" + + " from the Corresponding Source as a System Library, need not be\n" + + " included in conveying the object code work.\n" + + " \n" + + " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + + " tangible personal property which is normally used for personal, family,\n" + + " or household purposes, or (2) anything designed or sold for incorporation\n" + + " into a dwelling. In determining whether a product is a consumer product,\n" + + " doubtful cases shall be resolved in favor of coverage. For a particular\n" + + " product received by a particular user, \"normally used\" refers to a\n" + + " typical or common use of that class of product, regardless of the status\n" + + " of the particular user or of the way in which the particular user\n" + + " actually uses, or expects or is expected to use, the product. A product\n" + + " is a consumer product regardless of whether the product has substantial\n" + + " commercial, industrial or non-consumer uses, unless such uses represent\n" + + " the only significant mode of use of the product.\n" + + " \n" + + " \"Installation Information\" for a User Product means any methods,\n" + + " procedures, authorization keys, or other information required to install\n" + + " and execute modified versions of a covered work in that User Product from\n" + + " a modified version of its Corresponding Source. The information must\n" + + " suffice to ensure that the continued functioning of the modified object\n" + + " code is in no case prevented or interfered with solely because\n" + + " modification has been made.\n" + + " \n" + + " If you convey an object code work under this section in, or with, or\n" + + " specifically for use in, a User Product, and the conveying occurs as\n" + + " part of a transaction in which the right of possession and use of the\n" + + " User Product is transferred to the recipient in perpetuity or for a\n" + + " fixed term (regardless of how the transaction is characterized), the\n" + + " Corresponding Source conveyed under this section must be accompanied\n" + + " by the Installation Information. But this requirement does not apply\n" + + " if neither you nor any third party retains the ability to install\n" + + " modified object code on the User Product (for example, the work has\n" + + " been installed in ROM).\n" + + " \n" + + " The requirement to provide Installation Information does not include a\n" + + " requirement to continue to provide support service, warranty, or updates\n" + + " for a work that has been modified or installed by the recipient, or for\n" + + " the User Product in which it has been modified or installed. Access to a\n" + + " network may be denied when the modification itself materially and\n" + + " adversely affects the operation of the network or violates the rules and\n" + + " protocols for communication across the network.\n" + + " \n" + + " Corresponding Source conveyed, and Installation Information provided,\n" + + " in accord with this section must be in a format that is publicly\n" + + " documented (and with an implementation available to the public in\n" + + " source code form), and must require no special password or key for\n" + + " unpacking, reading or copying.\n" + + " \n" + + " 7. Additional Terms.\n" + + " \n" + + " \"Additional permissions\" are terms that supplement the terms of this\n" + + " License by making exceptions from one or more of its conditions.\n" + + " Additional permissions that are applicable to the entire Program shall\n" + + " be treated as though they were included in this License, to the extent\n" + + " that they are valid under applicable law. If additional permissions\n" + + " apply only to part of the Program, that part may be used separately\n" + + " under those permissions, but the entire Program remains governed by\n" + + " this License without regard to the additional permissions.\n" + + " \n" + + " When you convey a copy of a covered work, you may at your option\n" + + " remove any additional permissions from that copy, or from any part of\n" + + " it. (Additional permissions may be written to require their own\n" + + " removal in certain cases when you modify the work.) You may place\n" + + " additional permissions on material, added by you to a covered work,\n" + + " for which you have or can give appropriate copyright permission.\n" + + " \n" + + " Notwithstanding any other provision of this License, for material you\n" + + " add to a covered work, you may (if authorized by the copyright holders of\n" + + " that material) supplement the terms of this License with terms:\n" + + " \n" + + " a) Disclaiming warranty or limiting liability differently from the\n" + + " terms of sections 15 and 16 of this License; or\n" + + " \n" + + " b) Requiring preservation of specified reasonable legal notices or\n" + + " author attributions in that material or in the Appropriate Legal\n" + + " Notices displayed by works containing it; or\n" + + " \n" + + " c) Prohibiting misrepresentation of the origin of that material, or\n" + + " requiring that modified versions of such material be marked in\n" + + " reasonable ways as different from the original version; or\n" + + " \n" + + " d) Limiting the use for publicity purposes of names of licensors or\n" + + " authors of the material; or\n" + + " \n" + + " e) Declining to grant rights under trademark law for use of some\n" + + " trade names, trademarks, or service marks; or\n" + + " \n" + + " f) Requiring indemnification of licensors and authors of that\n" + + " material by anyone who conveys the material (or modified versions of\n" + + " it) with contractual assumptions of liability to the recipient, for\n" + + " any liability that these contractual assumptions directly impose on\n" + + " those licensors and authors.\n" + + " \n" + + " All other non-permissive additional terms are considered \"further\n" + + " restrictions\" within the meaning of section 10. If the Program as you\n" + + " received it, or any part of it, contains a notice stating that it is\n" + + " governed by this License along with a term that is a further\n" + + " restriction, you may remove that term. If a license document contains\n" + + " a further restriction but permits relicensing or conveying under this\n" + + " License, you may add to a covered work material governed by the terms\n" + + " of that license document, provided that the further restriction does\n" + + " not survive such relicensing or conveying.\n" + + " \n" + + " If you add terms to a covered work in accord with this section, you\n" + + " must place, in the relevant source files, a statement of the\n" + + " additional terms that apply to those files, or a notice indicating\n" + + " where to find the applicable terms.\n" + + " \n" + + " Additional terms, permissive or non-permissive, may be stated in the\n" + + " form of a separately written license, or stated as exceptions;\n" + + " the above requirements apply either way.\n" + + " \n" + + " 8. Termination.\n" + + " \n" + + " You may not propagate or modify a covered work except as expressly\n" + + " provided under this License. Any attempt otherwise to propagate or\n" + + " modify it is void, and will automatically terminate your rights under\n" + + " this License (including any patent licenses granted under the third\n" + + " paragraph of section 11).\n" + + " \n" + + " However, if you cease all violation of this License, then your\n" + + " license from a particular copyright holder is reinstated (a)\n" + + " provisionally, unless and until the copyright holder explicitly and\n" + + " finally terminates your license, and (b) permanently, if the copyright\n" + + " holder fails to notify you of the violation by some reasonable means\n" + + " prior to 60 days after the cessation.\n" + + " \n" + + " Moreover, your license from a particular copyright holder is\n" + + " reinstated permanently if the copyright holder notifies you of the\n" + + " violation by some reasonable means, this is the first time you have\n" + + " received notice of violation of this License (for any work) from that\n" + + " copyright holder, and you cure the violation prior to 30 days after\n" + + " your receipt of the notice.\n" + + " \n" + + " Termination of your rights under this section does not terminate the\n" + + " licenses of parties who have received copies or rights from you under\n" + + " this License. If your rights have been terminated and not permanently\n" + + " reinstated, you do not qualify to receive new licenses for the same\n" + + " material under section 10.\n" + + " \n" + + " 9. Acceptance Not Required for Having Copies.\n" + + " \n" + + " You are not required to accept this License in order to receive or\n" + + " run a copy of the Program. Ancillary propagation of a covered work\n" + + " occurring solely as a consequence of using peer-to-peer transmission\n" + + " to receive a copy likewise does not require acceptance. However,\n" + + " nothing other than this License grants you permission to propagate or\n" + + " modify any covered work. These actions infringe copyright if you do\n" + + " not accept this License. Therefore, by modifying or propagating a\n" + + " covered work, you indicate your acceptance of this License to do so.\n" + + " \n" + + " 10. Automatic Licensing of Downstream Recipients.\n" + + " \n" + + " Each time you convey a covered work, the recipient automatically\n" + + " receives a license from the original licensors, to run, modify and\n" + + " propagate that work, subject to this License. You are not responsible\n" + + " for enforcing compliance by third parties with this License.\n" + + " \n" + + " An \"entity transaction\" is a transaction transferring control of an\n" + + " organization, or substantially all assets of one, or subdividing an\n" + + " organization, or merging organizations. If propagation of a covered\n" + + " work results from an entity transaction, each party to that\n" + + " transaction who receives a copy of the work also receives whatever\n" + + " licenses to the work the party's predecessor in interest had or could\n" + + " give under the previous paragraph, plus a right to possession of the\n" + + " Corresponding Source of the work from the predecessor in interest, if\n" + + " the predecessor has it or can get it with reasonable efforts.\n" + + " \n" + + " You may not impose any further restrictions on the exercise of the\n" + + " rights granted or affirmed under this License. For example, you may\n" + + " not impose a license fee, royalty, or other charge for exercise of\n" + + " rights granted under this License, and you may not initiate litigation\n" + + " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + + " any patent claim is infringed by making, using, selling, offering for\n" + + " sale, or importing the Program or any portion of it.\n" + + " \n" + + " 11. Patents.\n" + + " \n" + + " A \"contributor\" is a copyright holder who authorizes use under this\n" + + " License of the Program or a work on which the Program is based. The\n" + + " work thus licensed is called the contributor's \"contributor version\".\n" + + " \n" + + " A contributor's \"essential patent claims\" are all patent claims\n" + + " owned or controlled by the contributor, whether already acquired or\n" + + " hereafter acquired, that would be infringed by some manner, permitted\n" + + " by this License, of making, using, or selling its contributor version,\n" + + " but do not include claims that would be infringed only as a\n" + + " consequence of further modification of the contributor version. For\n" + + " purposes of this definition, \"control\" includes the right to grant\n" + + " patent sublicenses in a manner consistent with the requirements of\n" + + " this License.\n" + + " \n" + + " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + + " patent license under the contributor's essential patent claims, to\n" + + " make, use, sell, offer for sale, import and otherwise run, modify and\n" + + " propagate the contents of its contributor version.\n" + + " \n" + + " In the following three paragraphs, a \"patent license\" is any express\n" + + " agreement or commitment, however denominated, not to enforce a patent\n" + + " (such as an express permission to practice a patent or covenant not to\n" + + " sue for patent infringement). To \"grant\" such a patent license to a\n" + + " party means to make such an agreement or commitment not to enforce a\n" + + " patent against the party.\n" + + " \n" + + " If you convey a covered work, knowingly relying on a patent license,\n" + + " and the Corresponding Source of the work is not available for anyone\n" + + " to copy, free of charge and under the terms of this License, through a\n" + + " publicly available network server or other readily accessible means,\n" + + " then you must either (1) cause the Corresponding Source to be so\n" + + " available, or (2) arrange to deprive yourself of the benefit of the\n" + + " patent license for this particular work, or (3) arrange, in a manner\n" + + " consistent with the requirements of this License, to extend the patent\n" + + " license to downstream recipients. \"Knowingly relying\" means you have\n" + + " actual knowledge that, but for the patent license, your conveying the\n" + + " covered work in a country, or your recipient's use of the covered work\n" + + " in a country, would infringe one or more identifiable patents in that\n" + + " country that you have reason to believe are valid.\n" + + " \n" + + " If, pursuant to or in connection with a single transaction or\n" + + " arrangement, you convey, or propagate by procuring conveyance of, a\n" + + " covered work, and grant a patent license to some of the parties\n" + + " receiving the covered work authorizing them to use, propagate, modify\n" + + " or convey a specific copy of the covered work, then the patent license\n" + + " you grant is automatically extended to all recipients of the covered\n" + + " work and works based on it.\n" + + " \n" + + " A patent license is \"discriminatory\" if it does not include within\n" + + " the scope of its coverage, prohibits the exercise of, or is\n" + + " conditioned on the non-exercise of one or more of the rights that are\n" + + " specifically granted under this License. You may not convey a covered\n" + + " work if you are a party to an arrangement with a third party that is\n" + + " in the business of distributing software, under which you make payment\n" + + " to the third party based on the extent of your activity of conveying\n" + + " the work, and under which the third party grants, to any of the\n" + + " parties who would receive the covered work from you, a discriminatory\n" + + " patent license (a) in connection with copies of the covered work\n" + + " conveyed by you (or copies made from those copies), or (b) primarily\n" + + " for and in connection with specific products or compilations that\n" + + " contain the covered work, unless you entered into that arrangement,\n" + + " or that patent license was granted, prior to 28 March 2007.\n" + + " \n" + + " Nothing in this License shall be construed as excluding or limiting\n" + + " any implied license or other defenses to infringement that may\n" + + " otherwise be available to you under applicable patent law.\n" + + " \n" + + " 12. No Surrender of Others' Freedom.\n" + + " \n" + + " If conditions are imposed on you (whether by court order, agreement or\n" + + " otherwise) that contradict the conditions of this License, they do not\n" + + " excuse you from the conditions of this License. If you cannot convey a\n" + + " covered work so as to satisfy simultaneously your obligations under this\n" + + " License and any other pertinent obligations, then as a consequence you may\n" + + " not convey it at all. For example, if you agree to terms that obligate" + + " you\n" + + " to collect a royalty for further conveying from those to whom you convey\n" + + " the Program, the only way you could satisfy both those terms and this\n" + + " License would be to refrain entirely from conveying the Program.\n" + + " \n" + + " 13. Use with the GNU Affero General Public License.\n" + + " \n" + + " Notwithstanding any other provision of this License, you have\n" + + " permission to link or combine any covered work with a work licensed\n" + + " under version 3 of the GNU Affero General Public License into a single\n" + + " combined work, and to convey the resulting work. The terms of this\n" + + " License will continue to apply to the part which is the covered work,\n" + + " but the special requirements of the GNU Affero General Public License,\n" + + " section 13, concerning interaction through a network will apply to the\n" + + " combination as such.\n" + + " \n" + + " 14. Revised Versions of this License.\n" + + " \n" + + " The Free Software Foundation may publish revised and/or new versions of\n" + + " the GNU General Public License from time to time. Such new versions will\n" + + " be similar in spirit to the present version, but may differ in detail to\n" + + " address new problems or concerns.\n" + + " \n" + + " Each version is given a distinguishing version number. If the\n" + + " Program specifies that a certain numbered version of the GNU General\n" + + " Public License \"or any later version\" applies to it, you have the\n" + + " option of following the terms and conditions either of that numbered\n" + + " version or of any later version published by the Free Software\n" + + " Foundation. If the Program does not specify a version number of the\n" + + " GNU General Public License, you may choose any version ever published\n" + + " by the Free Software Foundation.\n" + + " \n" + + " If the Program specifies that a proxy can decide which future\n" + + " versions of the GNU General Public License can be used, that proxy's\n" + + " public statement of acceptance of a version permanently authorizes you\n" + + " to choose that version for the Program.\n" + + " \n" + + " Later license versions may give you additional or different\n" + + " permissions. However, no additional obligations are imposed on any\n" + + " author or copyright holder as a result of your choosing to follow a\n" + + " later version.\n" + + " \n" + + " 15. Disclaimer of Warranty.\n" + + " \n" + + " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + + " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + + " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT" + + " WARRANTY\n" + + " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + + " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + + " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + + " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + + " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + + " \n" + + " 16. Limitation of Liability.\n" + + " \n" + + " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + + " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + + " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING" + + " ANY\n" + + " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + + " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + + " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + + " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + + " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + + " SUCH DAMAGES.\n" + + " \n" + + " 17. Interpretation of Sections 15 and 16.\n" + + " \n" + + " If the disclaimer of warranty and limitation of liability provided\n" + + " above cannot be given local legal effect according to their terms,\n" + + " reviewing courts shall apply local law that most closely approximates\n" + + " an absolute waiver of all civil liability in connection with the\n" + + " Program, unless a warranty or assumption of liability accompanies a\n" + + " copy of the Program in return for a fee.\n" + + " \n" + + " END OF TERMS AND CONDITIONS\n" + + " \n" + + " How to Apply These Terms to Your New Programs\n" + + " \n" + + " If you develop a new program, and you want it to be of the greatest\n" + + " possible use to the public, the best way to achieve this is to make it\n" + + " free software which everyone can redistribute and change under these" + + " terms.\n" + + " \n" + + " To do so, attach the following notices to the program. It is safest\n" + + " to attach them to the start of each source file to most effectively\n" + + " state the exclusion of warranty; and each file should have at least\n" + + " the \"copyright\" line and a pointer to where the full notice is found.\n" + + " \n" + + " \n" + + " Copyright (C) \n" + + " \n" + + " This program is free software: you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation, either version 3 of the License, or\n" + + " (at your option) any later version.\n" + + " \n" + + " This program is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " You should have received a copy of the GNU General Public License\n" + + " along with this program. If not, see .\n" + + " \n" + + " Also add information on how to contact you by electronic and paper mail.\n" + + " \n" + + " If the program does terminal interaction, make it output a short\n" + + " notice like this when it starts in an interactive mode:\n" + + " \n" + + " Copyright (C) \n" + + " This program comes with ABSOLUTELY NO WARRANTY; for details type `show" + + " w'.\n" + + " This is free software, and you are welcome to redistribute it\n" + + " under certain conditions; type `show c' for details.\n" + + " \n" + + " The hypothetical commands `show w' and `show c' should show the" + + " appropriate\n" + + " parts of the General Public License. Of course, your program's commands\n" + + " might be different; for a GUI interface, you would use an \"about box\".\n" + + " \n" + + " You should also get your employer (if you work as a programmer) or" + + " school,\n" + + " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + + " For more information on this, and how to apply and follow the GNU GPL, see\n" + + " .\n" + + " \n" + + " The GNU General Public License does not permit incorporating your" + + " program\n" + + " into proprietary programs. If your program is a subroutine library, you\n" + + " may consider it more useful to permit linking proprietary applications" + + " with\n" + + " the library. If this is what you want to do, use the GNU Lesser General\n" + + " Public License instead of this License. But first, please read\n" + + " .\n" + + " \n" + + " \n" + + " Name: libquadmath\n" + + " Files: scipy/.dylibs/libquadmath*.so\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + + " License: LGPL-2.1-or-later\n" + + " \n" + + " GCC Quad-Precision Math Library\n" + + " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + + " Written by Francois-Xavier Coudert \n" + + " \n" + + " This file is part of the libquadmath library.\n" + + " Libquadmath is free software; you can redistribute it and/or\n" + + " modify it under the terms of the GNU Library General Public\n" + + " License as published by the Free Software Foundation; either\n" + + " version 2.1 of the License, or (at your option) any later version.\n" + + " \n" + + " Libquadmath is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + + " Lesser General Public License for more details.\n" + + " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: numpy\n" + + "Required-by: gensim\n" + + "---\n" + + "Name: six\n" + + "Version: 1.16.0\n" + + "Summary: Python 2 and 3 compatibility utilities\n" + + "Home-page: https://github.com/benjaminp/six\n" + + "Author: Benjamin Peterson\n" + + "Author-email: benjamin@python.org\n" + + "License: MIT\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: \n" + + "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"; } private static final List EXPECTED_PIP_SHOW_RESULTS = new LinkedList<>(); static { - EXPECTED_PIP_SHOW_RESULTS.add("Name: altgraph\n" + - "Version: 0.17.2\n" + - "Summary: Python graph (network) package\n" + - "Home-page: https://altgraph.readthedocs.io\n" + - "Author: Ronald Oussoren\n" + - "Author-email: ronaldoussoren@mac.com\n" + - "License: MIT\n" + - "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + - "Requires: \n" + - "Required-by: macholib"); + EXPECTED_PIP_SHOW_RESULTS.add( + "Name: altgraph\n" + + "Version: 0.17.2\n" + + "Summary: Python graph (network) package\n" + + "Home-page: https://altgraph.readthedocs.io\n" + + "Author: Ronald Oussoren\n" + + "Author-email: ronaldoussoren@mac.com\n" + + "License: MIT\n" + + "Location:" + + " /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + + "Requires: \n" + + "Required-by: macholib"); - EXPECTED_PIP_SHOW_RESULTS.add("Name: scipy\n" + - "Version: 1.11.3\n" + - "Summary: Fundamental algorithms for scientific computing in Python\n" + - "Home-page: https://scipy.org/\n" + - "Author: \n" + - "Author-email: \n" + - "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions\n" + - " are met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above\n" + - " copyright notice, this list of conditions and the following\n" + - " disclaimer in the documentation and/or other materials provided\n" + - " with the distribution.\n" + - " \n" + - " 3. Neither the name of the copyright holder nor the names of its\n" + - " contributors may be used to endorse or promote products derived\n" + - " from this software without specific prior written permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " ----\n" + - " \n" + - " This binary distribution of SciPy also bundles the following software:\n" + - " \n" + - " \n" + - " Name: OpenBLAS\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled as a dynamically linked library\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 2011-2014, The OpenBLAS Project\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer in\n" + - " the documentation and/or other materials provided with the\n" + - " distribution.\n" + - " 3. Neither the name of the OpenBLAS project nor the names of\n" + - " its contributors may be used to endorse or promote products\n" + - " derived from this software without specific prior written\n" + - " permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + - " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + - " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + - " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + - " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + - " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + - " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + - " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + - " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + - " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: LAPACK\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled in OpenBLAS\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + - " of Tennessee Research Foundation. All rights\n" + - " reserved.\n" + - " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + - " rights reserved.\n" + - " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + - " reserved.\n" + - " \n" + - " $COPYRIGHT$\n" + - " \n" + - " Additional copyrights may follow\n" + - " \n" + - " $HEADER$\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " - Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " - Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer listed\n" + - " in this license in the documentation and/or other materials\n" + - " provided with the distribution.\n" + - " \n" + - " - Neither the name of the copyright holders nor the names of its\n" + - " contributors may be used to endorse or promote products derived from\n" + - " this software without specific prior written permission.\n" + - " \n" + - " The copyright holders provide no reassurances that the source code\n" + - " provided does not infringe any patent, copyright, or any other\n" + - " intellectual property rights of third parties. The copyright holders\n" + - " disclaim any liability to any recipient for claims brought against\n" + - " recipient by any third party for infringement of that parties\n" + - " intellectual property rights.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: GCC runtime library\n" + - " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + - " License: GPL-3.0-with-GCC-exception\n" + - " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + - " \n" + - " Libgfortran is free software; you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation; either version 3, or (at your option)\n" + - " any later version.\n" + - " \n" + - " Libgfortran is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " Under Section 7 of GPL version 3, you are granted additional\n" + - " permissions described in the GCC Runtime Library Exception, version\n" + - " 3.1, as published by the Free Software Foundation.\n" + - " \n" + - " You should have received a copy of the GNU General Public License and\n" + - " a copy of the GCC Runtime Library Exception along with this program;\n" + - " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + - " .\n" + - " \n" + - " ----\n" + - " \n" + - " Full text of license texts referred to above follows (that they are\n" + - " listed below does not necessarily imply the conditions apply to the\n" + - " present binary release):\n" + - " \n" + - " ----\n" + - " \n" + - " GCC RUNTIME LIBRARY EXCEPTION\n" + - " \n" + - " Version 3.1, 31 March 2009\n" + - " \n" + - " Copyright (C) 2009 Free Software Foundation, Inc. \n" + - " \n" + - " Everyone is permitted to copy and distribute verbatim copies of this\n" + - " license document, but changing it is not allowed.\n" + - " \n" + - " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + - " permission under section 7 of the GNU General Public License, version\n" + - " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + - " bears a notice placed by the copyright holder of the file stating that\n" + - " the file is governed by GPLv3 along with this Exception.\n" + - " \n" + - " When you use GCC to compile a program, GCC may combine portions of\n" + - " certain GCC header files and runtime libraries with the compiled\n" + - " program. The purpose of this Exception is to allow compilation of\n" + - " non-GPL (including proprietary) programs to use, in this way, the\n" + - " header files and runtime libraries covered by this Exception.\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " A file is an \"Independent Module\" if it either requires the Runtime\n" + - " Library for execution after a Compilation Process, or makes use of an\n" + - " interface provided by the Runtime Library, but is not otherwise based\n" + - " on the Runtime Library.\n" + - " \n" + - " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + - " modifications, governed by version 3 (or a specified later version) of\n" + - " the GNU General Public License (GPL) with the option of using any\n" + - " subsequent versions published by the FSF.\n" + - " \n" + - " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + - " modification and use would permit combination with GCC in accord with\n" + - " the license of GCC.\n" + - " \n" + - " \"Target Code\" refers to output from any compiler for a real or virtual\n" + - " target processor architecture, in executable form or suitable for\n" + - " input to an assembler, loader, linker and/or execution\n" + - " phase. Notwithstanding that, Target Code does not include data in any\n" + - " format that is used as a compiler intermediate representation, or used\n" + - " for producing a compiler intermediate representation.\n" + - " \n" + - " The \"Compilation Process\" transforms code entirely represented in\n" + - " non-intermediate languages designed for human-written code, and/or in\n" + - " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + - " use of source code generators and preprocessors need not be considered\n" + - " part of the Compilation Process, since the Compilation Process can be\n" + - " understood as starting with the output of the generators or\n" + - " preprocessors.\n" + - " \n" + - " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + - " with other GPL-compatible software, or if it is done without using any\n" + - " work based on GCC. For example, using non-GPL-compatible Software to\n" + - " optimize any GCC intermediate representations would not qualify as an\n" + - " Eligible Compilation Process.\n" + - " \n" + - " 1. Grant of Additional Permission.\n" + - " \n" + - " You have permission to propagate a work of Target Code formed by\n" + - " combining the Runtime Library with Independent Modules, even if such\n" + - " propagation would otherwise violate the terms of GPLv3, provided that\n" + - " all Target Code was generated by Eligible Compilation Processes. You\n" + - " may then convey such a combination under terms of your choice,\n" + - " consistent with the licensing of the Independent Modules.\n" + - " \n" + - " 2. No Weakening of GCC Copyleft.\n" + - " \n" + - " The availability of this Exception does not imply any general\n" + - " presumption that third-party software is unaffected by the copyleft\n" + - " requirements of the license of GCC.\n" + - " \n" + - " ----\n" + - " \n" + - " GNU GENERAL PUBLIC LICENSE\n" + - " Version 3, 29 June 2007\n" + - " \n" + - " Copyright (C) 2007 Free Software Foundation, Inc. \n" + - " Everyone is permitted to copy and distribute verbatim copies\n" + - " of this license document, but changing it is not allowed.\n" + - " \n" + - " Preamble\n" + - " \n" + - " The GNU General Public License is a free, copyleft license for\n" + - " software and other kinds of works.\n" + - " \n" + - " The licenses for most software and other practical works are designed\n" + - " to take away your freedom to share and change the works. By contrast,\n" + - " the GNU General Public License is intended to guarantee your freedom to\n" + - " share and change all versions of a program--to make sure it remains free\n" + - " software for all its users. We, the Free Software Foundation, use the\n" + - " GNU General Public License for most of our software; it applies also to\n" + - " any other work released this way by its authors. You can apply it to\n" + - " your programs, too.\n" + - " \n" + - " When we speak of free software, we are referring to freedom, not\n" + - " price. Our General Public Licenses are designed to make sure that you\n" + - " have the freedom to distribute copies of free software (and charge for\n" + - " them if you wish), that you receive source code or can get it if you\n" + - " want it, that you can change the software or use pieces of it in new\n" + - " free programs, and that you know you can do these things.\n" + - " \n" + - " To protect your rights, we need to prevent others from denying you\n" + - " these rights or asking you to surrender the rights. Therefore, you have\n" + - " certain responsibilities if you distribute copies of the software, or if\n" + - " you modify it: responsibilities to respect the freedom of others.\n" + - " \n" + - " For example, if you distribute copies of such a program, whether\n" + - " gratis or for a fee, you must pass on to the recipients the same\n" + - " freedoms that you received. You must make sure that they, too, receive\n" + - " or can get the source code. And you must show them these terms so they\n" + - " know their rights.\n" + - " \n" + - " Developers that use the GNU GPL protect your rights with two steps:\n" + - " (1) assert copyright on the software, and (2) offer you this License\n" + - " giving you legal permission to copy, distribute and/or modify it.\n" + - " \n" + - " For the developers' and authors' protection, the GPL clearly explains\n" + - " that there is no warranty for this free software. For both users' and\n" + - " authors' sake, the GPL requires that modified versions be marked as\n" + - " changed, so that their problems will not be attributed erroneously to\n" + - " authors of previous versions.\n" + - " \n" + - " Some devices are designed to deny users access to install or run\n" + - " modified versions of the software inside them, although the manufacturer\n" + - " can do so. This is fundamentally incompatible with the aim of\n" + - " protecting users' freedom to change the software. The systematic\n" + - " pattern of such abuse occurs in the area of products for individuals to\n" + - " use, which is precisely where it is most unacceptable. Therefore, we\n" + - " have designed this version of the GPL to prohibit the practice for those\n" + - " products. If such problems arise substantially in other domains, we\n" + - " stand ready to extend this provision to those domains in future versions\n" + - " of the GPL, as needed to protect the freedom of users.\n" + - " \n" + - " Finally, every program is threatened constantly by software patents.\n" + - " States should not allow patents to restrict development and use of\n" + - " software on general-purpose computers, but in those that do, we wish to\n" + - " avoid the special danger that patents applied to a free program could\n" + - " make it effectively proprietary. To prevent this, the GPL assures that\n" + - " patents cannot be used to render the program non-free.\n" + - " \n" + - " The precise terms and conditions for copying, distribution and\n" + - " modification follow.\n" + - " \n" + - " TERMS AND CONDITIONS\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " \"This License\" refers to version 3 of the GNU General Public License.\n" + - " \n" + - " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + - " works, such as semiconductor masks.\n" + - " \n" + - " \"The Program\" refers to any copyrightable work licensed under this\n" + - " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + - " \"recipients\" may be individuals or organizations.\n" + - " \n" + - " To \"modify\" a work means to copy from or adapt all or part of the work\n" + - " in a fashion requiring copyright permission, other than the making of an\n" + - " exact copy. The resulting work is called a \"modified version\" of the\n" + - " earlier work or a work \"based on\" the earlier work.\n" + - " \n" + - " A \"covered work\" means either the unmodified Program or a work based\n" + - " on the Program.\n" + - " \n" + - " To \"propagate\" a work means to do anything with it that, without\n" + - " permission, would make you directly or secondarily liable for\n" + - " infringement under applicable copyright law, except executing it on a\n" + - " computer or modifying a private copy. Propagation includes copying,\n" + - " distribution (with or without modification), making available to the\n" + - " public, and in some countries other activities as well.\n" + - " \n" + - " To \"convey\" a work means any kind of propagation that enables other\n" + - " parties to make or receive copies. Mere interaction with a user through\n" + - " a computer network, with no transfer of a copy, is not conveying.\n" + - " \n" + - " An interactive user interface displays \"Appropriate Legal Notices\"\n" + - " to the extent that it includes a convenient and prominently visible\n" + - " feature that (1) displays an appropriate copyright notice, and (2)\n" + - " tells the user that there is no warranty for the work (except to the\n" + - " extent that warranties are provided), that licensees may convey the\n" + - " work under this License, and how to view a copy of this License. If\n" + - " the interface presents a list of user commands or options, such as a\n" + - " menu, a prominent item in the list meets this criterion.\n" + - " \n" + - " 1. Source Code.\n" + - " \n" + - " The \"source code\" for a work means the preferred form of the work\n" + - " for making modifications to it. \"Object code\" means any non-source\n" + - " form of a work.\n" + - " \n" + - " A \"Standard Interface\" means an interface that either is an official\n" + - " standard defined by a recognized standards body, or, in the case of\n" + - " interfaces specified for a particular programming language, one that\n" + - " is widely used among developers working in that language.\n" + - " \n" + - " The \"System Libraries\" of an executable work include anything, other\n" + - " than the work as a whole, that (a) is included in the normal form of\n" + - " packaging a Major Component, but which is not part of that Major\n" + - " Component, and (b) serves only to enable use of the work with that\n" + - " Major Component, or to implement a Standard Interface for which an\n" + - " implementation is available to the public in source code form. A\n" + - " \"Major Component\", in this context, means a major essential component\n" + - " (kernel, window system, and so on) of the specific operating system\n" + - " (if any) on which the executable work runs, or a compiler used to\n" + - " produce the work, or an object code interpreter used to run it.\n" + - " \n" + - " The \"Corresponding Source\" for a work in object code form means all\n" + - " the source code needed to generate, install, and (for an executable\n" + - " work) run the object code and to modify the work, including scripts to\n" + - " control those activities. However, it does not include the work's\n" + - " System Libraries, or general-purpose tools or generally available free\n" + - " programs which are used unmodified in performing those activities but\n" + - " which are not part of the work. For example, Corresponding Source\n" + - " includes interface definition files associated with source files for\n" + - " the work, and the source code for shared libraries and dynamically\n" + - " linked subprograms that the work is specifically designed to require,\n" + - " such as by intimate data communication or control flow between those\n" + - " subprograms and other parts of the work.\n" + - " \n" + - " The Corresponding Source need not include anything that users\n" + - " can regenerate automatically from other parts of the Corresponding\n" + - " Source.\n" + - " \n" + - " The Corresponding Source for a work in source code form is that\n" + - " same work.\n" + - " \n" + - " 2. Basic Permissions.\n" + - " \n" + - " All rights granted under this License are granted for the term of\n" + - " copyright on the Program, and are irrevocable provided the stated\n" + - " conditions are met. This License explicitly affirms your unlimited\n" + - " permission to run the unmodified Program. The output from running a\n" + - " covered work is covered by this License only if the output, given its\n" + - " content, constitutes a covered work. This License acknowledges your\n" + - " rights of fair use or other equivalent, as provided by copyright law.\n" + - " \n" + - " You may make, run and propagate covered works that you do not\n" + - " convey, without conditions so long as your license otherwise remains\n" + - " in force. You may convey covered works to others for the sole purpose\n" + - " of having them make modifications exclusively for you, or provide you\n" + - " with facilities for running those works, provided that you comply with\n" + - " the terms of this License in conveying all material for which you do\n" + - " not control copyright. Those thus making or running the covered works\n" + - " for you must do so exclusively on your behalf, under your direction\n" + - " and control, on terms that prohibit them from making any copies of\n" + - " your copyrighted material outside their relationship with you.\n" + - " \n" + - " Conveying under any other circumstances is permitted solely under\n" + - " the conditions stated below. Sublicensing is not allowed; section 10\n" + - " makes it unnecessary.\n" + - " \n" + - " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + - " \n" + - " No covered work shall be deemed part of an effective technological\n" + - " measure under any applicable law fulfilling obligations under article\n" + - " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + - " similar laws prohibiting or restricting circumvention of such\n" + - " measures.\n" + - " \n" + - " When you convey a covered work, you waive any legal power to forbid\n" + - " circumvention of technological measures to the extent such circumvention\n" + - " is effected by exercising rights under this License with respect to\n" + - " the covered work, and you disclaim any intention to limit operation or\n" + - " modification of the work as a means of enforcing, against the work's\n" + - " users, your or third parties' legal rights to forbid circumvention of\n" + - " technological measures.\n" + - " \n" + - " 4. Conveying Verbatim Copies.\n" + - " \n" + - " You may convey verbatim copies of the Program's source code as you\n" + - " receive it, in any medium, provided that you conspicuously and\n" + - " appropriately publish on each copy an appropriate copyright notice;\n" + - " keep intact all notices stating that this License and any\n" + - " non-permissive terms added in accord with section 7 apply to the code;\n" + - " keep intact all notices of the absence of any warranty; and give all\n" + - " recipients a copy of this License along with the Program.\n" + - " \n" + - " You may charge any price or no price for each copy that you convey,\n" + - " and you may offer support or warranty protection for a fee.\n" + - " \n" + - " 5. Conveying Modified Source Versions.\n" + - " \n" + - " You may convey a work based on the Program, or the modifications to\n" + - " produce it from the Program, in the form of source code under the\n" + - " terms of section 4, provided that you also meet all of these conditions:\n" + - " \n" + - " a) The work must carry prominent notices stating that you modified\n" + - " it, and giving a relevant date.\n" + - " \n" + - " b) The work must carry prominent notices stating that it is\n" + - " released under this License and any conditions added under section\n" + - " 7. This requirement modifies the requirement in section 4 to\n" + - " \"keep intact all notices\".\n" + - " \n" + - " c) You must license the entire work, as a whole, under this\n" + - " License to anyone who comes into possession of a copy. This\n" + - " License will therefore apply, along with any applicable section 7\n" + - " additional terms, to the whole of the work, and all its parts,\n" + - " regardless of how they are packaged. This License gives no\n" + - " permission to license the work in any other way, but it does not\n" + - " invalidate such permission if you have separately received it.\n" + - " \n" + - " d) If the work has interactive user interfaces, each must display\n" + - " Appropriate Legal Notices; however, if the Program has interactive\n" + - " interfaces that do not display Appropriate Legal Notices, your\n" + - " work need not make them do so.\n" + - " \n" + - " A compilation of a covered work with other separate and independent\n" + - " works, which are not by their nature extensions of the covered work,\n" + - " and which are not combined with it such as to form a larger program,\n" + - " in or on a volume of a storage or distribution medium, is called an\n" + - " \"aggregate\" if the compilation and its resulting copyright are not\n" + - " used to limit the access or legal rights of the compilation's users\n" + - " beyond what the individual works permit. Inclusion of a covered work\n" + - " in an aggregate does not cause this License to apply to the other\n" + - " parts of the aggregate.\n" + - " \n" + - " 6. Conveying Non-Source Forms.\n" + - " \n" + - " You may convey a covered work in object code form under the terms\n" + - " of sections 4 and 5, provided that you also convey the\n" + - " machine-readable Corresponding Source under the terms of this License,\n" + - " in one of these ways:\n" + - " \n" + - " a) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by the\n" + - " Corresponding Source fixed on a durable physical medium\n" + - " customarily used for software interchange.\n" + - " \n" + - " b) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by a\n" + - " written offer, valid for at least three years and valid for as\n" + - " long as you offer spare parts or customer support for that product\n" + - " model, to give anyone who possesses the object code either (1) a\n" + - " copy of the Corresponding Source for all the software in the\n" + - " product that is covered by this License, on a durable physical\n" + - " medium customarily used for software interchange, for a price no\n" + - " more than your reasonable cost of physically performing this\n" + - " conveying of source, or (2) access to copy the\n" + - " Corresponding Source from a network server at no charge.\n" + - " \n" + - " c) Convey individual copies of the object code with a copy of the\n" + - " written offer to provide the Corresponding Source. This\n" + - " alternative is allowed only occasionally and noncommercially, and\n" + - " only if you received the object code with such an offer, in accord\n" + - " with subsection 6b.\n" + - " \n" + - " d) Convey the object code by offering access from a designated\n" + - " place (gratis or for a charge), and offer equivalent access to the\n" + - " Corresponding Source in the same way through the same place at no\n" + - " further charge. You need not require recipients to copy the\n" + - " Corresponding Source along with the object code. If the place to\n" + - " copy the object code is a network server, the Corresponding Source\n" + - " may be on a different server (operated by you or a third party)\n" + - " that supports equivalent copying facilities, provided you maintain\n" + - " clear directions next to the object code saying where to find the\n" + - " Corresponding Source. Regardless of what server hosts the\n" + - " Corresponding Source, you remain obligated to ensure that it is\n" + - " available for as long as needed to satisfy these requirements.\n" + - " \n" + - " e) Convey the object code using peer-to-peer transmission, provided\n" + - " you inform other peers where the object code and Corresponding\n" + - " Source of the work are being offered to the general public at no\n" + - " charge under subsection 6d.\n" + - " \n" + - " A separable portion of the object code, whose source code is excluded\n" + - " from the Corresponding Source as a System Library, need not be\n" + - " included in conveying the object code work.\n" + - " \n" + - " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + - " tangible personal property which is normally used for personal, family,\n" + - " or household purposes, or (2) anything designed or sold for incorporation\n" + - " into a dwelling. In determining whether a product is a consumer product,\n" + - " doubtful cases shall be resolved in favor of coverage. For a particular\n" + - " product received by a particular user, \"normally used\" refers to a\n" + - " typical or common use of that class of product, regardless of the status\n" + - " of the particular user or of the way in which the particular user\n" + - " actually uses, or expects or is expected to use, the product. A product\n" + - " is a consumer product regardless of whether the product has substantial\n" + - " commercial, industrial or non-consumer uses, unless such uses represent\n" + - " the only significant mode of use of the product.\n" + - " \n" + - " \"Installation Information\" for a User Product means any methods,\n" + - " procedures, authorization keys, or other information required to install\n" + - " and execute modified versions of a covered work in that User Product from\n" + - " a modified version of its Corresponding Source. The information must\n" + - " suffice to ensure that the continued functioning of the modified object\n" + - " code is in no case prevented or interfered with solely because\n" + - " modification has been made.\n" + - " \n" + - " If you convey an object code work under this section in, or with, or\n" + - " specifically for use in, a User Product, and the conveying occurs as\n" + - " part of a transaction in which the right of possession and use of the\n" + - " User Product is transferred to the recipient in perpetuity or for a\n" + - " fixed term (regardless of how the transaction is characterized), the\n" + - " Corresponding Source conveyed under this section must be accompanied\n" + - " by the Installation Information. But this requirement does not apply\n" + - " if neither you nor any third party retains the ability to install\n" + - " modified object code on the User Product (for example, the work has\n" + - " been installed in ROM).\n" + - " \n" + - " The requirement to provide Installation Information does not include a\n" + - " requirement to continue to provide support service, warranty, or updates\n" + - " for a work that has been modified or installed by the recipient, or for\n" + - " the User Product in which it has been modified or installed. Access to a\n" + - " network may be denied when the modification itself materially and\n" + - " adversely affects the operation of the network or violates the rules and\n" + - " protocols for communication across the network.\n" + - " \n" + - " Corresponding Source conveyed, and Installation Information provided,\n" + - " in accord with this section must be in a format that is publicly\n" + - " documented (and with an implementation available to the public in\n" + - " source code form), and must require no special password or key for\n" + - " unpacking, reading or copying.\n" + - " \n" + - " 7. Additional Terms.\n" + - " \n" + - " \"Additional permissions\" are terms that supplement the terms of this\n" + - " License by making exceptions from one or more of its conditions.\n" + - " Additional permissions that are applicable to the entire Program shall\n" + - " be treated as though they were included in this License, to the extent\n" + - " that they are valid under applicable law. If additional permissions\n" + - " apply only to part of the Program, that part may be used separately\n" + - " under those permissions, but the entire Program remains governed by\n" + - " this License without regard to the additional permissions.\n" + - " \n" + - " When you convey a copy of a covered work, you may at your option\n" + - " remove any additional permissions from that copy, or from any part of\n" + - " it. (Additional permissions may be written to require their own\n" + - " removal in certain cases when you modify the work.) You may place\n" + - " additional permissions on material, added by you to a covered work,\n" + - " for which you have or can give appropriate copyright permission.\n" + - " \n" + - " Notwithstanding any other provision of this License, for material you\n" + - " add to a covered work, you may (if authorized by the copyright holders of\n" + - " that material) supplement the terms of this License with terms:\n" + - " \n" + - " a) Disclaiming warranty or limiting liability differently from the\n" + - " terms of sections 15 and 16 of this License; or\n" + - " \n" + - " b) Requiring preservation of specified reasonable legal notices or\n" + - " author attributions in that material or in the Appropriate Legal\n" + - " Notices displayed by works containing it; or\n" + - " \n" + - " c) Prohibiting misrepresentation of the origin of that material, or\n" + - " requiring that modified versions of such material be marked in\n" + - " reasonable ways as different from the original version; or\n" + - " \n" + - " d) Limiting the use for publicity purposes of names of licensors or\n" + - " authors of the material; or\n" + - " \n" + - " e) Declining to grant rights under trademark law for use of some\n" + - " trade names, trademarks, or service marks; or\n" + - " \n" + - " f) Requiring indemnification of licensors and authors of that\n" + - " material by anyone who conveys the material (or modified versions of\n" + - " it) with contractual assumptions of liability to the recipient, for\n" + - " any liability that these contractual assumptions directly impose on\n" + - " those licensors and authors.\n" + - " \n" + - " All other non-permissive additional terms are considered \"further\n" + - " restrictions\" within the meaning of section 10. If the Program as you\n" + - " received it, or any part of it, contains a notice stating that it is\n" + - " governed by this License along with a term that is a further\n" + - " restriction, you may remove that term. If a license document contains\n" + - " a further restriction but permits relicensing or conveying under this\n" + - " License, you may add to a covered work material governed by the terms\n" + - " of that license document, provided that the further restriction does\n" + - " not survive such relicensing or conveying.\n" + - " \n" + - " If you add terms to a covered work in accord with this section, you\n" + - " must place, in the relevant source files, a statement of the\n" + - " additional terms that apply to those files, or a notice indicating\n" + - " where to find the applicable terms.\n" + - " \n" + - " Additional terms, permissive or non-permissive, may be stated in the\n" + - " form of a separately written license, or stated as exceptions;\n" + - " the above requirements apply either way.\n" + - " \n" + - " 8. Termination.\n" + - " \n" + - " You may not propagate or modify a covered work except as expressly\n" + - " provided under this License. Any attempt otherwise to propagate or\n" + - " modify it is void, and will automatically terminate your rights under\n" + - " this License (including any patent licenses granted under the third\n" + - " paragraph of section 11).\n" + - " \n" + - " However, if you cease all violation of this License, then your\n" + - " license from a particular copyright holder is reinstated (a)\n" + - " provisionally, unless and until the copyright holder explicitly and\n" + - " finally terminates your license, and (b) permanently, if the copyright\n" + - " holder fails to notify you of the violation by some reasonable means\n" + - " prior to 60 days after the cessation.\n" + - " \n" + - " Moreover, your license from a particular copyright holder is\n" + - " reinstated permanently if the copyright holder notifies you of the\n" + - " violation by some reasonable means, this is the first time you have\n" + - " received notice of violation of this License (for any work) from that\n" + - " copyright holder, and you cure the violation prior to 30 days after\n" + - " your receipt of the notice.\n" + - " \n" + - " Termination of your rights under this section does not terminate the\n" + - " licenses of parties who have received copies or rights from you under\n" + - " this License. If your rights have been terminated and not permanently\n" + - " reinstated, you do not qualify to receive new licenses for the same\n" + - " material under section 10.\n" + - " \n" + - " 9. Acceptance Not Required for Having Copies.\n" + - " \n" + - " You are not required to accept this License in order to receive or\n" + - " run a copy of the Program. Ancillary propagation of a covered work\n" + - " occurring solely as a consequence of using peer-to-peer transmission\n" + - " to receive a copy likewise does not require acceptance. However,\n" + - " nothing other than this License grants you permission to propagate or\n" + - " modify any covered work. These actions infringe copyright if you do\n" + - " not accept this License. Therefore, by modifying or propagating a\n" + - " covered work, you indicate your acceptance of this License to do so.\n" + - " \n" + - " 10. Automatic Licensing of Downstream Recipients.\n" + - " \n" + - " Each time you convey a covered work, the recipient automatically\n" + - " receives a license from the original licensors, to run, modify and\n" + - " propagate that work, subject to this License. You are not responsible\n" + - " for enforcing compliance by third parties with this License.\n" + - " \n" + - " An \"entity transaction\" is a transaction transferring control of an\n" + - " organization, or substantially all assets of one, or subdividing an\n" + - " organization, or merging organizations. If propagation of a covered\n" + - " work results from an entity transaction, each party to that\n" + - " transaction who receives a copy of the work also receives whatever\n" + - " licenses to the work the party's predecessor in interest had or could\n" + - " give under the previous paragraph, plus a right to possession of the\n" + - " Corresponding Source of the work from the predecessor in interest, if\n" + - " the predecessor has it or can get it with reasonable efforts.\n" + - " \n" + - " You may not impose any further restrictions on the exercise of the\n" + - " rights granted or affirmed under this License. For example, you may\n" + - " not impose a license fee, royalty, or other charge for exercise of\n" + - " rights granted under this License, and you may not initiate litigation\n" + - " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + - " any patent claim is infringed by making, using, selling, offering for\n" + - " sale, or importing the Program or any portion of it.\n" + - " \n" + - " 11. Patents.\n" + - " \n" + - " A \"contributor\" is a copyright holder who authorizes use under this\n" + - " License of the Program or a work on which the Program is based. The\n" + - " work thus licensed is called the contributor's \"contributor version\".\n" + - " \n" + - " A contributor's \"essential patent claims\" are all patent claims\n" + - " owned or controlled by the contributor, whether already acquired or\n" + - " hereafter acquired, that would be infringed by some manner, permitted\n" + - " by this License, of making, using, or selling its contributor version,\n" + - " but do not include claims that would be infringed only as a\n" + - " consequence of further modification of the contributor version. For\n" + - " purposes of this definition, \"control\" includes the right to grant\n" + - " patent sublicenses in a manner consistent with the requirements of\n" + - " this License.\n" + - " \n" + - " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + - " patent license under the contributor's essential patent claims, to\n" + - " make, use, sell, offer for sale, import and otherwise run, modify and\n" + - " propagate the contents of its contributor version.\n" + - " \n" + - " In the following three paragraphs, a \"patent license\" is any express\n" + - " agreement or commitment, however denominated, not to enforce a patent\n" + - " (such as an express permission to practice a patent or covenant not to\n" + - " sue for patent infringement). To \"grant\" such a patent license to a\n" + - " party means to make such an agreement or commitment not to enforce a\n" + - " patent against the party.\n" + - " \n" + - " If you convey a covered work, knowingly relying on a patent license,\n" + - " and the Corresponding Source of the work is not available for anyone\n" + - " to copy, free of charge and under the terms of this License, through a\n" + - " publicly available network server or other readily accessible means,\n" + - " then you must either (1) cause the Corresponding Source to be so\n" + - " available, or (2) arrange to deprive yourself of the benefit of the\n" + - " patent license for this particular work, or (3) arrange, in a manner\n" + - " consistent with the requirements of this License, to extend the patent\n" + - " license to downstream recipients. \"Knowingly relying\" means you have\n" + - " actual knowledge that, but for the patent license, your conveying the\n" + - " covered work in a country, or your recipient's use of the covered work\n" + - " in a country, would infringe one or more identifiable patents in that\n" + - " country that you have reason to believe are valid.\n" + - " \n" + - " If, pursuant to or in connection with a single transaction or\n" + - " arrangement, you convey, or propagate by procuring conveyance of, a\n" + - " covered work, and grant a patent license to some of the parties\n" + - " receiving the covered work authorizing them to use, propagate, modify\n" + - " or convey a specific copy of the covered work, then the patent license\n" + - " you grant is automatically extended to all recipients of the covered\n" + - " work and works based on it.\n" + - " \n" + - " A patent license is \"discriminatory\" if it does not include within\n" + - " the scope of its coverage, prohibits the exercise of, or is\n" + - " conditioned on the non-exercise of one or more of the rights that are\n" + - " specifically granted under this License. You may not convey a covered\n" + - " work if you are a party to an arrangement with a third party that is\n" + - " in the business of distributing software, under which you make payment\n" + - " to the third party based on the extent of your activity of conveying\n" + - " the work, and under which the third party grants, to any of the\n" + - " parties who would receive the covered work from you, a discriminatory\n" + - " patent license (a) in connection with copies of the covered work\n" + - " conveyed by you (or copies made from those copies), or (b) primarily\n" + - " for and in connection with specific products or compilations that\n" + - " contain the covered work, unless you entered into that arrangement,\n" + - " or that patent license was granted, prior to 28 March 2007.\n" + - " \n" + - " Nothing in this License shall be construed as excluding or limiting\n" + - " any implied license or other defenses to infringement that may\n" + - " otherwise be available to you under applicable patent law.\n" + - " \n" + - " 12. No Surrender of Others' Freedom.\n" + - " \n" + - " If conditions are imposed on you (whether by court order, agreement or\n" + - " otherwise) that contradict the conditions of this License, they do not\n" + - " excuse you from the conditions of this License. If you cannot convey a\n" + - " covered work so as to satisfy simultaneously your obligations under this\n" + - " License and any other pertinent obligations, then as a consequence you may\n" + - " not convey it at all. For example, if you agree to terms that obligate you\n" + - " to collect a royalty for further conveying from those to whom you convey\n" + - " the Program, the only way you could satisfy both those terms and this\n" + - " License would be to refrain entirely from conveying the Program.\n" + - " \n" + - " 13. Use with the GNU Affero General Public License.\n" + - " \n" + - " Notwithstanding any other provision of this License, you have\n" + - " permission to link or combine any covered work with a work licensed\n" + - " under version 3 of the GNU Affero General Public License into a single\n" + - " combined work, and to convey the resulting work. The terms of this\n" + - " License will continue to apply to the part which is the covered work,\n" + - " but the special requirements of the GNU Affero General Public License,\n" + - " section 13, concerning interaction through a network will apply to the\n" + - " combination as such.\n" + - " \n" + - " 14. Revised Versions of this License.\n" + - " \n" + - " The Free Software Foundation may publish revised and/or new versions of\n" + - " the GNU General Public License from time to time. Such new versions will\n" + - " be similar in spirit to the present version, but may differ in detail to\n" + - " address new problems or concerns.\n" + - " \n" + - " Each version is given a distinguishing version number. If the\n" + - " Program specifies that a certain numbered version of the GNU General\n" + - " Public License \"or any later version\" applies to it, you have the\n" + - " option of following the terms and conditions either of that numbered\n" + - " version or of any later version published by the Free Software\n" + - " Foundation. If the Program does not specify a version number of the\n" + - " GNU General Public License, you may choose any version ever published\n" + - " by the Free Software Foundation.\n" + - " \n" + - " If the Program specifies that a proxy can decide which future\n" + - " versions of the GNU General Public License can be used, that proxy's\n" + - " public statement of acceptance of a version permanently authorizes you\n" + - " to choose that version for the Program.\n" + - " \n" + - " Later license versions may give you additional or different\n" + - " permissions. However, no additional obligations are imposed on any\n" + - " author or copyright holder as a result of your choosing to follow a\n" + - " later version.\n" + - " \n" + - " 15. Disclaimer of Warranty.\n" + - " \n" + - " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + - " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + - " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + - " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + - " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + - " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + - " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + - " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + - " \n" + - " 16. Limitation of Liability.\n" + - " \n" + - " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + - " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + - " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + - " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + - " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + - " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + - " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + - " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + - " SUCH DAMAGES.\n" + - " \n" + - " 17. Interpretation of Sections 15 and 16.\n" + - " \n" + - " If the disclaimer of warranty and limitation of liability provided\n" + - " above cannot be given local legal effect according to their terms,\n" + - " reviewing courts shall apply local law that most closely approximates\n" + - " an absolute waiver of all civil liability in connection with the\n" + - " Program, unless a warranty or assumption of liability accompanies a\n" + - " copy of the Program in return for a fee.\n" + - " \n" + - " END OF TERMS AND CONDITIONS\n" + - " \n" + - " How to Apply These Terms to Your New Programs\n" + - " \n" + - " If you develop a new program, and you want it to be of the greatest\n" + - " possible use to the public, the best way to achieve this is to make it\n" + - " free software which everyone can redistribute and change under these terms.\n" + - " \n" + - " To do so, attach the following notices to the program. It is safest\n" + - " to attach them to the start of each source file to most effectively\n" + - " state the exclusion of warranty; and each file should have at least\n" + - " the \"copyright\" line and a pointer to where the full notice is found.\n" + - " \n" + - " \n" + - " Copyright (C) \n" + - " \n" + - " This program is free software: you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation, either version 3 of the License, or\n" + - " (at your option) any later version.\n" + - " \n" + - " This program is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " You should have received a copy of the GNU General Public License\n" + - " along with this program. If not, see .\n" + - " \n" + - " Also add information on how to contact you by electronic and paper mail.\n" + - " \n" + - " If the program does terminal interaction, make it output a short\n" + - " notice like this when it starts in an interactive mode:\n" + - " \n" + - " Copyright (C) \n" + - " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + - " This is free software, and you are welcome to redistribute it\n" + - " under certain conditions; type `show c' for details.\n" + - " \n" + - " The hypothetical commands `show w' and `show c' should show the appropriate\n" + - " parts of the General Public License. Of course, your program's commands\n" + - " might be different; for a GUI interface, you would use an \"about box\".\n" + - " \n" + - " You should also get your employer (if you work as a programmer) or school,\n" + - " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + - " For more information on this, and how to apply and follow the GNU GPL, see\n" + - " .\n" + - " \n" + - " The GNU General Public License does not permit incorporating your program\n" + - " into proprietary programs. If your program is a subroutine library, you\n" + - " may consider it more useful to permit linking proprietary applications with\n" + - " the library. If this is what you want to do, use the GNU Lesser General\n" + - " Public License instead of this License. But first, please read\n" + - " .\n" + - " \n" + - " \n" + - " Name: libquadmath\n" + - " Files: scipy/.dylibs/libquadmath*.so\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + - " License: LGPL-2.1-or-later\n" + - " \n" + - " GCC Quad-Precision Math Library\n" + - " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + - " Written by Francois-Xavier Coudert \n" + - " \n" + - " This file is part of the libquadmath library.\n" + - " Libquadmath is free software; you can redistribute it and/or\n" + - " modify it under the terms of the GNU Library General Public\n" + - " License as published by the Free Software Foundation; either\n" + - " version 2.1 of the License, or (at your option) any later version.\n" + - " \n" + - " Libquadmath is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + - " Lesser General Public License for more details.\n" + - " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: numpy\n" + - "Required-by: gensim"); + EXPECTED_PIP_SHOW_RESULTS.add( + "Name: scipy\n" + + "Version: 1.11.3\n" + + "Summary: Fundamental algorithms for scientific computing in Python\n" + + "Home-page: https://scipy.org/\n" + + "Author: \n" + + "Author-email: \n" + + "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions\n" + + " are met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above\n" + + " copyright notice, this list of conditions and the following\n" + + " disclaimer in the documentation and/or other materials provided\n" + + " with the distribution.\n" + + " \n" + + " 3. Neither the name of the copyright holder nor the names of its\n" + + " contributors may be used to endorse or promote products derived\n" + + " from this software without specific prior written permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " ----\n" + + " \n" + + " This binary distribution of SciPy also bundles the following software:\n" + + " \n" + + " \n" + + " Name: OpenBLAS\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled as a dynamically linked library\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 2011-2014, The OpenBLAS Project\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer in\n" + + " the documentation and/or other materials provided with the\n" + + " distribution.\n" + + " 3. Neither the name of the OpenBLAS project nor the names of\n" + + " its contributors may be used to endorse or promote products\n" + + " derived from this software without specific prior written\n" + + " permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS" + + " IS\"\n" + + " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO," + + " THE\n" + + " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR" + + " PURPOSE\n" + + " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + + " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR" + + " CONSEQUENTIAL\n" + + " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS" + + " OR\n" + + " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)" + + " HOWEVER\n" + + " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT" + + " LIABILITY,\n" + + " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF" + + " THE\n" + + " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: LAPACK\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled in OpenBLAS\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + + " of Tennessee Research Foundation. All rights\n" + + " reserved.\n" + + " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + + " rights reserved.\n" + + " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + + " reserved.\n" + + " \n" + + " $COPYRIGHT$\n" + + " \n" + + " Additional copyrights may follow\n" + + " \n" + + " $HEADER$\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " - Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " - Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer listed\n" + + " in this license in the documentation and/or other materials\n" + + " provided with the distribution.\n" + + " \n" + + " - Neither the name of the copyright holders nor the names of its\n" + + " contributors may be used to endorse or promote products derived from\n" + + " this software without specific prior written permission.\n" + + " \n" + + " The copyright holders provide no reassurances that the source code\n" + + " provided does not infringe any patent, copyright, or any other\n" + + " intellectual property rights of third parties. The copyright holders\n" + + " disclaim any liability to any recipient for claims brought against\n" + + " recipient by any third party for infringement of that parties\n" + + " intellectual property rights.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: GCC runtime library\n" + + " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + + " License: GPL-3.0-with-GCC-exception\n" + + " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + + " \n" + + " Libgfortran is free software; you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation; either version 3, or (at your option)\n" + + " any later version.\n" + + " \n" + + " Libgfortran is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " Under Section 7 of GPL version 3, you are granted additional\n" + + " permissions described in the GCC Runtime Library Exception, version\n" + + " 3.1, as published by the Free Software Foundation.\n" + + " \n" + + " You should have received a copy of the GNU General Public License and\n" + + " a copy of the GCC Runtime Library Exception along with this program;\n" + + " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + + " .\n" + + " \n" + + " ----\n" + + " \n" + + " Full text of license texts referred to above follows (that they are\n" + + " listed below does not necessarily imply the conditions apply to the\n" + + " present binary release):\n" + + " \n" + + " ----\n" + + " \n" + + " GCC RUNTIME LIBRARY EXCEPTION\n" + + " \n" + + " Version 3.1, 31 March 2009\n" + + " \n" + + " Copyright (C) 2009 Free Software Foundation, Inc. \n" + + " \n" + + " Everyone is permitted to copy and distribute verbatim copies of this\n" + + " license document, but changing it is not allowed.\n" + + " \n" + + " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + + " permission under section 7 of the GNU General Public License, version\n" + + " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + + " bears a notice placed by the copyright holder of the file stating that\n" + + " the file is governed by GPLv3 along with this Exception.\n" + + " \n" + + " When you use GCC to compile a program, GCC may combine portions of\n" + + " certain GCC header files and runtime libraries with the compiled\n" + + " program. The purpose of this Exception is to allow compilation of\n" + + " non-GPL (including proprietary) programs to use, in this way, the\n" + + " header files and runtime libraries covered by this Exception.\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " A file is an \"Independent Module\" if it either requires the Runtime\n" + + " Library for execution after a Compilation Process, or makes use of an\n" + + " interface provided by the Runtime Library, but is not otherwise based\n" + + " on the Runtime Library.\n" + + " \n" + + " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + + " modifications, governed by version 3 (or a specified later version) of\n" + + " the GNU General Public License (GPL) with the option of using any\n" + + " subsequent versions published by the FSF.\n" + + " \n" + + " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + + " modification and use would permit combination with GCC in accord with\n" + + " the license of GCC.\n" + + " \n" + + " \"Target Code\" refers to output from any compiler for a real or virtual\n" + + " target processor architecture, in executable form or suitable for\n" + + " input to an assembler, loader, linker and/or execution\n" + + " phase. Notwithstanding that, Target Code does not include data in any\n" + + " format that is used as a compiler intermediate representation, or used\n" + + " for producing a compiler intermediate representation.\n" + + " \n" + + " The \"Compilation Process\" transforms code entirely represented in\n" + + " non-intermediate languages designed for human-written code, and/or in\n" + + " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + + " use of source code generators and preprocessors need not be considered\n" + + " part of the Compilation Process, since the Compilation Process can be\n" + + " understood as starting with the output of the generators or\n" + + " preprocessors.\n" + + " \n" + + " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + + " with other GPL-compatible software, or if it is done without using any\n" + + " work based on GCC. For example, using non-GPL-compatible Software to\n" + + " optimize any GCC intermediate representations would not qualify as an\n" + + " Eligible Compilation Process.\n" + + " \n" + + " 1. Grant of Additional Permission.\n" + + " \n" + + " You have permission to propagate a work of Target Code formed by\n" + + " combining the Runtime Library with Independent Modules, even if such\n" + + " propagation would otherwise violate the terms of GPLv3, provided that\n" + + " all Target Code was generated by Eligible Compilation Processes. You\n" + + " may then convey such a combination under terms of your choice,\n" + + " consistent with the licensing of the Independent Modules.\n" + + " \n" + + " 2. No Weakening of GCC Copyleft.\n" + + " \n" + + " The availability of this Exception does not imply any general\n" + + " presumption that third-party software is unaffected by the copyleft\n" + + " requirements of the license of GCC.\n" + + " \n" + + " ----\n" + + " \n" + + " GNU GENERAL PUBLIC LICENSE\n" + + " Version 3, 29 June 2007\n" + + " \n" + + " Copyright (C) 2007 Free Software Foundation, Inc. \n" + + " Everyone is permitted to copy and distribute verbatim copies\n" + + " of this license document, but changing it is not allowed.\n" + + " \n" + + " Preamble\n" + + " \n" + + " The GNU General Public License is a free, copyleft license for\n" + + " software and other kinds of works.\n" + + " \n" + + " The licenses for most software and other practical works are designed\n" + + " to take away your freedom to share and change the works. By contrast,\n" + + " the GNU General Public License is intended to guarantee your freedom to\n" + + " share and change all versions of a program--to make sure it remains free\n" + + " software for all its users. We, the Free Software Foundation, use the\n" + + " GNU General Public License for most of our software; it applies also to\n" + + " any other work released this way by its authors. You can apply it to\n" + + " your programs, too.\n" + + " \n" + + " When we speak of free software, we are referring to freedom, not\n" + + " price. Our General Public Licenses are designed to make sure that you\n" + + " have the freedom to distribute copies of free software (and charge for\n" + + " them if you wish), that you receive source code or can get it if you\n" + + " want it, that you can change the software or use pieces of it in new\n" + + " free programs, and that you know you can do these things.\n" + + " \n" + + " To protect your rights, we need to prevent others from denying you\n" + + " these rights or asking you to surrender the rights. Therefore, you have\n" + + " certain responsibilities if you distribute copies of the software, or if\n" + + " you modify it: responsibilities to respect the freedom of others.\n" + + " \n" + + " For example, if you distribute copies of such a program, whether\n" + + " gratis or for a fee, you must pass on to the recipients the same\n" + + " freedoms that you received. You must make sure that they, too, receive\n" + + " or can get the source code. And you must show them these terms so they\n" + + " know their rights.\n" + + " \n" + + " Developers that use the GNU GPL protect your rights with two steps:\n" + + " (1) assert copyright on the software, and (2) offer you this License\n" + + " giving you legal permission to copy, distribute and/or modify it.\n" + + " \n" + + " For the developers' and authors' protection, the GPL clearly explains\n" + + " that there is no warranty for this free software. For both users' and\n" + + " authors' sake, the GPL requires that modified versions be marked as\n" + + " changed, so that their problems will not be attributed erroneously to\n" + + " authors of previous versions.\n" + + " \n" + + " Some devices are designed to deny users access to install or run\n" + + " modified versions of the software inside them, although the manufacturer\n" + + " can do so. This is fundamentally incompatible with the aim of\n" + + " protecting users' freedom to change the software. The systematic\n" + + " pattern of such abuse occurs in the area of products for individuals to\n" + + " use, which is precisely where it is most unacceptable. Therefore, we\n" + + " have designed this version of the GPL to prohibit the practice for those\n" + + " products. If such problems arise substantially in other domains, we\n" + + " stand ready to extend this provision to those domains in future versions\n" + + " of the GPL, as needed to protect the freedom of users.\n" + + " \n" + + " Finally, every program is threatened constantly by software patents.\n" + + " States should not allow patents to restrict development and use of\n" + + " software on general-purpose computers, but in those that do, we wish to\n" + + " avoid the special danger that patents applied to a free program could\n" + + " make it effectively proprietary. To prevent this, the GPL assures that\n" + + " patents cannot be used to render the program non-free.\n" + + " \n" + + " The precise terms and conditions for copying, distribution and\n" + + " modification follow.\n" + + " \n" + + " TERMS AND CONDITIONS\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " \"This License\" refers to version 3 of the GNU General Public License.\n" + + " \n" + + " \"Copyright\" also means copyright-like laws that apply to other kinds" + + " of\n" + + " works, such as semiconductor masks.\n" + + " \n" + + " \"The Program\" refers to any copyrightable work licensed under this\n" + + " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + + " \"recipients\" may be individuals or organizations.\n" + + " \n" + + " To \"modify\" a work means to copy from or adapt all or part of the work\n" + + " in a fashion requiring copyright permission, other than the making of an\n" + + " exact copy. The resulting work is called a \"modified version\" of the\n" + + " earlier work or a work \"based on\" the earlier work.\n" + + " \n" + + " A \"covered work\" means either the unmodified Program or a work based\n" + + " on the Program.\n" + + " \n" + + " To \"propagate\" a work means to do anything with it that, without\n" + + " permission, would make you directly or secondarily liable for\n" + + " infringement under applicable copyright law, except executing it on a\n" + + " computer or modifying a private copy. Propagation includes copying,\n" + + " distribution (with or without modification), making available to the\n" + + " public, and in some countries other activities as well.\n" + + " \n" + + " To \"convey\" a work means any kind of propagation that enables other\n" + + " parties to make or receive copies. Mere interaction with a user through\n" + + " a computer network, with no transfer of a copy, is not conveying.\n" + + " \n" + + " An interactive user interface displays \"Appropriate Legal Notices\"\n" + + " to the extent that it includes a convenient and prominently visible\n" + + " feature that (1) displays an appropriate copyright notice, and (2)\n" + + " tells the user that there is no warranty for the work (except to the\n" + + " extent that warranties are provided), that licensees may convey the\n" + + " work under this License, and how to view a copy of this License. If\n" + + " the interface presents a list of user commands or options, such as a\n" + + " menu, a prominent item in the list meets this criterion.\n" + + " \n" + + " 1. Source Code.\n" + + " \n" + + " The \"source code\" for a work means the preferred form of the work\n" + + " for making modifications to it. \"Object code\" means any non-source\n" + + " form of a work.\n" + + " \n" + + " A \"Standard Interface\" means an interface that either is an official\n" + + " standard defined by a recognized standards body, or, in the case of\n" + + " interfaces specified for a particular programming language, one that\n" + + " is widely used among developers working in that language.\n" + + " \n" + + " The \"System Libraries\" of an executable work include anything, other\n" + + " than the work as a whole, that (a) is included in the normal form of\n" + + " packaging a Major Component, but which is not part of that Major\n" + + " Component, and (b) serves only to enable use of the work with that\n" + + " Major Component, or to implement a Standard Interface for which an\n" + + " implementation is available to the public in source code form. A\n" + + " \"Major Component\", in this context, means a major essential component\n" + + " (kernel, window system, and so on) of the specific operating system\n" + + " (if any) on which the executable work runs, or a compiler used to\n" + + " produce the work, or an object code interpreter used to run it.\n" + + " \n" + + " The \"Corresponding Source\" for a work in object code form means all\n" + + " the source code needed to generate, install, and (for an executable\n" + + " work) run the object code and to modify the work, including scripts to\n" + + " control those activities. However, it does not include the work's\n" + + " System Libraries, or general-purpose tools or generally available free\n" + + " programs which are used unmodified in performing those activities but\n" + + " which are not part of the work. For example, Corresponding Source\n" + + " includes interface definition files associated with source files for\n" + + " the work, and the source code for shared libraries and dynamically\n" + + " linked subprograms that the work is specifically designed to require,\n" + + " such as by intimate data communication or control flow between those\n" + + " subprograms and other parts of the work.\n" + + " \n" + + " The Corresponding Source need not include anything that users\n" + + " can regenerate automatically from other parts of the Corresponding\n" + + " Source.\n" + + " \n" + + " The Corresponding Source for a work in source code form is that\n" + + " same work.\n" + + " \n" + + " 2. Basic Permissions.\n" + + " \n" + + " All rights granted under this License are granted for the term of\n" + + " copyright on the Program, and are irrevocable provided the stated\n" + + " conditions are met. This License explicitly affirms your unlimited\n" + + " permission to run the unmodified Program. The output from running a\n" + + " covered work is covered by this License only if the output, given its\n" + + " content, constitutes a covered work. This License acknowledges your\n" + + " rights of fair use or other equivalent, as provided by copyright law.\n" + + " \n" + + " You may make, run and propagate covered works that you do not\n" + + " convey, without conditions so long as your license otherwise remains\n" + + " in force. You may convey covered works to others for the sole purpose\n" + + " of having them make modifications exclusively for you, or provide you\n" + + " with facilities for running those works, provided that you comply with\n" + + " the terms of this License in conveying all material for which you do\n" + + " not control copyright. Those thus making or running the covered works\n" + + " for you must do so exclusively on your behalf, under your direction\n" + + " and control, on terms that prohibit them from making any copies of\n" + + " your copyrighted material outside their relationship with you.\n" + + " \n" + + " Conveying under any other circumstances is permitted solely under\n" + + " the conditions stated below. Sublicensing is not allowed; section 10\n" + + " makes it unnecessary.\n" + + " \n" + + " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + + " \n" + + " No covered work shall be deemed part of an effective technological\n" + + " measure under any applicable law fulfilling obligations under article\n" + + " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + + " similar laws prohibiting or restricting circumvention of such\n" + + " measures.\n" + + " \n" + + " When you convey a covered work, you waive any legal power to forbid\n" + + " circumvention of technological measures to the extent such circumvention\n" + + " is effected by exercising rights under this License with respect to\n" + + " the covered work, and you disclaim any intention to limit operation or\n" + + " modification of the work as a means of enforcing, against the work's\n" + + " users, your or third parties' legal rights to forbid circumvention of\n" + + " technological measures.\n" + + " \n" + + " 4. Conveying Verbatim Copies.\n" + + " \n" + + " You may convey verbatim copies of the Program's source code as you\n" + + " receive it, in any medium, provided that you conspicuously and\n" + + " appropriately publish on each copy an appropriate copyright notice;\n" + + " keep intact all notices stating that this License and any\n" + + " non-permissive terms added in accord with section 7 apply to the code;\n" + + " keep intact all notices of the absence of any warranty; and give all\n" + + " recipients a copy of this License along with the Program.\n" + + " \n" + + " You may charge any price or no price for each copy that you convey,\n" + + " and you may offer support or warranty protection for a fee.\n" + + " \n" + + " 5. Conveying Modified Source Versions.\n" + + " \n" + + " You may convey a work based on the Program, or the modifications to\n" + + " produce it from the Program, in the form of source code under the\n" + + " terms of section 4, provided that you also meet all of these conditions:\n" + + " \n" + + " a) The work must carry prominent notices stating that you modified\n" + + " it, and giving a relevant date.\n" + + " \n" + + " b) The work must carry prominent notices stating that it is\n" + + " released under this License and any conditions added under section\n" + + " 7. This requirement modifies the requirement in section 4 to\n" + + " \"keep intact all notices\".\n" + + " \n" + + " c) You must license the entire work, as a whole, under this\n" + + " License to anyone who comes into possession of a copy. This\n" + + " License will therefore apply, along with any applicable section 7\n" + + " additional terms, to the whole of the work, and all its parts,\n" + + " regardless of how they are packaged. This License gives no\n" + + " permission to license the work in any other way, but it does not\n" + + " invalidate such permission if you have separately received it.\n" + + " \n" + + " d) If the work has interactive user interfaces, each must display\n" + + " Appropriate Legal Notices; however, if the Program has interactive\n" + + " interfaces that do not display Appropriate Legal Notices, your\n" + + " work need not make them do so.\n" + + " \n" + + " A compilation of a covered work with other separate and independent\n" + + " works, which are not by their nature extensions of the covered work,\n" + + " and which are not combined with it such as to form a larger program,\n" + + " in or on a volume of a storage or distribution medium, is called an\n" + + " \"aggregate\" if the compilation and its resulting copyright are not\n" + + " used to limit the access or legal rights of the compilation's users\n" + + " beyond what the individual works permit. Inclusion of a covered work\n" + + " in an aggregate does not cause this License to apply to the other\n" + + " parts of the aggregate.\n" + + " \n" + + " 6. Conveying Non-Source Forms.\n" + + " \n" + + " You may convey a covered work in object code form under the terms\n" + + " of sections 4 and 5, provided that you also convey the\n" + + " machine-readable Corresponding Source under the terms of this License,\n" + + " in one of these ways:\n" + + " \n" + + " a) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by the\n" + + " Corresponding Source fixed on a durable physical medium\n" + + " customarily used for software interchange.\n" + + " \n" + + " b) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by a\n" + + " written offer, valid for at least three years and valid for as\n" + + " long as you offer spare parts or customer support for that product\n" + + " model, to give anyone who possesses the object code either (1) a\n" + + " copy of the Corresponding Source for all the software in the\n" + + " product that is covered by this License, on a durable physical\n" + + " medium customarily used for software interchange, for a price no\n" + + " more than your reasonable cost of physically performing this\n" + + " conveying of source, or (2) access to copy the\n" + + " Corresponding Source from a network server at no charge.\n" + + " \n" + + " c) Convey individual copies of the object code with a copy of the\n" + + " written offer to provide the Corresponding Source. This\n" + + " alternative is allowed only occasionally and noncommercially, and\n" + + " only if you received the object code with such an offer, in accord\n" + + " with subsection 6b.\n" + + " \n" + + " d) Convey the object code by offering access from a designated\n" + + " place (gratis or for a charge), and offer equivalent access to the\n" + + " Corresponding Source in the same way through the same place at no\n" + + " further charge. You need not require recipients to copy the\n" + + " Corresponding Source along with the object code. If the place to\n" + + " copy the object code is a network server, the Corresponding Source\n" + + " may be on a different server (operated by you or a third party)\n" + + " that supports equivalent copying facilities, provided you maintain\n" + + " clear directions next to the object code saying where to find the\n" + + " Corresponding Source. Regardless of what server hosts the\n" + + " Corresponding Source, you remain obligated to ensure that it is\n" + + " available for as long as needed to satisfy these requirements.\n" + + " \n" + + " e) Convey the object code using peer-to-peer transmission, provided\n" + + " you inform other peers where the object code and Corresponding\n" + + " Source of the work are being offered to the general public at no\n" + + " charge under subsection 6d.\n" + + " \n" + + " A separable portion of the object code, whose source code is excluded\n" + + " from the Corresponding Source as a System Library, need not be\n" + + " included in conveying the object code work.\n" + + " \n" + + " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + + " tangible personal property which is normally used for personal, family,\n" + + " or household purposes, or (2) anything designed or sold for incorporation\n" + + " into a dwelling. In determining whether a product is a consumer product,\n" + + " doubtful cases shall be resolved in favor of coverage. For a particular\n" + + " product received by a particular user, \"normally used\" refers to a\n" + + " typical or common use of that class of product, regardless of the status\n" + + " of the particular user or of the way in which the particular user\n" + + " actually uses, or expects or is expected to use, the product. A product\n" + + " is a consumer product regardless of whether the product has substantial\n" + + " commercial, industrial or non-consumer uses, unless such uses represent\n" + + " the only significant mode of use of the product.\n" + + " \n" + + " \"Installation Information\" for a User Product means any methods,\n" + + " procedures, authorization keys, or other information required to install\n" + + " and execute modified versions of a covered work in that User Product from\n" + + " a modified version of its Corresponding Source. The information must\n" + + " suffice to ensure that the continued functioning of the modified object\n" + + " code is in no case prevented or interfered with solely because\n" + + " modification has been made.\n" + + " \n" + + " If you convey an object code work under this section in, or with, or\n" + + " specifically for use in, a User Product, and the conveying occurs as\n" + + " part of a transaction in which the right of possession and use of the\n" + + " User Product is transferred to the recipient in perpetuity or for a\n" + + " fixed term (regardless of how the transaction is characterized), the\n" + + " Corresponding Source conveyed under this section must be accompanied\n" + + " by the Installation Information. But this requirement does not apply\n" + + " if neither you nor any third party retains the ability to install\n" + + " modified object code on the User Product (for example, the work has\n" + + " been installed in ROM).\n" + + " \n" + + " The requirement to provide Installation Information does not include a\n" + + " requirement to continue to provide support service, warranty, or updates\n" + + " for a work that has been modified or installed by the recipient, or for\n" + + " the User Product in which it has been modified or installed. Access to a\n" + + " network may be denied when the modification itself materially and\n" + + " adversely affects the operation of the network or violates the rules and\n" + + " protocols for communication across the network.\n" + + " \n" + + " Corresponding Source conveyed, and Installation Information provided,\n" + + " in accord with this section must be in a format that is publicly\n" + + " documented (and with an implementation available to the public in\n" + + " source code form), and must require no special password or key for\n" + + " unpacking, reading or copying.\n" + + " \n" + + " 7. Additional Terms.\n" + + " \n" + + " \"Additional permissions\" are terms that supplement the terms of this\n" + + " License by making exceptions from one or more of its conditions.\n" + + " Additional permissions that are applicable to the entire Program shall\n" + + " be treated as though they were included in this License, to the extent\n" + + " that they are valid under applicable law. If additional permissions\n" + + " apply only to part of the Program, that part may be used separately\n" + + " under those permissions, but the entire Program remains governed by\n" + + " this License without regard to the additional permissions.\n" + + " \n" + + " When you convey a copy of a covered work, you may at your option\n" + + " remove any additional permissions from that copy, or from any part of\n" + + " it. (Additional permissions may be written to require their own\n" + + " removal in certain cases when you modify the work.) You may place\n" + + " additional permissions on material, added by you to a covered work,\n" + + " for which you have or can give appropriate copyright permission.\n" + + " \n" + + " Notwithstanding any other provision of this License, for material you\n" + + " add to a covered work, you may (if authorized by the copyright holders of\n" + + " that material) supplement the terms of this License with terms:\n" + + " \n" + + " a) Disclaiming warranty or limiting liability differently from the\n" + + " terms of sections 15 and 16 of this License; or\n" + + " \n" + + " b) Requiring preservation of specified reasonable legal notices or\n" + + " author attributions in that material or in the Appropriate Legal\n" + + " Notices displayed by works containing it; or\n" + + " \n" + + " c) Prohibiting misrepresentation of the origin of that material, or\n" + + " requiring that modified versions of such material be marked in\n" + + " reasonable ways as different from the original version; or\n" + + " \n" + + " d) Limiting the use for publicity purposes of names of licensors or\n" + + " authors of the material; or\n" + + " \n" + + " e) Declining to grant rights under trademark law for use of some\n" + + " trade names, trademarks, or service marks; or\n" + + " \n" + + " f) Requiring indemnification of licensors and authors of that\n" + + " material by anyone who conveys the material (or modified versions of\n" + + " it) with contractual assumptions of liability to the recipient, for\n" + + " any liability that these contractual assumptions directly impose on\n" + + " those licensors and authors.\n" + + " \n" + + " All other non-permissive additional terms are considered \"further\n" + + " restrictions\" within the meaning of section 10. If the Program as you\n" + + " received it, or any part of it, contains a notice stating that it is\n" + + " governed by this License along with a term that is a further\n" + + " restriction, you may remove that term. If a license document contains\n" + + " a further restriction but permits relicensing or conveying under this\n" + + " License, you may add to a covered work material governed by the terms\n" + + " of that license document, provided that the further restriction does\n" + + " not survive such relicensing or conveying.\n" + + " \n" + + " If you add terms to a covered work in accord with this section, you\n" + + " must place, in the relevant source files, a statement of the\n" + + " additional terms that apply to those files, or a notice indicating\n" + + " where to find the applicable terms.\n" + + " \n" + + " Additional terms, permissive or non-permissive, may be stated in the\n" + + " form of a separately written license, or stated as exceptions;\n" + + " the above requirements apply either way.\n" + + " \n" + + " 8. Termination.\n" + + " \n" + + " You may not propagate or modify a covered work except as expressly\n" + + " provided under this License. Any attempt otherwise to propagate or\n" + + " modify it is void, and will automatically terminate your rights under\n" + + " this License (including any patent licenses granted under the third\n" + + " paragraph of section 11).\n" + + " \n" + + " However, if you cease all violation of this License, then your\n" + + " license from a particular copyright holder is reinstated (a)\n" + + " provisionally, unless and until the copyright holder explicitly and\n" + + " finally terminates your license, and (b) permanently, if the copyright\n" + + " holder fails to notify you of the violation by some reasonable means\n" + + " prior to 60 days after the cessation.\n" + + " \n" + + " Moreover, your license from a particular copyright holder is\n" + + " reinstated permanently if the copyright holder notifies you of the\n" + + " violation by some reasonable means, this is the first time you have\n" + + " received notice of violation of this License (for any work) from that\n" + + " copyright holder, and you cure the violation prior to 30 days after\n" + + " your receipt of the notice.\n" + + " \n" + + " Termination of your rights under this section does not terminate the\n" + + " licenses of parties who have received copies or rights from you under\n" + + " this License. If your rights have been terminated and not permanently\n" + + " reinstated, you do not qualify to receive new licenses for the same\n" + + " material under section 10.\n" + + " \n" + + " 9. Acceptance Not Required for Having Copies.\n" + + " \n" + + " You are not required to accept this License in order to receive or\n" + + " run a copy of the Program. Ancillary propagation of a covered work\n" + + " occurring solely as a consequence of using peer-to-peer transmission\n" + + " to receive a copy likewise does not require acceptance. However,\n" + + " nothing other than this License grants you permission to propagate or\n" + + " modify any covered work. These actions infringe copyright if you do\n" + + " not accept this License. Therefore, by modifying or propagating a\n" + + " covered work, you indicate your acceptance of this License to do so.\n" + + " \n" + + " 10. Automatic Licensing of Downstream Recipients.\n" + + " \n" + + " Each time you convey a covered work, the recipient automatically\n" + + " receives a license from the original licensors, to run, modify and\n" + + " propagate that work, subject to this License. You are not responsible\n" + + " for enforcing compliance by third parties with this License.\n" + + " \n" + + " An \"entity transaction\" is a transaction transferring control of an\n" + + " organization, or substantially all assets of one, or subdividing an\n" + + " organization, or merging organizations. If propagation of a covered\n" + + " work results from an entity transaction, each party to that\n" + + " transaction who receives a copy of the work also receives whatever\n" + + " licenses to the work the party's predecessor in interest had or could\n" + + " give under the previous paragraph, plus a right to possession of the\n" + + " Corresponding Source of the work from the predecessor in interest, if\n" + + " the predecessor has it or can get it with reasonable efforts.\n" + + " \n" + + " You may not impose any further restrictions on the exercise of the\n" + + " rights granted or affirmed under this License. For example, you may\n" + + " not impose a license fee, royalty, or other charge for exercise of\n" + + " rights granted under this License, and you may not initiate litigation\n" + + " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + + " any patent claim is infringed by making, using, selling, offering for\n" + + " sale, or importing the Program or any portion of it.\n" + + " \n" + + " 11. Patents.\n" + + " \n" + + " A \"contributor\" is a copyright holder who authorizes use under this\n" + + " License of the Program or a work on which the Program is based. The\n" + + " work thus licensed is called the contributor's \"contributor version\".\n" + + " \n" + + " A contributor's \"essential patent claims\" are all patent claims\n" + + " owned or controlled by the contributor, whether already acquired or\n" + + " hereafter acquired, that would be infringed by some manner, permitted\n" + + " by this License, of making, using, or selling its contributor version,\n" + + " but do not include claims that would be infringed only as a\n" + + " consequence of further modification of the contributor version. For\n" + + " purposes of this definition, \"control\" includes the right to grant\n" + + " patent sublicenses in a manner consistent with the requirements of\n" + + " this License.\n" + + " \n" + + " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + + " patent license under the contributor's essential patent claims, to\n" + + " make, use, sell, offer for sale, import and otherwise run, modify and\n" + + " propagate the contents of its contributor version.\n" + + " \n" + + " In the following three paragraphs, a \"patent license\" is any express\n" + + " agreement or commitment, however denominated, not to enforce a patent\n" + + " (such as an express permission to practice a patent or covenant not to\n" + + " sue for patent infringement). To \"grant\" such a patent license to a\n" + + " party means to make such an agreement or commitment not to enforce a\n" + + " patent against the party.\n" + + " \n" + + " If you convey a covered work, knowingly relying on a patent license,\n" + + " and the Corresponding Source of the work is not available for anyone\n" + + " to copy, free of charge and under the terms of this License, through a\n" + + " publicly available network server or other readily accessible means,\n" + + " then you must either (1) cause the Corresponding Source to be so\n" + + " available, or (2) arrange to deprive yourself of the benefit of the\n" + + " patent license for this particular work, or (3) arrange, in a manner\n" + + " consistent with the requirements of this License, to extend the patent\n" + + " license to downstream recipients. \"Knowingly relying\" means you have\n" + + " actual knowledge that, but for the patent license, your conveying the\n" + + " covered work in a country, or your recipient's use of the covered work\n" + + " in a country, would infringe one or more identifiable patents in that\n" + + " country that you have reason to believe are valid.\n" + + " \n" + + " If, pursuant to or in connection with a single transaction or\n" + + " arrangement, you convey, or propagate by procuring conveyance of, a\n" + + " covered work, and grant a patent license to some of the parties\n" + + " receiving the covered work authorizing them to use, propagate, modify\n" + + " or convey a specific copy of the covered work, then the patent license\n" + + " you grant is automatically extended to all recipients of the covered\n" + + " work and works based on it.\n" + + " \n" + + " A patent license is \"discriminatory\" if it does not include within\n" + + " the scope of its coverage, prohibits the exercise of, or is\n" + + " conditioned on the non-exercise of one or more of the rights that are\n" + + " specifically granted under this License. You may not convey a covered\n" + + " work if you are a party to an arrangement with a third party that is\n" + + " in the business of distributing software, under which you make payment\n" + + " to the third party based on the extent of your activity of conveying\n" + + " the work, and under which the third party grants, to any of the\n" + + " parties who would receive the covered work from you, a discriminatory\n" + + " patent license (a) in connection with copies of the covered work\n" + + " conveyed by you (or copies made from those copies), or (b) primarily\n" + + " for and in connection with specific products or compilations that\n" + + " contain the covered work, unless you entered into that arrangement,\n" + + " or that patent license was granted, prior to 28 March 2007.\n" + + " \n" + + " Nothing in this License shall be construed as excluding or limiting\n" + + " any implied license or other defenses to infringement that may\n" + + " otherwise be available to you under applicable patent law.\n" + + " \n" + + " 12. No Surrender of Others' Freedom.\n" + + " \n" + + " If conditions are imposed on you (whether by court order, agreement or\n" + + " otherwise) that contradict the conditions of this License, they do not\n" + + " excuse you from the conditions of this License. If you cannot convey a\n" + + " covered work so as to satisfy simultaneously your obligations under this\n" + + " License and any other pertinent obligations, then as a consequence you may\n" + + " not convey it at all. For example, if you agree to terms that obligate" + + " you\n" + + " to collect a royalty for further conveying from those to whom you convey\n" + + " the Program, the only way you could satisfy both those terms and this\n" + + " License would be to refrain entirely from conveying the Program.\n" + + " \n" + + " 13. Use with the GNU Affero General Public License.\n" + + " \n" + + " Notwithstanding any other provision of this License, you have\n" + + " permission to link or combine any covered work with a work licensed\n" + + " under version 3 of the GNU Affero General Public License into a single\n" + + " combined work, and to convey the resulting work. The terms of this\n" + + " License will continue to apply to the part which is the covered work,\n" + + " but the special requirements of the GNU Affero General Public License,\n" + + " section 13, concerning interaction through a network will apply to the\n" + + " combination as such.\n" + + " \n" + + " 14. Revised Versions of this License.\n" + + " \n" + + " The Free Software Foundation may publish revised and/or new versions of\n" + + " the GNU General Public License from time to time. Such new versions will\n" + + " be similar in spirit to the present version, but may differ in detail to\n" + + " address new problems or concerns.\n" + + " \n" + + " Each version is given a distinguishing version number. If the\n" + + " Program specifies that a certain numbered version of the GNU General\n" + + " Public License \"or any later version\" applies to it, you have the\n" + + " option of following the terms and conditions either of that numbered\n" + + " version or of any later version published by the Free Software\n" + + " Foundation. If the Program does not specify a version number of the\n" + + " GNU General Public License, you may choose any version ever published\n" + + " by the Free Software Foundation.\n" + + " \n" + + " If the Program specifies that a proxy can decide which future\n" + + " versions of the GNU General Public License can be used, that proxy's\n" + + " public statement of acceptance of a version permanently authorizes you\n" + + " to choose that version for the Program.\n" + + " \n" + + " Later license versions may give you additional or different\n" + + " permissions. However, no additional obligations are imposed on any\n" + + " author or copyright holder as a result of your choosing to follow a\n" + + " later version.\n" + + " \n" + + " 15. Disclaimer of Warranty.\n" + + " \n" + + " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + + " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + + " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT" + + " WARRANTY\n" + + " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + + " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + + " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + + " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + + " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + + " \n" + + " 16. Limitation of Liability.\n" + + " \n" + + " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + + " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + + " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING" + + " ANY\n" + + " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + + " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + + " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + + " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + + " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + + " SUCH DAMAGES.\n" + + " \n" + + " 17. Interpretation of Sections 15 and 16.\n" + + " \n" + + " If the disclaimer of warranty and limitation of liability provided\n" + + " above cannot be given local legal effect according to their terms,\n" + + " reviewing courts shall apply local law that most closely approximates\n" + + " an absolute waiver of all civil liability in connection with the\n" + + " Program, unless a warranty or assumption of liability accompanies a\n" + + " copy of the Program in return for a fee.\n" + + " \n" + + " END OF TERMS AND CONDITIONS\n" + + " \n" + + " How to Apply These Terms to Your New Programs\n" + + " \n" + + " If you develop a new program, and you want it to be of the greatest\n" + + " possible use to the public, the best way to achieve this is to make it\n" + + " free software which everyone can redistribute and change under these" + + " terms.\n" + + " \n" + + " To do so, attach the following notices to the program. It is safest\n" + + " to attach them to the start of each source file to most effectively\n" + + " state the exclusion of warranty; and each file should have at least\n" + + " the \"copyright\" line and a pointer to where the full notice is found.\n" + + " \n" + + " \n" + + " Copyright (C) \n" + + " \n" + + " This program is free software: you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation, either version 3 of the License, or\n" + + " (at your option) any later version.\n" + + " \n" + + " This program is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " You should have received a copy of the GNU General Public License\n" + + " along with this program. If not, see .\n" + + " \n" + + " Also add information on how to contact you by electronic and paper mail.\n" + + " \n" + + " If the program does terminal interaction, make it output a short\n" + + " notice like this when it starts in an interactive mode:\n" + + " \n" + + " Copyright (C) \n" + + " This program comes with ABSOLUTELY NO WARRANTY; for details type `show" + + " w'.\n" + + " This is free software, and you are welcome to redistribute it\n" + + " under certain conditions; type `show c' for details.\n" + + " \n" + + " The hypothetical commands `show w' and `show c' should show the" + + " appropriate\n" + + " parts of the General Public License. Of course, your program's commands\n" + + " might be different; for a GUI interface, you would use an \"about box\".\n" + + " \n" + + " You should also get your employer (if you work as a programmer) or" + + " school,\n" + + " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + + " For more information on this, and how to apply and follow the GNU GPL, see\n" + + " .\n" + + " \n" + + " The GNU General Public License does not permit incorporating your" + + " program\n" + + " into proprietary programs. If your program is a subroutine library, you\n" + + " may consider it more useful to permit linking proprietary applications" + + " with\n" + + " the library. If this is what you want to do, use the GNU Lesser General\n" + + " Public License instead of this License. But first, please read\n" + + " .\n" + + " \n" + + " \n" + + " Name: libquadmath\n" + + " Files: scipy/.dylibs/libquadmath*.so\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + + " License: LGPL-2.1-or-later\n" + + " \n" + + " GCC Quad-Precision Math Library\n" + + " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + + " Written by Francois-Xavier Coudert \n" + + " \n" + + " This file is part of the libquadmath library.\n" + + " Libquadmath is free software; you can redistribute it and/or\n" + + " modify it under the terms of the GNU Library General Public\n" + + " License as published by the Free Software Foundation; either\n" + + " version 2.1 of the License, or (at your option) any later version.\n" + + " \n" + + " Libquadmath is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + + " Lesser General Public License for more details.\n" + + " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: numpy\n" + + "Required-by: gensim"); - EXPECTED_PIP_SHOW_RESULTS.add("Name: six\n" + - "Version: 1.16.0\n" + - "Summary: Python 2 and 3 compatibility utilities\n" + - "Home-page: https://github.com/benjaminp/six\n" + - "Author: Benjamin Peterson\n" + - "Author-email: benjamin@python.org\n" + - "License: MIT\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: \n" + - "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"); + EXPECTED_PIP_SHOW_RESULTS.add( + "Name: six\n" + + "Version: 1.16.0\n" + + "Summary: Python 2 and 3 compatibility utilities\n" + + "Home-page: https://github.com/benjaminp/six\n" + + "Author: Benjamin Peterson\n" + + "Author-email: benjamin@python.org\n" + + "License: MIT\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: \n" + + "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"); } } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java index 68eebd72..8b004967 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java @@ -15,262 +15,311 @@ */ package com.redhat.exhort.utils; +import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipFreeze; +import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipShow; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; + import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentMatcher; import org.mockito.MockedStatic; import org.mockito.Mockito; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; - -import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipFreeze; -import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipShow; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; - class PythonControllerRealEnvTest extends ExhortTest { private static PythonControllerRealEnv pythonControllerRealEnv; - private final String PIP_FREEZE_LINES_CYCLIC = getStringFromFile("msc","python","pip_freeze_lines_cyclic.txt"); - private final String PIP_SHOW_LINES_CYCLIC = getStringFromFile("msc","python","pip_show_lines_cyclic.txt"); - -// ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { -// @Override -// public boolean matches(String[] command) { -// return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); -// } -// // in var args, must override type default method' void.class in argumentMatcher interface in order to let custom ArgumentMatcher work correctly. -// @Override -// public Class type() -// { -// return String[].class; -// } -// -// }; -// -// ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { -// @Override -// public boolean matches(String[] command) { -// return Arrays.stream(command).anyMatch(word -> word.contains("show")); -// } -// -// @Override -// public Class type() -// { -// return String[].class; -// } -// -// }; - + private final String PIP_FREEZE_LINES_CYCLIC = + getStringFromFile("msc", "python", "pip_freeze_lines_cyclic.txt"); + private final String PIP_SHOW_LINES_CYCLIC = + getStringFromFile("msc", "python", "pip_show_lines_cyclic.txt"); + + // ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { + // @Override + // public boolean matches(String[] command) { + // return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); + // } + // // in var args, must override type default method' void.class in argumentMatcher interface + // in order to let + // custom ArgumentMatcher work correctly. + // @Override + // public Class type() + // { + // return String[].class; + // } + // + // }; + // + // ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { + // @Override + // public boolean matches(String[] command) { + // return Arrays.stream(command).anyMatch(word -> word.contains("show")); + // } + // + // @Override + // public Class type() + // { + // return String[].class; + // } + // + // }; @BeforeEach - void setUp() { - pythonControllerRealEnv = new PythonControllerRealEnv("python3","pip3"); + void setUp() { + pythonControllerRealEnv = new PythonControllerRealEnv("python3", "pip3"); } @AfterEach - void tearDown() { - } - + void tearDown() {} @ParameterizedTest - @ValueSource(booleans = { true,false }) + @ValueSource(booleans = {true, false}) void get_Dependencies_With_Match_Manifest_Versions(boolean MatchManifestVersionsEnabled) { - Set expectedSetOfPackages = Set.of("click", "flask", "importlib-metadata", "zipp", "itsdangerous", "jinja2", "MarkupSafe", "Werkzeug", "dataclasses", "typing-extensions"); + Set expectedSetOfPackages = + Set.of( + "click", + "flask", + "importlib-metadata", + "zipp", + "itsdangerous", + "jinja2", + "MarkupSafe", + "Werkzeug", + "dataclasses", + "typing-extensions"); MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); String requirementsPath = getFileFromString("requirements.txt", "Flask==2.0.3\nclick==8.0.5\n"); - String pipFreeze = "click==8.0.4\nflask==2.0.3\nimportlib-metadata==4.8.3\nzipp==3.6.0\nitsdangerous==2.0.1\njinja2==3.0.3\nMarkupSafe==2.0.1\nWerkzeug==2.0.3\ndataclasses==0.8\ntyping_extensions==4.1.1\n"; - String pipShowResults = "Name: click\n" + - "Version: 8.0.4\n" + - "Summary: Composable command line interface toolkit\n" + - "Home-page: https://palletsprojects.com/p/click/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: importlib-metadata\n" + - "Required-by: Flask, uvicorn\n" + - "---\n" + - "Name: Flask\n" + - "Version: 2.0.3\n" + - "Summary: A simple framework for building complex web applications.\n" + - "Home-page: https://palletsprojects.com/p/flask\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: click, itsdangerous, Jinja2, Werkzeug\n" + - "Required-by: \n" + - "---\n" + - "Name: importlib-metadata\n" + - "Version: 4.8.3\n" + - "Summary: Read metadata from Python packages\n" + - "Home-page: https://github.com/python/importlib_metadata\n" + - "Author: Jason R. Coombs\n" + - "Author-email: jaraco@jaraco.com\n" + - "License: UNKNOWN\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: typing-extensions, zipp\n" + - "Required-by: click, cyclonedx-bom, cyclonedx-python-lib\n" + - "---\n" + - "Name: zipp\n" + - "Version: 3.6.0\n" + - "Summary: Backport of pathlib-compatible object wrapper for zip files\n" + - "Home-page: https://github.com/jaraco/zipp\n" + - "Author: Jason R. Coombs\n" + - "Author-email: jaraco@jaraco.com\n" + - "License: UNKNOWN\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: importlib-metadata\n" + - "---\n" + - "Name: itsdangerous\n" + - "Version: 2.0.1\n" + - "Summary: Safely pass data to untrusted environments and back.\n" + - "Home-page: https://palletsprojects.com/p/itsdangerous/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: Flask\n" + - "---\n" + - "Name: Jinja2\n" + - "Version: 3.0.3\n" + - "Summary: A very fast and expressive template engine.\n" + - "Home-page: https://palletsprojects.com/p/jinja/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + - "Requires: MarkupSafe\n" + - "Required-by: ansible-core, Flask\n" + - "---\n" + - "Name: MarkupSafe\n" + - "Version: 2.0.1\n" + - "Summary: Safely add untrusted strings to HTML/XML markup.\n" + - "Home-page: https://palletsprojects.com/p/markupsafe/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: Jinja2, Mako\n" + - "---\n" + - "Name: Werkzeug\n" + - "Version: 2.0.3\n" + - "Summary: The comprehensive WSGI web application library.\n" + - "Home-page: https://palletsprojects.com/p/werkzeug/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: dataclasses\n" + - "Required-by: Flask\n" + - "---\n" + - "Name: dataclasses\n" + - "Version: 0.8\n" + - "Summary: A backport of the dataclasses module for Python 3.6\n" + - "Home-page: https://github.com/ericvsmith/dataclasses\n" + - "Author: Eric V. Smith\n" + - "Author-email: eric@python.org\n" + - "License: Apache\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: anyio, h11, pydantic, Werkzeug\n" + - "---\n" + - "Name: typing_extensions\n" + - "Version: 4.1.1\n" + - "Summary: Backported and Experimental Type Hints for Python 3.6+\n" + - "Home-page: \n" + - "Author: \n" + - "Author-email: \"Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee\" \n" + - "License: \n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: anyio, asgiref, h11, immutables, importlib-metadata, pydantic, starlette, uvicorn\n"; - - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))).thenReturn(pipFreeze); - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))).thenReturn(pipShowResults); + String pipFreeze = + "click==8.0.4\n" + + "flask==2.0.3\n" + + "importlib-metadata==4.8.3\n" + + "zipp==3.6.0\n" + + "itsdangerous==2.0.1\n" + + "jinja2==3.0.3\n" + + "MarkupSafe==2.0.1\n" + + "Werkzeug==2.0.3\n" + + "dataclasses==0.8\n" + + "typing_extensions==4.1.1\n"; + String pipShowResults = + "Name: click\n" + + "Version: 8.0.4\n" + + "Summary: Composable command line interface toolkit\n" + + "Home-page: https://palletsprojects.com/p/click/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: importlib-metadata\n" + + "Required-by: Flask, uvicorn\n" + + "---\n" + + "Name: Flask\n" + + "Version: 2.0.3\n" + + "Summary: A simple framework for building complex web applications.\n" + + "Home-page: https://palletsprojects.com/p/flask\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: click, itsdangerous, Jinja2, Werkzeug\n" + + "Required-by: \n" + + "---\n" + + "Name: importlib-metadata\n" + + "Version: 4.8.3\n" + + "Summary: Read metadata from Python packages\n" + + "Home-page: https://github.com/python/importlib_metadata\n" + + "Author: Jason R. Coombs\n" + + "Author-email: jaraco@jaraco.com\n" + + "License: UNKNOWN\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: typing-extensions, zipp\n" + + "Required-by: click, cyclonedx-bom, cyclonedx-python-lib\n" + + "---\n" + + "Name: zipp\n" + + "Version: 3.6.0\n" + + "Summary: Backport of pathlib-compatible object wrapper for zip files\n" + + "Home-page: https://github.com/jaraco/zipp\n" + + "Author: Jason R. Coombs\n" + + "Author-email: jaraco@jaraco.com\n" + + "License: UNKNOWN\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: importlib-metadata\n" + + "---\n" + + "Name: itsdangerous\n" + + "Version: 2.0.1\n" + + "Summary: Safely pass data to untrusted environments and back.\n" + + "Home-page: https://palletsprojects.com/p/itsdangerous/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: Flask\n" + + "---\n" + + "Name: Jinja2\n" + + "Version: 3.0.3\n" + + "Summary: A very fast and expressive template engine.\n" + + "Home-page: https://palletsprojects.com/p/jinja/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + + "Requires: MarkupSafe\n" + + "Required-by: ansible-core, Flask\n" + + "---\n" + + "Name: MarkupSafe\n" + + "Version: 2.0.1\n" + + "Summary: Safely add untrusted strings to HTML/XML markup.\n" + + "Home-page: https://palletsprojects.com/p/markupsafe/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: Jinja2, Mako\n" + + "---\n" + + "Name: Werkzeug\n" + + "Version: 2.0.3\n" + + "Summary: The comprehensive WSGI web application library.\n" + + "Home-page: https://palletsprojects.com/p/werkzeug/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: dataclasses\n" + + "Required-by: Flask\n" + + "---\n" + + "Name: dataclasses\n" + + "Version: 0.8\n" + + "Summary: A backport of the dataclasses module for Python 3.6\n" + + "Home-page: https://github.com/ericvsmith/dataclasses\n" + + "Author: Eric V. Smith\n" + + "Author-email: eric@python.org\n" + + "License: Apache\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: anyio, h11, pydantic, Werkzeug\n" + + "---\n" + + "Name: typing_extensions\n" + + "Version: 4.1.1\n" + + "Summary: Backported and Experimental Type Hints for Python 3.6+\n" + + "Home-page: \n" + + "Author: \n" + + "Author-email: \"Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee\"" + + " \n" + + "License: \n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: anyio, asgiref, h11, immutables, importlib-metadata, pydantic," + + " starlette, uvicorn\n"; + + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))) + .thenReturn(pipFreeze); + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))) + .thenReturn(pipShowResults); if (!MatchManifestVersionsEnabled) { System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); } if (MatchManifestVersionsEnabled) { - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> pythonControllerRealEnv.getDependencies(requirementsPath, true), "Expected getDependencies/2 to throw RuntimeException, due to version mismatch, but it didn't."); + RuntimeException runtimeException = + assertThrows( + RuntimeException.class, + () -> pythonControllerRealEnv.getDependencies(requirementsPath, true), + "Expected getDependencies/2 to throw RuntimeException, due to version mismatch, but" + + " it didn't."); operationsMockedStatic.close(); - assertTrue(runtimeException.getMessage().contains("Can't continue with analysis - versions mismatch for dependency name=click, manifest version=8.0.5, installed Version=8.0.4")); - } - else - { - - List> dependencies = pythonControllerRealEnv.getDependencies(requirementsPath, true); + assertTrue( + runtimeException + .getMessage() + .contains( + "Can't continue with analysis - versions mismatch for dependency name=click," + + " manifest version=8.0.5, installed Version=8.0.4")); + } else { + + List> dependencies = + pythonControllerRealEnv.getDependencies(requirementsPath, true); System.clearProperty("MATCH_MANIFEST_VERSIONS"); // collect all packages returned from getDependencies into Set. System.out.println(dependencies); Set actualSetOfPackages = new HashSet(); - dependencies.forEach( entry -> { - accumulateAllPackages(entry,actualSetOfPackages); - - }); + dependencies.forEach( + entry -> { + accumulateAllPackages(entry, actualSetOfPackages); + }); // Check that all actual collected packages are exactly the ones that are expected - Set expectedSetOfPackagesLC = expectedSetOfPackages.stream().map(packageName -> packageName.replace("_","-")).map(String::toLowerCase).collect(Collectors.toSet()); - - Set actualSetOfPackagesLC = actualSetOfPackages.stream().map(packageName -> packageName.replace("_","-")).map(String::toLowerCase).collect(Collectors.toSet()); + Set expectedSetOfPackagesLC = + expectedSetOfPackages.stream() + .map(packageName -> packageName.replace("_", "-")) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + + Set actualSetOfPackagesLC = + actualSetOfPackages.stream() + .map(packageName -> packageName.replace("_", "-")) + .map(String::toLowerCase) + .collect(Collectors.toSet()); assertTrue(actualSetOfPackagesLC.containsAll(expectedSetOfPackagesLC)); assertTrue(expectedSetOfPackagesLC.containsAll(actualSetOfPackagesLC)); operationsMockedStatic.close(); } - } private void accumulateAllPackages(Map entry, Set actualSetOfPackages) { actualSetOfPackages.add(entry.get("name")); - if(entry.get("dependencies") != null) - { - ((List>)entry.get("dependencies")).stream().forEach( record -> - { - accumulateAllPackages(record,actualSetOfPackages); - }); + if (entry.get("dependencies") != null) { + ((List>) entry.get("dependencies")) + .stream() + .forEach( + record -> { + accumulateAllPackages(record, actualSetOfPackages); + }); } } @Test void get_Dependencies_from_Cyclic_Tree() { MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); -// ArgumentMatcher matchCommandPipFreeze = command -> Arrays.stream(command).anyMatch(word -> word.contains("freeze")); - - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),argThat(matchCommandPipFreeze))).thenReturn(PIP_FREEZE_LINES_CYCLIC); -// operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),any(String[].class))).thenReturn(PIP_FREEZE_LINES_CYCLIC); - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),argThat(matchCommandPipShow))).thenReturn(PIP_SHOW_LINES_CYCLIC); - String requirementsTxt = getFileFromResource("requirements.txt", "msc", "python", "requirements-cyclic-test.txt"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - List> dependencies = pythonControllerRealEnv.getDependencies(requirementsTxt, true); + // ArgumentMatcher matchCommandPipFreeze = command -> + // Arrays.stream(command).anyMatch(word -> + // word.contains("freeze")); + + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))) + .thenReturn(PIP_FREEZE_LINES_CYCLIC); + // operationsMockedStatic.when(() -> + // Operations.runProcessGetOutput(any(Path.class),any(String[].class))).thenReturn(PIP_FREEZE_LINES_CYCLIC); + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))) + .thenReturn(PIP_SHOW_LINES_CYCLIC); + String requirementsTxt = + getFileFromResource("requirements.txt", "msc", "python", "requirements-cyclic-test.txt"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + List> dependencies = + pythonControllerRealEnv.getDependencies(requirementsTxt, true); System.clearProperty("MATCH_MANIFEST_VERSIONS"); - assertEquals(104,dependencies.size()); + assertEquals(104, dependencies.size()); operationsMockedStatic.close(); - } @Test void get_Dependency_Name_requirements() { - assertEquals("something",PythonControllerRealEnv.getDependencyName("something==2.0.5")); - assertEquals("something",PythonControllerRealEnv.getDependencyName("something == 2.0.5")); - assertEquals("something",PythonControllerRealEnv.getDependencyName("something>=2.0.5")); - + assertEquals("something", PythonControllerRealEnv.getDependencyName("something==2.0.5")); + assertEquals("something", PythonControllerRealEnv.getDependencyName("something == 2.0.5")); + assertEquals("something", PythonControllerRealEnv.getDependencyName("something>=2.0.5")); } - - @Test void automaticallyInstallPackageOnEnvironment() { assertFalse(this.pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment()); @@ -286,5 +335,4 @@ void isRealEnv() { void isVirtualEnv() { assertFalse(this.pythonControllerRealEnv.isVirtualEnv()); } - } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java index 0dcda9d8..920e5c8f 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java @@ -15,25 +15,20 @@ */ package com.redhat.exhort.utils; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.Spy; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class PythonControllerVirtualEnvTest extends ExhortTest { @@ -41,20 +36,22 @@ class PythonControllerVirtualEnvTest extends ExhortTest { private static PythonControllerVirtualEnv spiedPythonControllerVirtualEnv; private ObjectMapper om = new ObjectMapper(); + @BeforeAll static void setUp() { pythonControllerVirtualEnv = new PythonControllerVirtualEnv("python3"); spiedPythonControllerVirtualEnv = Mockito.spy(pythonControllerVirtualEnv); - } @Test void test_Virtual_Environment_Install_Best_Efforts() throws JsonProcessingException { - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); - List> dependencies = spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true); + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + String requirementsTxt = + getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); + List> dependencies = + spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true); System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies)); System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); @@ -63,32 +60,35 @@ void test_Virtual_Environment_Install_Best_Efforts() throws JsonProcessingExcept @Test void test_Virtual_Environment_Install_Best_Efforts_Conflict_MMV_Should_Throw_Runtime_Exception() { - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true)); + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + String requirementsTxt = + getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); + RuntimeException runtimeException = + assertThrows( + RuntimeException.class, + () -> spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true)); assertTrue(runtimeException.getMessage().contains("Conflicting settings")); System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - } @Test void test_Virtual_Environment_Flow() throws IOException { -// Mockito + // Mockito String requirementsTxt = "Jinja2==3.0.3"; - Path requirementsFilePath = Path.of(System.getProperty("user.dir").toString(), "requirements.txt"); + Path requirementsFilePath = + Path.of(System.getProperty("user.dir").toString(), "requirements.txt"); Files.write(requirementsFilePath, requirementsTxt.getBytes()); -// MockedStatic operationsMockedStatic = mockStatic(Operations.class); -// when(spiedPythonControllerVirtualEnv.) - List> dependencies = spiedPythonControllerVirtualEnv.getDependencies(requirementsFilePath.toString(), true); + // MockedStatic operationsMockedStatic = mockStatic(Operations.class); + // when(spiedPythonControllerVirtualEnv.) + List> dependencies = + spiedPythonControllerVirtualEnv.getDependencies(requirementsFilePath.toString(), true); verify(spiedPythonControllerVirtualEnv).prepareEnvironment(anyString()); verify(spiedPythonControllerVirtualEnv).installPackages(anyString()); verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); verify(spiedPythonControllerVirtualEnv).automaticallyInstallPackageOnEnvironment(); - verify(spiedPythonControllerVirtualEnv,never()).isRealEnv(); - verify(spiedPythonControllerVirtualEnv,times(2)).isVirtualEnv(); - - + verify(spiedPythonControllerVirtualEnv, never()).isRealEnv(); + verify(spiedPythonControllerVirtualEnv, times(2)).isVirtualEnv(); } @Test