From becb54f6bf5d6a13326a98aa318e8f8f7f8fc840 Mon Sep 17 00:00:00 2001 From: Jude Niroshan Date: Thu, 18 Apr 2024 13:20:34 +0200 Subject: [PATCH] feat: use Google java spotless formatter Signed-off-by: Jude Niroshan --- pom.xml | 10 +- src/main/java/com/redhat/exhort/Api.java | 160 +- src/main/java/com/redhat/exhort/Provider.java | 74 +- .../com/redhat/exhort/api/PackageRef.java | 252 +- .../com/redhat/exhort/api/package-info.java | 2 +- .../serialization/PackageRefDeserializer.java | 29 +- .../serialization/PackageURLSerializer.java | 21 +- .../api/serialization/package-info.java | 2 +- .../PackageNotInstalledException.java | 6 +- .../java/com/redhat/exhort/image/Image.java | 600 +-- .../com/redhat/exhort/image/ImageRef.java | 248 +- .../com/redhat/exhort/image/ImageUtils.java | 843 ++-- .../com/redhat/exhort/image/Platform.java | 238 +- .../com/redhat/exhort/impl/ExhortApi.java | 1198 ++--- .../redhat/exhort/impl/RequestManager.java | 48 +- .../com/redhat/exhort/impl/package-info.java | 2 +- .../logging/ClientTraceIdSimpleFormatter.java | 185 +- .../redhat/exhort/logging/LoggersFactory.java | 18 +- .../java/com/redhat/exhort/package-info.java | 2 +- .../exhort/providers/BaseJavaProvider.java | 334 +- .../exhort/providers/GoModulesProvider.java | 940 ++-- .../exhort/providers/GradleProvider.java | 523 +-- .../exhort/providers/JavaMavenProvider.java | 648 +-- .../providers/JavaScriptNpmProvider.java | 363 +- .../exhort/providers/PythonPipProvider.java | 427 +- .../redhat/exhort/providers/package-info.java | 6 +- .../com/redhat/exhort/sbom/CycloneDXSbom.java | 441 +- .../java/com/redhat/exhort/sbom/Sbom.java | 36 +- .../com/redhat/exhort/sbom/SbomFactory.java | 13 +- .../com/redhat/exhort/tools/Ecosystem.java | 94 +- .../com/redhat/exhort/tools/Operations.java | 361 +- .../com/redhat/exhort/tools/package-info.java | 2 +- .../exhort/utils/PythonControllerBase.java | 827 ++-- .../exhort/utils/PythonControllerRealEnv.java | 72 +- .../exhort/utils/PythonControllerTestEnv.java | 41 +- .../utils/PythonControllerVirtualEnv.java | 134 +- .../exhort/utils/StringInsensitive.java | 52 +- .../vcs/GitVersionControlSystemImpl.java | 218 +- .../java/com/redhat/exhort/vcs/TagInfo.java | 56 +- .../exhort/vcs/VersionControlSystem.java | 50 +- src/main/java/module-info.java | 60 +- .../java/com/redhat/exhort/ExhortTest.java | 97 +- .../com/redhat/exhort/image/ImageRefTest.java | 129 +- .../com/redhat/exhort/image/ImageTest.java | 923 ++-- .../redhat/exhort/image/ImageUtilsTest.java | 1724 ++++---- .../com/redhat/exhort/image/PlatformTest.java | 188 +- .../com/redhat/exhort/impl/ExhortApiIT.java | 503 ++- .../redhat/exhort/impl/Exhort_Api_Test.java | 1451 +++--- .../GoModulesMainModuleVersionTest.java | 144 +- .../Golang_Modules_Provider_Test.java | 300 +- .../providers/Gradle_Provider_Test.java | 368 +- .../exhort/providers/HelperExtension.java | 77 +- .../exhort/providers/Java_Envs_Test.java | 36 +- .../providers/Java_Maven_Provider_Test.java | 294 +- .../providers/Javascript_Envs_Test.java | 58 +- .../Javascript_Npm_Provider_Test.java | 293 +- .../providers/PythonEnvironmentExtension.java | 140 +- .../providers/Python_Provider_Test.java | 280 +- .../redhat/exhort/tools/Ecosystem_Test.java | 21 +- .../redhat/exhort/tools/OperationsTest.java | 43 +- .../redhat/exhort/tools/Operations_Test.java | 66 +- .../utils/PythonControllerBaseTest.java | 3936 +++++++++-------- .../utils/PythonControllerRealEnvTest.java | 531 +-- .../utils/PythonControllerVirtualEnvTest.java | 112 +- 64 files changed, 11104 insertions(+), 10246 deletions(-) diff --git a/pom.xml b/pom.xml index c90495d4..1103f44d 100644 --- a/pom.xml +++ b/pom.xml @@ -702,13 +702,15 @@ limitations under the License.]]> - - 2.39.0 - - + + + true + ${project.basedir}/license-header --> + + diff --git a/src/main/java/com/redhat/exhort/Api.java b/src/main/java/com/redhat/exhort/Api.java index e0e332cb..d728cffa 100644 --- a/src/main/java/com/redhat/exhort/Api.java +++ b/src/main/java/com/redhat/exhort/Api.java @@ -24,91 +24,93 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -/** 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"; + public static final String CYCLONEDX_MEDIA_TYPE = "application/vnd.cyclonedx+json"; - enum MediaType { - APPLICATION_JSON, - TEXT_HTML, - MULTIPART_MIXED; + enum MediaType { + APPLICATION_JSON, + TEXT_HTML, + MULTIPART_MIXED; - @Override - public String toString() { - return this.name().toLowerCase().replace("_", "/"); - } + @Override + public String toString() { + return this.name().toLowerCase().replace("_", "/"); } + } - /** POJO class used for aggregating multipart/mixed analysis requests. */ - class MixedReport { - public final byte[] html; - public final AnalysisReport json; - - public MixedReport(final byte[] html, final AnalysisReport json) { - this.html = html; - this.json = json; - } - - public MixedReport() { - this.html = new byte[0]; - this.json = new AnalysisReport(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - var that = (MixedReport) o; - return Arrays.equals(this.html, that.html) && Objects.equals(this.json, that.json); - } - - @Override - public int hashCode() { - return 31 * Objects.hash(json) + Arrays.hashCode(html); - } + /** POJO class used for aggregating multipart/mixed analysis requests. */ + class MixedReport { + public final byte[] html; + public final AnalysisReport json; + + public MixedReport(final byte[] html, final AnalysisReport json) { + this.html = html; + this.json = json; + } + + public MixedReport() { + this.html = new byte[0]; + this.json = new AnalysisReport(); } - /** - * Use for creating a stack analysis HTML report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return a mixed reports for both HTML and JSON wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysisMixed(String manifestFile) throws IOException; - - /** - * Use for creating a stack analysis HTML report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return the HTML report as a String wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysisHtml(String manifestFile) throws IOException; - - /** - * Use for creating a stack analysis deserialized Json report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysis(String manifestFile) throws IOException; - - /** - * Use for creating a component analysis deserialized Json report for a given type and content. - * - * @param manifestType the type of the manifest, i.e. {@code pom.xml} - * @param manifestContent a byte array of the manifest's content - * @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 manifestFile) throws IOException; - - CompletableFuture> imageAnalysis(Set imageRefs) throws IOException; - - CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException; + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || this.getClass() != o.getClass()) return false; + var that = (MixedReport) o; + return Arrays.equals(this.html, that.html) && Objects.equals(this.json, that.json); + } + + @Override + public int hashCode() { + return 31 * Objects.hash(json) + Arrays.hashCode(html); + } + } + + /** + * Use for creating a stack analysis HTML report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return a mixed reports for both HTML and JSON wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysisMixed(String manifestFile) throws IOException; + + /** + * Use for creating a stack analysis HTML report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return the HTML report as a String wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysisHtml(String manifestFile) throws IOException; + + /** + * Use for creating a stack analysis deserialized Json report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysis(String manifestFile) throws IOException; + + /** + * Use for creating a component analysis deserialized Json report for a given type and content. + * + * @param manifestType the type of the manifest, i.e. {@code pom.xml} + * @param manifestContent a byte array of the manifest's content + * @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 manifestFile) 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 c66ae65e..a3b72753 100644 --- a/src/main/java/com/redhat/exhort/Provider.java +++ b/src/main/java/com/redhat/exhort/Provider.java @@ -21,50 +21,50 @@ 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. - **/ - public static class Content { - public final byte[] buffer; - public final String type; + /** + * 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) { - this.buffer = buffer; - this.type = type; - } + public Content(byte[] buffer, String type) { + this.buffer = buffer; + this.type = type; } + } - /** The ecosystem of this provider, i.e. maven. */ - public final Ecosystem.Type ecosystem; + /** The ecosystem of this provider, i.e. maven. */ + public final Ecosystem.Type ecosystem; - protected final ObjectMapper objectMapper = new ObjectMapper(); + protected final ObjectMapper objectMapper = new ObjectMapper(); - protected Provider(Ecosystem.Type ecosystem) { - this.ecosystem = ecosystem; - } + protected Provider(Ecosystem.Type ecosystem) { + this.ecosystem = ecosystem; + } - /** - * Use for providing content for a stack analysis request. - * - * @param manifestPath the Path for the manifest file - * @return A Content record aggregating the body content and content type - * @throws IOException when failed to load the manifest file - */ - public abstract Content provideStack(Path manifestPath) throws IOException; + /** + * Use for providing content for a stack analysis request. + * + * @param manifestPath the Path for the manifest file + * @return A Content record aggregating the body content and content type + * @throws IOException when failed to load the manifest file + */ + public abstract Content provideStack(Path manifestPath) throws IOException; - /** - * Use for providing content for a component analysis request. - * - * @param manifestContent the content of the manifest file - * @return A Content record aggregating the body content and content type - * @throws IOException when failed to load the manifest content - */ - public abstract Content provideComponent(byte[] manifestContent) throws IOException; + /** + * Use for providing content for a component analysis request. + * + * @param manifestContent the content of the manifest file + * @return A Content record aggregating the body content and content type + * @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; + 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 1d15dc37..347d48bc 100644 --- a/src/main/java/com/redhat/exhort/api/PackageRef.java +++ b/src/main/java/com/redhat/exhort/api/PackageRef.java @@ -24,156 +24,156 @@ public class PackageRef { - private static final String MAVEN_TYPE = "maven"; - - @JsonSerialize(using = PackageURLSerializer.class) - @JsonValue - private final PackageURL purl; - - public PackageRef(String purl) { - Objects.requireNonNull(purl); - try { - this.purl = new PackageURL(purl); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); - } + private static final String MAVEN_TYPE = "maven"; + + @JsonSerialize(using = PackageURLSerializer.class) + @JsonValue + private final PackageURL purl; + + public PackageRef(String purl) { + Objects.requireNonNull(purl); + try { + this.purl = new PackageURL(purl); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); } + } - public PackageRef(PackageURL purl) { - Objects.requireNonNull(purl); - this.purl = purl; - } + public PackageRef(PackageURL purl) { + Objects.requireNonNull(purl); + this.purl = purl; + } - public PackageURL purl() { - return purl; - } + public PackageURL purl() { + return purl; + } - public String ref() { - return purl.toString(); - } + public String ref() { + return purl.toString(); + } - public String name() { - if (purl.getNamespace() == null) { - return purl.getName(); - } - return purl.getNamespace() + ":" + purl.getName(); + public String name() { + if (purl.getNamespace() == null) { + return purl.getName(); } - - public String version() { - return purl.getVersion(); + return purl.getNamespace() + ":" + purl.getName(); + } + + public String version() { + return purl.getVersion(); + } + + @Override + public int hashCode() { + return purl.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; } - - @Override - public int hashCode() { - return purl.hashCode(); + if (!(other instanceof PackageRef)) { + return false; } + return Objects.equals(purl, ((PackageRef) other).purl()); + } - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (!(other instanceof PackageRef)) { - return false; - } - return Objects.equals(purl, ((PackageRef) other).purl()); + public static PackageRef parse(String gav, String pkgManager) { + var parts = gav.split(":"); + if (parts.length < 4 || parts.length > 6) { + throw new IllegalArgumentException("Unexpected GAV format. " + gav); } - - public static PackageRef parse(String gav, String pkgManager) { - var parts = gav.split(":"); - if (parts.length < 4 || parts.length > 6) { - throw new IllegalArgumentException("Unexpected GAV format. " + gav); - } - if (parts.length < 6) { - return builder() - .namespace(parts[0]) - .name(parts[1]) - .version(parts[3]) - .pkgManager(pkgManager) - .build(); - } - return builder() - .namespace(parts[0]) - .name(parts[1]) - .version(parts[4]) - .pkgManager(pkgManager) - .build(); + if (parts.length < 6) { + return builder() + .namespace(parts[0]) + .name(parts[1]) + .version(parts[3]) + .pkgManager(pkgManager) + .build(); + } + return builder() + .namespace(parts[0]) + .name(parts[1]) + .version(parts[4]) + .pkgManager(pkgManager) + .build(); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + if (prefix == null) { + prefix = ""; } - /** - * Convert the instance into URL query string. - * - * @param prefix prefix of the query string - * @return URL query string - */ - public String toUrlQueryString(String prefix) { - if (prefix == null) { - prefix = ""; - } + return String.format("%s=%s", prefix, this.toString()); + } - return String.format("%s=%s", prefix, this.toString()); - } + public static Builder builder() { + return new Builder(); + } - public static Builder builder() { - return new Builder(); - } + public static class Builder { - public static class Builder { + String namespace; + String name; + String version; + String pkgManager; + String purl; - String namespace; - String name; - String version; - String pkgManager; - String purl; + public Builder purl(String purl) { + this.purl = purl; + return this; + } - public Builder purl(String purl) { - this.purl = purl; - return this; - } + public Builder pkgManager(String pkgManager) { + this.pkgManager = pkgManager; + return this; + } - public Builder pkgManager(String pkgManager) { - this.pkgManager = pkgManager; - return this; - } + public Builder version(String version) { + this.version = version; + return this; + } - public Builder version(String version) { - this.version = version; - return this; - } + public Builder name(String name) { + this.name = name; + return this; + } - public Builder name(String name) { - this.name = name; - return this; - } + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } - public Builder namespace(String namespace) { - this.namespace = namespace; - return this; - } + private Builder() {} - private Builder() {} - - public PackageRef build() { - try { - if (Objects.isNull(purl)) { - Objects.requireNonNull(pkgManager); - Objects.requireNonNull(name); - Objects.requireNonNull(version); - return new PackageRef(new PackageURL(pkgManager, namespace, name, version, null, null)); - } - return new PackageRef(new PackageURL(purl)); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); - } + public PackageRef build() { + try { + if (Objects.isNull(purl)) { + Objects.requireNonNull(pkgManager); + Objects.requireNonNull(name); + Objects.requireNonNull(version); + return new PackageRef(new PackageURL(pkgManager, namespace, name, version, null, null)); } + return new PackageRef(new PackageURL(purl)); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); + } } + } - public String toGav() { - return String.format("%s:%s", name(), purl.getVersion()); - } + public String toGav() { + return String.format("%s:%s", name(), purl.getVersion()); + } - @Override - public String toString() { - return purl.toString(); - } + @Override + public String toString() { + return purl.toString(); + } } 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 e2595732..f192e2bf 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java @@ -25,21 +25,22 @@ 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 ea3d9b23..e61c786c 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java @@ -23,16 +23,17 @@ public class PackageURLSerializer extends StdSerializer { - public PackageURLSerializer() { - this(null); - } + public PackageURLSerializer() { + this(null); + } - public PackageURLSerializer(Class c) { - super(c); - } + public PackageURLSerializer(Class c) { + super(c); + } - @Override - public void serialize(PackageURL value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeString(value.toString()); - } + @Override + public void serialize(PackageURL value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeString(value.toString()); + } } 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/exception/PackageNotInstalledException.java b/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java index 56c006a6..9e9dde79 100644 --- a/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java +++ b/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java @@ -16,7 +16,7 @@ package com.redhat.exhort.exception; public class PackageNotInstalledException extends RuntimeException { - public PackageNotInstalledException(String message) { - super(message); - } + public PackageNotInstalledException(String message) { + super(message); + } } diff --git a/src/main/java/com/redhat/exhort/image/Image.java b/src/main/java/com/redhat/exhort/image/Image.java index b8c8e0de..a3ba9297 100644 --- a/src/main/java/com/redhat/exhort/image/Image.java +++ b/src/main/java/com/redhat/exhort/image/Image.java @@ -29,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 @@ -48,311 +49,318 @@ */ 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])"; - // 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 + ")+)?"); - // 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]+)?$"); - // 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,}$"); - // The repository part of the full image - private String repository; - // Registry - private String registry; - // Tag name - private String tag; - // Digest - private String digest; - // User name - private String user; - - /** - * Create an image name - * - * @param fullName The fullname of the image in Docker format. - */ - public Image(String fullName) { - this(fullName, null); - } - - /** - * Create an image name with a tag. If a tag is provided (i.e. is not null) then this tag is used. - * Otherwise the tag of the provided name is used (if any). - * - * @param fullName The fullname of the image in Docker format. I - * @param givenTag tag to use. Can be null in which case the tag specified in fullName is used. - */ - public Image(String fullName, String givenTag) { - if (fullName == null) { - throw new NullPointerException("Image name must not be null"); - } - - // set digest to null as default - digest = null; - // check if digest is part of fullName, if so -> extract it - if (fullName.contains("@sha256")) { // Of it contains digest - String[] digestParts = fullName.split("@"); - digest = digestParts[1]; - fullName = digestParts[0]; - } - - // check for tag - 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]"); - } - // extract tag if it exists - tag = givenTag != null ? givenTag : matcher.group(2); - String rest = matcher.group(1); - - // extract registry, repository, user - parseComponentsBeforeTag(rest); - - /* - * set tag to latest if tag AND digest are null - * if digest is not null but tag is -> leave it! - * -> in case of "image_name@sha256" it is not required to get resolved to "latest" - */ - if (tag == null && digest == null) { - tag = "latest"; - } - - doValidate(); - } - - /** - * 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 - */ - public static void validate(String image) { - // Validation will be triggered during construction - new Image(image); - } - - public String getRepository() { - return repository; - } - - public String getRegistry() { - return registry; - } - - public String getTag() { - return tag; - } - - public String getDigest() { - return digest; - } - - public void setDigest(String digest) { - if (this.digest == null) { - this.digest = digest; - } - } - - @Override - 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); + // --------------------------------------------------------------------- + // 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])"; + // 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 + ")+)?"); + // 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]+)?$"); + // 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,}$"); + // The repository part of the full image + private String repository; + // Registry + private String registry; + // Tag name + private String tag; + // Digest + private String digest; + // User name + private String user; + + /** + * Create an image name + * + * @param fullName The fullname of the image in Docker format. + */ + public Image(String fullName) { + this(fullName, null); + } + + /** + * Create an image name with a tag. If a tag is provided (i.e. is not null) then this tag is used. + * Otherwise the tag of the provided name is used (if any). + * + * @param fullName The fullname of the image in Docker format. I + * @param givenTag tag to use. Can be null in which case the tag specified in fullName is used. + */ + public Image(String fullName, String givenTag) { + if (fullName == null) { + throw new NullPointerException("Image name must not be null"); } - @Override - public int hashCode() { - return Objects.hash(repository, registry, tag, digest, user); + // set digest to null as default + digest = null; + // check if digest is part of fullName, if so -> extract it + if (fullName.contains("@sha256")) { // Of it contains digest + String[] digestParts = fullName.split("@"); + digest = digestParts[1]; + fullName = digestParts[0]; } - @Override - public String toString() { - return this.getFullName(); + // check for tag + 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]"); } + // extract tag if it exists + tag = givenTag != null ? givenTag : matcher.group(2); + String rest = matcher.group(1); - public boolean hasRegistry() { - return registry != null && registry.length() > 0; - } - - private String joinTail(String[] parts) { - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < parts.length; i++) { - builder.append(parts[i]); - if (i < parts.length - 1) { - builder.append("/"); - } - } - return builder.toString(); - } + // extract registry, repository, user + parseComponentsBeforeTag(rest); - private boolean isRegistry(String part) { - return part.contains(".") || part.contains(":"); - } - - /** - * 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 + /* + * set tag to latest if tag AND digest are null + * if digest is not null but tag is -> leave it! + * -> in case of "image_name@sha256" it is not required to get resolved to "latest" */ - public String getNameWithoutTag() { - return getNameWithoutTag(null); + if (tag == null && digest == null) { + tag = "latest"; } - /** - * 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) - */ - public String getNameWithoutTag(String optionalRegistry) { - StringBuilder ret = new StringBuilder(); - if (registry != null || optionalRegistry != null) { - ret.append(registry != null ? registry : optionalRegistry).append("/"); - } - ret.append(repository); - return ret.toString(); + doValidate(); + } + + /** + * 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 + */ + public static void validate(String image) { + // Validation will be triggered during construction + new Image(image); + } + + public String getRepository() { + return repository; + } + + public String getRegistry() { + return registry; + } + + public String getTag() { + return tag; + } + + public String getDigest() { + return digest; + } + + public void setDigest(String digest) { + if (this.digest == null) { + this.digest = digest; } - - // ================================================================================================ - - // Validations patterns, taken directly from the docker source --> - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go - // 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) - * - * @return full name with the original registry and the original tag given (if any). - */ - public String getFullName() { - return getFullName(null); + } + + @Override + 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); + } + + @Override + public int hashCode() { + return Objects.hash(repository, registry, tag, digest, user); + } + + @Override + public String toString() { + return this.getFullName(); + } + + public boolean hasRegistry() { + return registry != null && registry.length() > 0; + } + + private String joinTail(String[] parts) { + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < parts.length; i++) { + builder.append(parts[i]); + if (i < parts.length - 1) { + builder.append("/"); + } } - - /** - * 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). - */ - public String getFullName(String optionalRegistry) { - String fullName = getNameWithoutTag(optionalRegistry); - if (tag != null) { - fullName = fullName + ":" + tag; - } - if (digest != null) { - fullName = fullName + "@" + digest; - } - return fullName; + return builder.toString(); + } + + private boolean isRegistry(String part) { + return part.contains(".") || part.contains(":"); + } + + /** + * 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 + */ + public String getNameWithoutTag() { + return getNameWithoutTag(null); + } + + /** + * 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 + * ) + */ + public String getNameWithoutTag(String optionalRegistry) { + StringBuilder ret = new StringBuilder(); + if (registry != null || optionalRegistry != null) { + ret.append(registry != null ? registry : optionalRegistry).append("/"); } - - // ========================================================== - - /** - * 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 - */ - public String getUser() { - return user; + ret.append(repository); + return ret.toString(); + } + + // ================================================================================================ + + // Validations patterns, taken directly from the docker source --> + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go + // 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) + * + * @return full name with the original registry and the original tag given (if any). + */ + public String getFullName() { + return getFullName(null); + } + + /** + * 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 + * ). + */ + public String getFullName(String optionalRegistry) { + String fullName = getNameWithoutTag(optionalRegistry); + if (tag != null) { + fullName = fullName + ":" + tag; } - - /** - * Get the simple name of the image, which is the repository sans the user parts. - * - * @return simple name of the image - */ - public String getSimpleName() { - String prefix = user + "/"; - return repository.startsWith(prefix) ? repository.substring(prefix.length()) : repository; + if (digest != null) { + fullName = fullName + "@" + digest; } - - public String getNameWithOptionalRepository(String optionalRepository) { - if (optionalRepository != null) { - String simpleName = getFullName(); - String[] simpleNameParts = simpleName.split("/"); - if (simpleNameParts.length > 0) { - return optionalRepository + "/" + simpleNameParts[simpleNameParts.length - 1]; - } - } - return getFullName(); + return fullName; + } + + // ========================================================== + + /** + * 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 + */ + public String getUser() { + return user; + } + + /** + * Get the simple name of the image, which is the repository sans the user parts. + * + * @return simple name of the image + */ + public String getSimpleName() { + String prefix = user + "/"; + return repository.startsWith(prefix) ? repository.substring(prefix.length()) : repository; + } + + public String getNameWithOptionalRepository(String optionalRepository) { + if (optionalRepository != null) { + String simpleName = getFullName(); + String[] simpleNameParts = simpleName.split("/"); + if (simpleNameParts.length > 0) { + return optionalRepository + "/" + simpleNameParts[simpleNameParts.length - 1]; + } } - - // Validate parts and throw an IllegalArgumentException if a part is not valid - 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 + return getFullName(); + } + + // Validate parts and throw an IllegalArgumentException if a part is not valid + 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 }; - 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 (errors.size() > 0) { - StringBuilder buf = new StringBuilder(); - buf.append(String.format("Given Docker name '%s' is invalid:\n", getFullName())); - for (String error : errors) { - buf.append(String.format(" * %s\n", error)); - } - buf.append("See http://bit.ly/docker_image_fmt for more details"); - throw new IllegalArgumentException(buf.toString()); - } + 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())); + } } - - private void parseComponentsBeforeTag(String rest) { - String[] parts = rest.split("\\s*/\\s*"); - if (parts.length == 1) { - registry = null; - user = null; - repository = parts[0]; - } else if (parts.length >= 2) { - if (isRegistry(parts[0])) { - registry = parts[0]; - if (parts.length > 2) { - user = parts[1]; - repository = joinTail(parts); - } else { - user = null; - repository = parts[1]; - } - } else { - registry = null; - user = parts[0]; - repository = rest; - } + if (errors.size() > 0) { + StringBuilder buf = new StringBuilder(); + buf.append(String.format("Given Docker name '%s' is invalid:\n", getFullName())); + for (String error : errors) { + buf.append(String.format(" * %s\n", error)); + } + buf.append("See http://bit.ly/docker_image_fmt for more details"); + throw new IllegalArgumentException(buf.toString()); + } + } + + private void parseComponentsBeforeTag(String rest) { + String[] parts = rest.split("\\s*/\\s*"); + if (parts.length == 1) { + registry = null; + user = null; + repository = parts[0]; + } else if (parts.length >= 2) { + if (isRegistry(parts[0])) { + registry = parts[0]; + if (parts.length > 2) { + user = parts[1]; + repository = joinTail(parts); + } else { + user = null; + repository = parts[1]; } + } else { + registry = null; + user = parts[0]; + repository = rest; + } } + } } diff --git a/src/main/java/com/redhat/exhort/image/ImageRef.java b/src/main/java/com/redhat/exhort/image/ImageRef.java index 6065a870..265ce9b1 100644 --- a/src/main/java/com/redhat/exhort/image/ImageRef.java +++ b/src/main/java/com/redhat/exhort/image/ImageRef.java @@ -27,144 +27,144 @@ public class ImageRef { - public static final String OCI_TYPE = "oci"; - public static final String REPOSITORY_QUALIFIER = "repository_url"; - public static final String TAG_QUALIFIER = "tag"; - public static final String ARCH_QUALIFIER = "arch"; - public static final String OS_QUALIFIER = "os"; - public static final String VARIANT_QUALIFIER = "variant"; + public static final String OCI_TYPE = "oci"; + public static final String REPOSITORY_QUALIFIER = "repository_url"; + public static final String TAG_QUALIFIER = "tag"; + public static final String ARCH_QUALIFIER = "arch"; + public static final String OS_QUALIFIER = "os"; + public static final String VARIANT_QUALIFIER = "variant"; - private Image image; - private Platform platform; + private Image image; + private Platform platform; - public ImageRef(String image, String platform) { - this.image = new Image(image); + public ImageRef(String image, String platform) { + this.image = new Image(image); - if (platform != null) { - this.platform = new Platform(platform); - } - - checkImageDigest(); + if (platform != null) { + this.platform = new Platform(platform); } - public ImageRef(PackageURL packageURL) { - String name = null; - String version = null; - String tag = null; - String repositoryRrl = null; - String arch = null; - String os = null; - String variant = null; - - Map qualifiers = packageURL.getQualifiers(); - if (qualifiers != null && !qualifiers.isEmpty()) { - repositoryRrl = qualifiers.get(REPOSITORY_QUALIFIER); - tag = qualifiers.get(TAG_QUALIFIER); - arch = qualifiers.get(ARCH_QUALIFIER); - os = qualifiers.get(OS_QUALIFIER); - variant = qualifiers.get(VARIANT_QUALIFIER); - } - name = packageURL.getName(); - version = packageURL.getVersion(); - - String imageName = name; - if (repositoryRrl != null) { - imageName = repositoryRrl; - } - if (tag != null) { - imageName = imageName + ":" + tag; - } - if (version != null) { - imageName = imageName + "@" + version; - } - - this.image = new Image(imageName); - - if (arch != null && os != null) { - this.platform = new Platform(os, arch, variant); - } + checkImageDigest(); + } + + public ImageRef(PackageURL packageURL) { + String name = null; + String version = null; + String tag = null; + String repositoryRrl = null; + String arch = null; + String os = null; + String variant = null; + + Map qualifiers = packageURL.getQualifiers(); + if (qualifiers != null && !qualifiers.isEmpty()) { + repositoryRrl = qualifiers.get(REPOSITORY_QUALIFIER); + tag = qualifiers.get(TAG_QUALIFIER); + arch = qualifiers.get(ARCH_QUALIFIER); + os = qualifiers.get(OS_QUALIFIER); + variant = qualifiers.get(VARIANT_QUALIFIER); } + name = packageURL.getName(); + version = packageURL.getVersion(); - public Image getImage() { - return image; + String imageName = name; + if (repositoryRrl != null) { + imageName = repositoryRrl; } - - public Platform getPlatform() { - return platform; + if (tag != null) { + imageName = imageName + ":" + tag; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ImageRef imageRef = (ImageRef) o; - return Objects.equals(image, imageRef.image) && Objects.equals(platform, imageRef.platform); + if (version != null) { + imageName = imageName + "@" + version; } - @Override - public int hashCode() { - return Objects.hash(image, platform); - } - - @Override - public String toString() { - return "ImageRef{" + "image='" + image + '\'' + ", platform='" + platform + '\'' + '}'; - } + this.image = new Image(imageName); - void checkImageDigest() { - if (this.image.getDigest() == null) { - try { - var digests = getImageDigests(this); - if (digests.isEmpty()) { - throw new RuntimeException("Failed to get any image digest"); - } - if (digests.size() == 1 && digests.containsKey(Platform.EMPTY_PLATFORM)) { - this.image.setDigest(digests.get(Platform.EMPTY_PLATFORM)); - } else { - if (this.platform == null) { - this.platform = getImagePlatform(); - } - if (this.platform == null) { - 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)); - } - this.image.setDigest(digests.get(this.platform)); - } - } catch (JsonProcessingException | IllegalArgumentException ex) { - throw new RuntimeException("Failed to get image digest", ex); - } - } + if (arch != null && os != null) { + this.platform = new Platform(os, arch, variant); } - - // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci - public PackageURL getPackageURL() throws MalformedPackageURLException { - TreeMap qualifiers = new TreeMap<>(); - var repositoryUrl = this.image.getNameWithoutTag(); - var simpleName = this.image.getSimpleName(); - if (repositoryUrl != null && !repositoryUrl.equalsIgnoreCase(simpleName)) { - qualifiers.put(REPOSITORY_QUALIFIER, repositoryUrl.toLowerCase()); - } - if (this.platform != null) { - qualifiers.put(ARCH_QUALIFIER, this.platform.getArchitecture().toLowerCase()); - qualifiers.put(OS_QUALIFIER, this.platform.getOs().toLowerCase()); - if (this.platform.getVariant() != null) { - qualifiers.put(VARIANT_QUALIFIER, this.platform.getVariant().toLowerCase()); - } + } + + public Image getImage() { + return image; + } + + public Platform getPlatform() { + return platform; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageRef imageRef = (ImageRef) o; + return Objects.equals(image, imageRef.image) && Objects.equals(platform, imageRef.platform); + } + + @Override + public int hashCode() { + return Objects.hash(image, platform); + } + + @Override + public String toString() { + return "ImageRef{" + "image='" + image + '\'' + ", platform='" + platform + '\'' + '}'; + } + + void checkImageDigest() { + if (this.image.getDigest() == null) { + try { + var digests = getImageDigests(this); + if (digests.isEmpty()) { + throw new RuntimeException("Failed to get any image digest"); } - var tag = this.image.getTag(); - if (tag != null) { - qualifiers.put(TAG_QUALIFIER, tag); + if (digests.size() == 1 && digests.containsKey(Platform.EMPTY_PLATFORM)) { + this.image.setDigest(digests.get(Platform.EMPTY_PLATFORM)); + } else { + if (this.platform == null) { + this.platform = getImagePlatform(); + } + if (this.platform == null) { + 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)); + } + this.image.setDigest(digests.get(this.platform)); } - - return new PackageURL( - OCI_TYPE, - null, - this.image.getSimpleName().toLowerCase(), - image.getDigest().toLowerCase(), - qualifiers, - null); + } catch (JsonProcessingException | IllegalArgumentException ex) { + throw new RuntimeException("Failed to get image digest", ex); + } + } + } + + // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci + public PackageURL getPackageURL() throws MalformedPackageURLException { + TreeMap qualifiers = new TreeMap<>(); + var repositoryUrl = this.image.getNameWithoutTag(); + var simpleName = this.image.getSimpleName(); + if (repositoryUrl != null && !repositoryUrl.equalsIgnoreCase(simpleName)) { + qualifiers.put(REPOSITORY_QUALIFIER, repositoryUrl.toLowerCase()); + } + if (this.platform != null) { + qualifiers.put(ARCH_QUALIFIER, this.platform.getArchitecture().toLowerCase()); + qualifiers.put(OS_QUALIFIER, this.platform.getOs().toLowerCase()); + if (this.platform.getVariant() != null) { + qualifiers.put(VARIANT_QUALIFIER, this.platform.getVariant().toLowerCase()); + } } + var tag = this.image.getTag(); + if (tag != null) { + qualifiers.put(TAG_QUALIFIER, tag); + } + + 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 97ba1e0d..8268393e 100644 --- a/src/main/java/com/redhat/exhort/image/ImageUtils.java +++ b/src/main/java/com/redhat/exhort/image/ImageUtils.java @@ -42,459 +42,492 @@ public class ImageUtils { - static final String EXHORT_SYFT_CONFIG_PATH = "EXHORT_SYFT_CONFIG_PATH"; - static final String EXHORT_SYFT_IMAGE_SOURCE = "EXHORT_SYFT_IMAGE_SOURCE"; - static final String EXHORT_IMAGE_PLATFORM = "EXHORT_IMAGE_PLATFORM"; - static final String EXHORT_IMAGE_OS = "EXHORT_IMAGE_OS"; - static final String EXHORT_IMAGE_ARCH = "EXHORT_IMAGE_ARCH"; - 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 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")); - - static String updatePATHEnv(String execPath) { - String path = System.getenv("PATH"); - if (path != null) { - return String.format("PATH=%s%s%s", path, File.pathSeparator, execPath); - } else { - return String.format("PATH=%s", execPath); - } + static final String EXHORT_SYFT_CONFIG_PATH = "EXHORT_SYFT_CONFIG_PATH"; + static final String EXHORT_SYFT_IMAGE_SOURCE = "EXHORT_SYFT_IMAGE_SOURCE"; + static final String EXHORT_IMAGE_PLATFORM = "EXHORT_IMAGE_PLATFORM"; + static final String EXHORT_IMAGE_OS = "EXHORT_IMAGE_OS"; + static final String EXHORT_IMAGE_ARCH = "EXHORT_IMAGE_ARCH"; + 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 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")); + + static String updatePATHEnv(String execPath) { + String path = System.getenv("PATH"); + if (path != null) { + return String.format("PATH=%s%s%s", path, File.pathSeparator, execPath); + } else { + return String.format("PATH=%s", execPath); } + } - public static JsonNode generateImageSBOM(ImageRef imageRef) throws IOException, MalformedPackageURLException { - var output = execSyft(imageRef); - - if (!output.getError().isEmpty() || output.getExitCode() != 0) { - throw new RuntimeException(output.getError()); - } - - var node = OBJECT_MAPPER.readTree(output.getOutput()); - if (node.hasNonNull("metadata")) { - var metadataNode = node.get("metadata"); - if (metadataNode.hasNonNull("component")) { - var componentNode = metadataNode.get("component"); - if (componentNode.isObject()) { - String imagePurl = imageRef.getPackageURL().canonicalize(); - ((ObjectNode) componentNode).set("purl", new TextNode(imagePurl)); - return node; - } - } - } + public static JsonNode generateImageSBOM(ImageRef imageRef) + throws IOException, MalformedPackageURLException { + var output = execSyft(imageRef); - throw new RuntimeException(String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); } - static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { - var syft = Operations.getCustomPathOrElse("syft"); - var docker = Operations.getCustomPathOrElse("docker"); - var podman = Operations.getCustomPathOrElse("podman"); - - var syftConfigPath = getStringValueEnvironment(EXHORT_SYFT_CONFIG_PATH, ""); - 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 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" - }; - } 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" - }; + var node = OBJECT_MAPPER.readTree(output.getOutput()); + if (node.hasNonNull("metadata")) { + var metadataNode = node.get("metadata"); + if (metadataNode.hasNonNull("component")) { + var componentNode = metadataNode.get("component"); + if (componentNode.isObject()) { + String imagePurl = imageRef.getPackageURL().canonicalize(); + ((ObjectNode) componentNode).set("purl", new TextNode(imagePurl)); + return node; } - - return Operations.runProcessGetFullOutput(null, cmd, envs.isEmpty() ? null : envs.toArray(new String[1])); + } } - static List getSyftEnvs(String dockerPath, String podmanPath) { - String path = null; - if (!dockerPath.isEmpty() && !podmanPath.isEmpty()) { - path = String.format("%s%s%s", dockerPath, File.pathSeparator, podmanPath); - } else if (!dockerPath.isEmpty()) { - path = dockerPath; - } else if (!podmanPath.isEmpty()) { - path = podmanPath; - } - var envPath = path != null ? updatePATHEnv(path) : null; - - List envs = new ArrayList<>(1); - if (envPath != null) { - envs.add(envPath); - } - return envs; + throw new RuntimeException( + String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); + } + + static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { + var syft = Operations.getCustomPathOrElse("syft"); + var docker = Operations.getCustomPathOrElse("docker"); + var podman = Operations.getCustomPathOrElse("podman"); + + var syftConfigPath = getStringValueEnvironment(EXHORT_SYFT_CONFIG_PATH, ""); + 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 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" + }; + } 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" + }; } - public static Platform getImagePlatform() { - var platform = getStringValueEnvironment(EXHORT_IMAGE_PLATFORM, ""); - if (!platform.isEmpty()) { - return new Platform(platform); - } - - var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); - SyftImageSource source = SyftImageSource.getImageSource(imageSource); - - var os = getStringValueEnvironment(EXHORT_IMAGE_OS, ""); - if (os.isEmpty()) { - os = source.getOs(); - } - var arch = getStringValueEnvironment(EXHORT_IMAGE_ARCH, ""); - if (arch.isEmpty()) { - arch = source.getArch(); - } - if (!os.isEmpty() && !arch.isEmpty()) { - if (!Platform.isVariantRequired(os, arch)) { - return new Platform(os, arch, null); - } - - var variant = getStringValueEnvironment(EXHORT_IMAGE_VARIANT, ""); - if (variant.isEmpty()) { - variant = source.getVariant(); - } - if (!variant.isEmpty()) { - return new Platform(os, arch, variant); - } - } - - return null; + return Operations.runProcessGetFullOutput( + null, cmd, envs.isEmpty() ? null : envs.toArray(new String[1])); + } + + static List getSyftEnvs(String dockerPath, String podmanPath) { + String path = null; + if (!dockerPath.isEmpty() && !podmanPath.isEmpty()) { + path = String.format("%s%s%s", dockerPath, File.pathSeparator, podmanPath); + } else if (!dockerPath.isEmpty()) { + path = dockerPath; + } else if (!podmanPath.isEmpty()) { + path = podmanPath; } + var envPath = path != null ? updatePATHEnv(path) : null; - static String hostInfo(String engine, String info) { - var exec = Operations.getCustomPathOrElse(engine); - var cmd = new String[] {exec, "info"}; - - var output = Operations.runProcessGetFullOutput(null, cmd, null); - 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(""); + List envs = new ArrayList<>(1); + if (envPath != null) { + envs.add(envPath); } + return envs; + } - static String dockerGetOs() { - return hostInfo("docker", "OSType"); + public static Platform getImagePlatform() { + var platform = getStringValueEnvironment(EXHORT_IMAGE_PLATFORM, ""); + if (!platform.isEmpty()) { + return new Platform(platform); } - static String dockerGetArch() { - var arch = hostInfo("docker", "Architecture"); - arch = archMapping.get(arch); - return Objects.requireNonNullElse(arch, ""); - } + var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); + SyftImageSource source = SyftImageSource.getImageSource(imageSource); - static String dockerGetVariant() { - var variant = hostInfo("docker", "Architecture"); - variant = variantMapping.get(variant); - return Objects.requireNonNullElse(variant, ""); + var os = getStringValueEnvironment(EXHORT_IMAGE_OS, ""); + if (os.isEmpty()) { + os = source.getOs(); } - - static String podmanGetOs() { - return hostInfo("podman", "os"); + var arch = getStringValueEnvironment(EXHORT_IMAGE_ARCH, ""); + if (arch.isEmpty()) { + arch = source.getArch(); } - - static String podmanGetArch() { - return hostInfo("podman", "arch"); + if (!os.isEmpty() && !arch.isEmpty()) { + if (!Platform.isVariantRequired(os, arch)) { + return new Platform(os, arch, null); + } + + var variant = getStringValueEnvironment(EXHORT_IMAGE_VARIANT, ""); + if (variant.isEmpty()) { + variant = source.getVariant(); + } + if (!variant.isEmpty()) { + return new Platform(os, arch, variant); + } } - static String podmanGetVariant() { - return hostInfo("podman", "variant"); - } + return null; + } - static String dockerPodmanInfo(Supplier dockerSupplier, Supplier podmanSupplier) { - var info = dockerSupplier.get(); - if (info.isEmpty()) { - info = podmanSupplier.get(); - } - return info; - } + static String hostInfo(String engine, String info) { + var exec = Operations.getCustomPathOrElse(engine); + var cmd = new String[] {exec, "info"}; - public static Map getImageDigests(ImageRef imageRef) throws JsonProcessingException { - var output = execSkopeoInspect(imageRef, true); + var output = Operations.runProcessGetFullOutput(null, cmd, null); + if (output.getOutput().isEmpty() + && (!output.getError().isEmpty() || output.getExitCode() != 0)) { + throw new RuntimeException(output.getError()); + } - if (!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(""); + } + + static String dockerGetOs() { + return hostInfo("docker", "OSType"); + } + + static String dockerGetArch() { + var arch = hostInfo("docker", "Architecture"); + arch = archMapping.get(arch); + return Objects.requireNonNullElse(arch, ""); + } + + static String dockerGetVariant() { + var variant = hostInfo("docker", "Architecture"); + variant = variantMapping.get(variant); + return Objects.requireNonNullElse(variant, ""); + } + + static String podmanGetOs() { + return hostInfo("podman", "os"); + } + + static String podmanGetArch() { + return hostInfo("podman", "arch"); + } + + static String podmanGetVariant() { + return hostInfo("podman", "variant"); + } + + static String dockerPodmanInfo(Supplier dockerSupplier, Supplier podmanSupplier) { + var info = dockerSupplier.get(); + if (info.isEmpty()) { + info = podmanSupplier.get(); + } + return info; + } - var node = OBJECT_MAPPER.readTree(output.getOutput()); - if (node.hasNonNull("mediaType")) { - var mediaTypeNode = node.get("mediaType"); - if (mediaTypeNode.isTextual()) { - var mediaType = mediaTypeNode.asText(); - switch (mediaType) { - case MEDIA_TYPE_OCI1_MANIFEST: - case MEDIA_TYPE_DOCKER2_MANIFEST: - return getSingleImageDigest(imageRef); - - case MEDIA_TYPE_OCI1_MANIFEST_LIST: - case MEDIA_TYPE_DOCKER2_MANIFEST_LIST: - return getMultiImageDigests(node); - } - } - } + public static Map getImageDigests(ImageRef imageRef) + throws JsonProcessingException { + var output = execSkopeoInspect(imageRef, true); - throw new RuntimeException(String.format("The image info is invalid: %s", output.getOutput())); + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); } - static Map getMultiImageDigests(JsonNode node) { - if (node.hasNonNull("manifests")) { - 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())); - } + var node = OBJECT_MAPPER.readTree(output.getOutput()); + if (node.hasNonNull("mediaType")) { + var mediaTypeNode = node.get("mediaType"); + if (mediaTypeNode.isTextual()) { + var mediaType = mediaTypeNode.asText(); + switch (mediaType) { + case MEDIA_TYPE_OCI1_MANIFEST: + case MEDIA_TYPE_DOCKER2_MANIFEST: + return getSingleImageDigest(imageRef); + + case MEDIA_TYPE_OCI1_MANIFEST_LIST: + case MEDIA_TYPE_DOCKER2_MANIFEST_LIST: + return getMultiImageDigests(node); } - return Collections.emptyMap(); + } } - static boolean filterMediaType(JsonNode manifestNode) { - if (manifestNode.hasNonNull("mediaType")) { - 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 false; + throw new RuntimeException(String.format("The image info is invalid: %s", output.getOutput())); + } + + static Map getMultiImageDigests(JsonNode node) { + if (node.hasNonNull("manifests")) { + 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())); + } } - - static boolean filterDigest(JsonNode manifestNode) { - if (manifestNode.hasNonNull("digest")) { - var digestNode = manifestNode.get("digest"); - return digestNode.isTextual(); - } - return false; + return Collections.emptyMap(); + } + + static boolean filterMediaType(JsonNode manifestNode) { + if (manifestNode.hasNonNull("mediaType")) { + 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 false; + } - static boolean filterPlatform(JsonNode manifestNode) { - if (manifestNode.hasNonNull("platform")) { - var platformNode = manifestNode.get("platform"); - if (platformNode.isObject()) { - if (platformNode.hasNonNull("architecture") && platformNode.hasNonNull("os")) { - var architectureNode = platformNode.get("architecture"); - var osNode = platformNode.get("os"); - if (architectureNode.isTextual() && osNode.isTextual()) { - if (platformNode.hasNonNull("variant")) { - var variantNode = platformNode.get("variant"); - if (variantNode.isTextual()) { - try { - new Platform(String.format( - "%s/%s/%s", - osNode.asText(), architectureNode.asText(), variantNode.asText())); - } catch (IllegalArgumentException e) { - return false; - } - return true; - } - } - try { - new Platform(String.format("%s/%s", osNode.asText(), architectureNode.asText())); - } catch (IllegalArgumentException e) { - return false; - } - return true; - } + static boolean filterDigest(JsonNode manifestNode) { + if (manifestNode.hasNonNull("digest")) { + var digestNode = manifestNode.get("digest"); + return digestNode.isTextual(); + } + return false; + } + + static boolean filterPlatform(JsonNode manifestNode) { + if (manifestNode.hasNonNull("platform")) { + var platformNode = manifestNode.get("platform"); + if (platformNode.isObject()) { + if (platformNode.hasNonNull("architecture") && platformNode.hasNonNull("os")) { + var architectureNode = platformNode.get("architecture"); + var osNode = platformNode.get("os"); + if (architectureNode.isTextual() && osNode.isTextual()) { + if (platformNode.hasNonNull("variant")) { + var variantNode = platformNode.get("variant"); + if (variantNode.isTextual()) { + try { + new Platform( + String.format( + "%s/%s/%s", + osNode.asText(), architectureNode.asText(), variantNode.asText())); + } catch (IllegalArgumentException e) { + return false; } + return true; + } + } + try { + new Platform(String.format("%s/%s", osNode.asText(), architectureNode.asText())); + } catch (IllegalArgumentException e) { + return false; } + return true; + } } - return false; + } } + return false; + } - static Map getSingleImageDigest(ImageRef imageRef) throws JsonProcessingException { - var output = execSkopeoInspect(imageRef, false); - - if (!output.getError().isEmpty() || output.getExitCode() != 0) { - throw new RuntimeException(output.getError()); - } - - var node = OBJECT_MAPPER.readTree(output.getOutput()); + static Map getSingleImageDigest(ImageRef imageRef) + throws JsonProcessingException { + var output = execSkopeoInspect(imageRef, false); - if (node.hasNonNull("Digest")) { - var digestNode = node.get("Digest"); - if (digestNode.isTextual()) { - return Collections.singletonMap(EMPTY_PLATFORM, digestNode.asText()); - } - } - return Collections.emptyMap(); + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); } - static Operations.ProcessExecOutput execSkopeoInspect(ImageRef imageRef, boolean raw) { - var skopeo = Operations.getCustomPathOrElse("skopeo"); - - var configPath = getStringValueEnvironment(EXHORT_SKOPEO_CONFIG_PATH, ""); - var daemonHost = getStringValueEnvironment(EXHORT_IMAGE_SERVICE_ENDPOINT, ""); - - 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()) - }; - } 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()) - }; - } + var node = OBJECT_MAPPER.readTree(output.getOutput()); - return Operations.runProcessGetFullOutput(null, cmd, null); + if (node.hasNonNull("Digest")) { + var digestNode = node.get("Digest"); + if (digestNode.isTextual()) { + return Collections.singletonMap(EMPTY_PLATFORM, digestNode.asText()); + } + } + return Collections.emptyMap(); + } + + static Operations.ProcessExecOutput execSkopeoInspect(ImageRef imageRef, boolean raw) { + var skopeo = Operations.getCustomPathOrElse("skopeo"); + + var configPath = getStringValueEnvironment(EXHORT_SKOPEO_CONFIG_PATH, ""); + var daemonHost = getStringValueEnvironment(EXHORT_IMAGE_SERVICE_ENDPOINT, ""); + + 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()) + }; + } 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()) + }; } - 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); - - 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) { - this.name = name; - this.osSupplier = osSupplier; - this.archSupplier = archSupplier; - this.variantSupplier = variantSupplier; - } + 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); + + 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) { + this.name = name; + this.osSupplier = osSupplier; + this.archSupplier = archSupplier; + this.variantSupplier = variantSupplier; + } - 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))); - } + 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))); + } - String getOs() { - return osSupplier.get(); - } + String getOs() { + return osSupplier.get(); + } - String getArch() { - return archSupplier.get(); - } + String getArch() { + return archSupplier.get(); + } - String getVariant() { - return variantSupplier.get(); - } + String getVariant() { + return variantSupplier.get(); } + } } diff --git a/src/main/java/com/redhat/exhort/image/Platform.java b/src/main/java/com/redhat/exhort/image/Platform.java index bd42f6db..70a7deb6 100644 --- a/src/main/java/com/redhat/exhort/image/Platform.java +++ b/src/main/java/com/redhat/exhort/image/Platform.java @@ -20,138 +20,140 @@ 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( - new Platform().os("linux").arch("amd64"), - new Platform().os("linux").arch("arm").variant("v5"), - new Platform().os("linux").arch("arm").variant("v6"), - new Platform().os("linux").arch("arm").variant("v7"), - new Platform().os("linux").arch("arm64").variant("v8"), - new Platform().os("linux").arch("386"), - new Platform().os("linux").arch("mips64le"), - new Platform().os("linux").arch("ppc64le"), - new Platform().os("linux").arch("riscv64"), - new Platform().os("linux").arch("s390x"), - new Platform().os("windows").arch("arm64")); - - public static final Platform EMPTY_PLATFORM = new Platform(); - - private String os; - private String architecture; - private String variant; - - private Platform() {} - - public Platform(String platform) { - if (platform == null) { - throw new IllegalArgumentException("Invalid platform: null"); - } - - String[] parts = platform.split("/"); - if (parts.length == 1) { - this.os = "linux"; - this.architecture = parts[0]; - } else if (parts.length == 2) { - this.os = parts[0]; - this.architecture = parts[1]; - this.variant = getVariant(this.os, this.architecture); - } else if (parts.length == 3) { - this.os = parts[0]; - this.architecture = parts[1]; - this.variant = parts[2]; - } else { - throw new IllegalArgumentException(String.format("Invalid platform: %s", platform)); - } - - if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException(String.format("Image platform is not supported: %s", 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( + new Platform().os("linux").arch("amd64"), + new Platform().os("linux").arch("arm").variant("v5"), + new Platform().os("linux").arch("arm").variant("v6"), + new Platform().os("linux").arch("arm").variant("v7"), + new Platform().os("linux").arch("arm64").variant("v8"), + new Platform().os("linux").arch("386"), + new Platform().os("linux").arch("mips64le"), + new Platform().os("linux").arch("ppc64le"), + new Platform().os("linux").arch("riscv64"), + new Platform().os("linux").arch("s390x"), + new Platform().os("windows").arch("arm64")); + + public static final Platform EMPTY_PLATFORM = new Platform(); + + private String os; + private String architecture; + private String variant; + + private Platform() {} + + public Platform(String platform) { + if (platform == null) { + throw new IllegalArgumentException("Invalid platform: null"); } - public Platform(String os, String arch, String variant) { - if (arch == null) { - throw new IllegalArgumentException("Invalid platform arch: null"); - } - this.architecture = arch; - - if (os == null) { - this.os = "linux"; - } else { - this.os = os; - } - - if (variant != null) { - this.variant = variant; - } else { - this.variant = getVariant(this.os, this.architecture); - } - - if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException( - String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); - } + String[] parts = platform.split("/"); + if (parts.length == 1) { + this.os = "linux"; + this.architecture = parts[0]; + } else if (parts.length == 2) { + this.os = parts[0]; + this.architecture = parts[1]; + this.variant = getVariant(this.os, this.architecture); + } else if (parts.length == 3) { + this.os = parts[0]; + this.architecture = parts[1]; + this.variant = parts[2]; + } else { + throw new IllegalArgumentException(String.format("Invalid platform: %s", platform)); } - static String getVariant(String os, String arch) { - if ("linux".equals(os) && "arm64".equals(arch)) { // in case variant "v8" is not specified - return "v8"; - } - return null; + if (!SUPPORTED_PLATFORMS.contains(this)) { + throw new IllegalArgumentException( + String.format("Image platform is not supported: %s", platform)); } + } - public static boolean isVariantRequired(String os, String arch) { - return "linux".equals(os) && "arm".equals(arch); + public Platform(String os, String arch, String variant) { + if (arch == null) { + throw new IllegalArgumentException("Invalid platform arch: null"); } + this.architecture = arch; - private Platform os(String os) { - this.os = os; - return this; + if (os == null) { + this.os = "linux"; + } else { + this.os = os; } - private Platform arch(String arch) { - this.architecture = arch; - return this; + if (variant != null) { + this.variant = variant; + } else { + this.variant = getVariant(this.os, this.architecture); } - private Platform variant(String variant) { - this.variant = variant; - return this; + if (!SUPPORTED_PLATFORMS.contains(this)) { + throw new IllegalArgumentException( + String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); } + } - public String getOs() { - return os; + static String getVariant(String os, String arch) { + if ("linux".equals(os) && "arm64".equals(arch)) { // in case variant "v8" is not specified + return "v8"; } - - public String getArchitecture() { - return architecture; - } - - public String getVariant() { - return variant; - } - - @Override - 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); - } - - @Override - public int hashCode() { - return Objects.hash(os, architecture, variant); - } - - @Override - public String toString() { - if (this.variant == null) { - return String.format("%s/%s", this.os, this.architecture); - } else { - return String.format("%s/%s/%s", this.os, this.architecture, this.variant); - } + return null; + } + + public static boolean isVariantRequired(String os, String arch) { + return "linux".equals(os) && "arm".equals(arch); + } + + private Platform os(String os) { + this.os = os; + return this; + } + + private Platform arch(String arch) { + this.architecture = arch; + return this; + } + + private Platform variant(String variant) { + this.variant = variant; + return this; + } + + public String getOs() { + return os; + } + + public String getArchitecture() { + return architecture; + } + + public String getVariant() { + return variant; + } + + @Override + 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); + } + + @Override + public int hashCode() { + return Objects.hash(os, architecture, variant); + } + + @Override + public String toString() { + if (this.variant == null) { + return String.format("%s/%s", this.os, this.architecture); + } else { + return String.format("%s/%s/%s", this.os, this.architecture, this.variant); } + } } diff --git a/src/main/java/com/redhat/exhort/impl/ExhortApi.java b/src/main/java/com/redhat/exhort/impl/ExhortApi.java index f3a0aff9..ddca1efe 100644 --- a/src/main/java/com/redhat/exhort/impl/ExhortApi.java +++ b/src/main/java/com/redhat/exhort/impl/ExhortApi.java @@ -59,596 +59,660 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * 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 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"; - public static final String RHDA_SOURCE_HEADER = "rhda-source"; - public static final String RHDA_OPERATION_TYPE_HEADER = "rhda-operation-type"; - public static final String EXHORT_REQUEST_ID_HEADER_NAME = "ex-request-id"; - - private final String endpoint; - - public String getEndpoint() { - return endpoint; - } - - 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); - - } + // 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"; + public static final String RHDA_SOURCE_HEADER = "rhda-source"; + public static final String RHDA_OPERATION_TYPE_HEADER = "rhda-operation-type"; + public static final String EXHORT_REQUEST_ID_HEADER_NAME = "ex-request-id"; + + private final String endpoint; + + public String getEndpoint() { + return endpoint; + } + + 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. */ + private enum TokenProvider { + SNYK, + OSS_INDEX; /** - * Enum for identifying token environment variables and their - * corresponding request headers. + * Get the expected environment variable name. + * + * @return i.e. EXHORT_SNYK_TOKEN */ - private enum TokenProvider { - SNYK, - OSS_INDEX; - - /** - * Get the expected environment variable name. - * - * @return i.e. EXHORT_SNYK_TOKEN - */ - String getVarName() { - return String.format("EXHORT_%s_TOKEN", this); - } - - String getUserVarName() { - return String.format("EXHORT_%s_USER", this); - } - - /** - * Get the expected request header name. - * - * @return i.e. ex-snyk-token - */ - String getHeaderName() { - return String.format( - "ex-%s-token", this.toString().replace("_", "-").toLowerCase()); - } - - String getUserHeaderName() { - return String.format("ex-%s-user", this.toString().replace("_", "-").toLowerCase()); - } + String getVarName() { + return String.format("EXHORT_%s_TOKEN", this); } - private final HttpClient client; - private final ObjectMapper mapper; - - private LocalDateTime startTime; - private LocalDateTime providerEndTime; - private LocalDateTime endTime; - - public ExhortApi() { - this(HttpClient.newHttpClient()); + String getUserVarName() { + return String.format("EXHORT_%s_USER", this); } /** - * Get the HTTP protocol Version set by client in environment variable, if not set, the default is HTTP Protocol Version 1.1 + * Get the expected request header name. * - * @return i.e. HttpClient.Version.HTTP_1.1 + * @return i.e. ex-snyk-token */ - 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; - } - - 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"); - 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"); - if (exhortConfig == null) { - 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(); - properties.load(exhortConfig); - 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())); - System.setProperty("EXHORT_DEV_MODE", "false"); - } - } - - this.endpoint = getExhortUrl(); - } - - private String commonHookBeginning(boolean startOfApi) { - if (startOfApi) { - generateClientRequestId(); - if (debugLoggingIsNeeded()) { - LOG.info("Start of exhort-java-api client"); - } + String getHeaderName() { + return String.format("ex-%s-token", this.toString().replace("_", "-").toLowerCase()); + } + + String getUserHeaderName() { + return String.format("ex-%s-user", this.toString().replace("_", "-").toLowerCase()); + } + } + + private final HttpClient client; + private final ObjectMapper mapper; + + private LocalDateTime startTime; + private LocalDateTime providerEndTime; + private LocalDateTime endTime; + + public ExhortApi() { + this(HttpClient.newHttpClient()); + } + + /** + * 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; + } + + 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"); + 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"); + if (exhortConfig == null) { + LOG.info( + "config.properties not found on the class path, fallback to default DEV MODE =" + + " false"); + System.setProperty("EXHORT_DEV_MODE", "false"); } else { - if (Objects.isNull(getClientRequestId())) { - generateClientRequestId(); - } - if (debugLoggingIsNeeded()) { - - this.startTime = LocalDateTime.now(); - - LOG.info(String.format("Starting time: %s", this.startTime)); - } + Properties properties = new Properties(); + properties.load(exhortConfig); + System.setProperty("EXHORT_DEV_MODE", (String) properties.get("EXHORT_DEV_MODE")); } - return getClientRequestId(); - } - - private static void generateClientRequestId() { - RequestManager.getInstance().addClientTraceIdToRequest(UUID.randomUUID().toString()); - } - - private static String getClientRequestId() { - return RequestManager.getInstance().getTraceIdOfRequest(); - } - - public String getExhortUrl() { - String endpoint; - if (getBooleanValueEnvironment("EXHORT_DEV_MODE", "false")) { - endpoint = getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV); - - } else { - 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)); - } - return endpoint; - } - - public static boolean getBooleanValueEnvironment(String key, String 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)); - return result; - } - - @Override - 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(); + } catch (IOException e) { + 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"); + } + } + + this.endpoint = getExhortUrl(); + } + + private String commonHookBeginning(boolean startOfApi) { + if (startOfApi) { + generateClientRequestId(); + if (debugLoggingIsNeeded()) { + LOG.info("Start of exhort-java-api client"); + } + } else { + if (Objects.isNull(getClientRequestId())) { + generateClientRequestId(); + } + if (debugLoggingIsNeeded()) { + + this.startTime = LocalDateTime.now(); + + LOG.info(String.format("Starting time: %s", this.startTime)); + } + } + return getClientRequestId(); + } + + private static void generateClientRequestId() { + RequestManager.getInstance().addClientTraceIdToRequest(UUID.randomUUID().toString()); + } + + private static String getClientRequestId() { + return RequestManager.getInstance().getTraceIdOfRequest(); + } + + public String getExhortUrl() { + String endpoint; + if (getBooleanValueEnvironment("EXHORT_DEV_MODE", "false")) { + endpoint = getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV); + + } else { + 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)); + } + return endpoint; + } + + public static boolean getBooleanValueEnvironment(String key, String 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)); + return result; + } + + @Override + 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(); } - }); - } - - @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 (Objects.isNull(jsonPart) + && MediaType.APPLICATION_JSON + .toString() + .equals(mp.getBodyPart(i).getContentType())) { + jsonPart = + this.mapper.readValue( + mp.getBodyPart(i).getInputStream().readAllBytes(), + AnalysisReport.class); } - 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 { - 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) { - 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())); - } - commonHookAfterExhortResponse(); - try { - - return this.mapper.readValue(response.body(), AnalysisReport.class); - } catch (JsonProcessingException e) { - throw new CompletionException(e); - } - - } 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())); - 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))); - } - - public static boolean debugLoggingIsNeeded() { - return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG", "false")); - } - - @Override - 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); - } - - private void commonHookAfterProviderCreatedSbomAndBeforeExhort() { - 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))); - } - } - - private void commonHookAfterExhortResponse() { - 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)); - } - RequestManager.getInstance().removeClientTraceIdFromRequest(); - } - - @Override - public CompletableFuture componentAnalysis(String manifestFile) throws IOException { - String exClientTraceId = commonHookBeginning(false); - var manifestPath = Paths.get(manifestFile); - var provider = Ecosystem.getProvider(manifestPath); - var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); - var content = provider.provideComponent(manifestPath); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - 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(); - }); - } - - /** - * 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 - * @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 { - var manifestPath = Paths.get(manifestFile); - var provider = Ecosystem.getProvider(manifestPath); - var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); - var content = provider.provideStack(manifestPath); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - - return buildRequest(content, uri, acceptType, "Stack Analysis"); - } - - @Override - 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"); - } - - @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"); - } - - 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) { + } + } 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]; + }); + } + + @Override + 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) { + 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())); + } + commonHookAfterExhortResponse(); + try { + + return this.mapper.readValue(response.body(), AnalysisReport.class); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + + } 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())); + 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))); + } + + public static boolean debugLoggingIsNeeded() { + return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG", "false")); + } + + @Override + 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); + } + + private void commonHookAfterProviderCreatedSbomAndBeforeExhort() { + 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))); + } + } + + private void commonHookAfterExhortResponse() { + 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)); + } + RequestManager.getInstance().removeClientTraceIdFromRequest(); + } + + @Override + public CompletableFuture componentAnalysis(String manifestFile) + throws IOException { + String exClientTraceId = commonHookBeginning(false); + var manifestPath = Paths.get(manifestFile); + var provider = Ecosystem.getProvider(manifestPath); + var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); + var content = provider.provideComponent(manifestPath); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + 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(); + }); + } + + /** + * 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 + * @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 { + var manifestPath = Paths.get(manifestFile); + var provider = Ecosystem.getProvider(manifestPath); + var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); + var content = provider.provideStack(manifestPath); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + + return buildRequest(content, uri, acceptType, "Stack Analysis"); + } + + @Override + 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"); + } + + @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"); + } + + 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) { + 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); - } - }) - .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))); - } catch (JsonProcessingException e) { - throw new CompletionException(e); - } - } else { - return Collections.emptyMap(); - } - } - - 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); - 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) { - 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())); - } - } 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())); - } - commonHookAfterExhortResponse(); - return responseGenerator.apply(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 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))); - - // 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)) { + } + }, + e -> mapper.convertValue(e.getValue(), AnalysisReport.class))); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + } else { + return Collections.emptyMap(); + } + } + + 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); + 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) { + 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())); + } + } 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())); + } + commonHookAfterExhortResponse(); + return responseGenerator.apply(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 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))); + + // 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 { + } else { var propToken = System.getProperty(p.getVarName()); if (Objects.nonNull(propToken)) { - request.setHeader(p.getHeaderName(), propToken); + request.setHeader(p.getHeaderName(), propToken); } - } - var envUser = System.getenv(p.getUserHeaderName()); - if (Objects.nonNull(envUser)) { + } + var envUser = System.getenv(p.getUserHeaderName()); + if (Objects.nonNull(envUser)) { request.setHeader(p.getUserHeaderName(), envUser); - } else { + } else { var propUser = System.getProperty(p.getUserVarName()); if (Objects.nonNull(propUser)) { - request.setHeader(p.getUserHeaderName(), 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) - // Environment variable/property name = RHDA_SOURCE - String rhdaSource = calculateHeaderValue(RHDA_SOURCE_HEADER); - if (rhdaSource != null && Optional.of(rhdaSource).isPresent()) { - request.setHeader(RHDA_SOURCE_HEADER, rhdaSource); - } - request.setHeader(RHDA_OPERATION_TYPE_HEADER, analysisType); - - return request.build(); - } - - private String calculateHeaderValue(String headerName) { - String result; - result = calculateHeaderValueActual(headerName); - if (result == null) { - result = calculateHeaderValueActual(headerName.toUpperCase().replace("-", "_")); - } - return result; - } - - private String calculateHeaderValueActual(String headerName) { - String result = null; - result = System.getenv(headerName); - if (result == null) { - result = System.getProperty(headerName); - } - return result; - } + } + }); + // 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) + // Environment variable/property name = RHDA_SOURCE + String rhdaSource = calculateHeaderValue(RHDA_SOURCE_HEADER); + if (rhdaSource != null && Optional.of(rhdaSource).isPresent()) { + request.setHeader(RHDA_SOURCE_HEADER, rhdaSource); + } + request.setHeader(RHDA_OPERATION_TYPE_HEADER, analysisType); + + return request.build(); + } + + private String calculateHeaderValue(String headerName) { + String result; + result = calculateHeaderValueActual(headerName); + if (result == null) { + result = calculateHeaderValueActual(headerName.toUpperCase().replace("-", "_")); + } + return result; + } + + private String calculateHeaderValueActual(String headerName) { + String result = null; + result = System.getenv(headerName); + if (result == null) { + result = System.getProperty(headerName); + } + return result; + } } diff --git a/src/main/java/com/redhat/exhort/impl/RequestManager.java b/src/main/java/com/redhat/exhort/impl/RequestManager.java index ea4c4ea0..1e40bea2 100644 --- a/src/main/java/com/redhat/exhort/impl/RequestManager.java +++ b/src/main/java/com/redhat/exhort/impl/RequestManager.java @@ -21,35 +21,33 @@ public class RequestManager { - private static RequestManager requestManager; - private Map requests; - - public static RequestManager getInstance() { - if (Objects.isNull(requestManager)) { - requestManager = new RequestManager(); - } - return requestManager; - } + private static RequestManager requestManager; + private Map requests; - private RequestManager() { - requests = new HashMap<>(); + public static RequestManager getInstance() { + if (Objects.isNull(requestManager)) { + requestManager = new RequestManager(); } + return requestManager; + } - public synchronized void addClientTraceIdToRequest(String requestId) { - requests.put(concatenatedThreadId(), requestId); - } + private RequestManager() { + requests = new HashMap<>(); + } - public synchronized void removeClientTraceIdFromRequest() { - requests.remove(concatenatedThreadId()); - } + public synchronized void addClientTraceIdToRequest(String requestId) { + requests.put(concatenatedThreadId(), requestId); + } - public String getTraceIdOfRequest() { - return requests.get(concatenatedThreadId()); - } + public synchronized void removeClientTraceIdFromRequest() { + requests.remove(concatenatedThreadId()); + } - private static String concatenatedThreadId() { - return String.format( - "%s-%s", - Thread.currentThread().getName(), Thread.currentThread().getId()); - } + public String getTraceIdOfRequest() { + return requests.get(concatenatedThreadId()); + } + + private static String concatenatedThreadId() { + 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 623d54c6..2d067cfb 100644 --- a/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java +++ b/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java @@ -32,105 +32,110 @@ public class ClientTraceIdSimpleFormatter extends SimpleFormatter { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - public ClientTraceIdSimpleFormatter() { - this.objectMapper = new ObjectMapper(); - this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - } + 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<>(); - ObjectMapper objectMapper = new ObjectMapper(); - 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<>(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); - String source; - if (record.getSourceClassName() != null) { - source = record.getSourceClassName(); - if (record.getSourceMethodName() != null) { - source += " " + record.getSourceMethodName(); - } - } else { - source = record.getLoggerName(); - } - String message = formatMessage(record); - String throwable = ""; - if (record.getThrown() != null) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - pw.println(); - record.getThrown().printStackTrace(pw); - pw.close(); - throwable = sw.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()); - 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 { - messageKeysValues.put("logMessage", message); - } - try { - 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()); - } + ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); } - - private String suffixRequired(Map messageKeysValues, String message) { - if (((String) messageKeysValues.get("logMessage")).trim().contains("log Message Contains a structure")) { - return message.trim() + System.lineSeparator(); - } else { - return ""; - } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.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()); + 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 { + messageKeysValues.put("logMessage", message); } + try { + 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()); + } + } - private boolean messageContainsOutputStructure(String message) { - String messageWithLC = message.toLowerCase(); - return messageWithLC.contains("package manager") && messageWithLC.contains("output"); + private String suffixRequired(Map messageKeysValues, String message) { + if (((String) messageKeysValues.get("logMessage")) + .trim() + .contains("log Message Contains a structure")) { + return message.trim() + System.lineSeparator(); + } else { + return ""; } + } + + 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; - } - try { - objectMapper.readTree(jsonPartOfMessage); - } catch (JacksonException e) { - return false; - } - return true; + private boolean isValidJson(String jsonPartOfMessage) { + if (Objects.isNull(jsonPartOfMessage)) { + return false; + } + try { + objectMapper.readTree(jsonPartOfMessage); + } catch (JacksonException e) { + return false; } + return true; + } - 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 { - return null; - } + 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 { + 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 e79ce515..e57baea3 100644 --- a/src/main/java/com/redhat/exhort/logging/LoggersFactory.java +++ b/src/main/java/com/redhat/exhort/logging/LoggersFactory.java @@ -19,14 +19,14 @@ import java.util.logging.Logger; public class LoggersFactory { - public static Logger getLogger(String loggerName) { - Logger logger = Logger.getLogger(loggerName); - if (logger.getHandlers().length == 0) { - ConsoleHandler handler = new ConsoleHandler(); - handler.setFormatter(new ClientTraceIdSimpleFormatter()); - logger.addHandler(handler); - } - logger.setUseParentHandlers(false); - return logger; + public static Logger getLogger(String loggerName) { + Logger logger = Logger.getLogger(loggerName); + 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 f3967306..c6c4778f 100644 --- a/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java +++ b/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java @@ -27,188 +27,188 @@ public abstract class BaseJavaProvider extends Provider { - protected BaseJavaProvider(Ecosystem.Type ecosystem) { - super(ecosystem); - } + protected BaseJavaProvider(Ecosystem.Type ecosystem) { + super(ecosystem); + } - void parseDependencyTree(String src, int srcDepth, String[] lines, Sbom sbom) { - if (lines.length == 0) { - return; - } - 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) { - PackageURL from = parseDep(src); - PackageURL to = parseDep(target); - if (dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { - sbom.addDependency(from, to); - } - } else { - String[] modifiedLines = Arrays.copyOfRange(lines, index, lines.length); - parseDependencyTree(lines[index - 1], getDepth(lines[index - 1]), modifiedLines, sbom); - } - if (index < lines.length - 1) { - target = lines[++index]; - targetDepth = getDepth(target); - } else { - index++; - } - } + void parseDependencyTree(String src, int srcDepth, String[] lines, Sbom sbom) { + if (lines.length == 0) { + return; } - - static boolean dependencyIsNotTestScope(PackageURL artifact) { - return (Objects.nonNull(artifact.getQualifiers()) - && !artifact.getQualifiers().get("scope").equals("test")) - || Objects.isNull(artifact.getQualifiers()); + 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) { + PackageURL from = parseDep(src); + PackageURL to = parseDep(target); + if (dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { + sbom.addDependency(from, to); + } + } else { + String[] modifiedLines = Arrays.copyOfRange(lines, index, lines.length); + parseDependencyTree(lines[index - 1], getDepth(lines[index - 1]), modifiedLines, sbom); + } + if (index < lines.length - 1) { + target = lines[++index]; + targetDepth = getDepth(target); + } else { + index++; + } + } + } + + static boolean dependencyIsNotTestScope(PackageURL artifact) { + return (Objects.nonNull(artifact.getQualifiers()) + && !artifact.getQualifiers().get("scope").equals("test")) + || Objects.isNull(artifact.getQualifiers()); + } + + PackageURL parseDep(String dep) { + // root package + DependencyAggregator dependencyAggregator = new DependencyAggregator(); + // in case line in dependency tree text starts with a letter ( for root artifact). + if (dep.matches("^\\w.*")) { + dependencyAggregator = new DependencyAggregator(); + String[] parts = dep.split(":"); + dependencyAggregator.groupId = parts[0]; + dependencyAggregator.artifactId = parts[1]; + dependencyAggregator.version = parts[3]; + + return dependencyAggregator.toPurl(); + } + int firstDash = dep.indexOf("-"); + String dependency = dep.substring(++firstDash).trim(); + 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 scopeLength; + if (dependency.indexOf(":compile") > -1) { + scopeLength = ":compile".length(); + } else { + scopeLength = ":test".length(); + } + dependency = dependency.substring(0, endIndex + scopeLength); + String[] parts = dependency.split(":"); + // contains only GAV + packaging + scope + if (parts.length == 5) { + dependencyAggregator.groupId = parts[0]; + 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(); + } + } + // 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) { + dependencyAggregator.groupId = parts[0]; + 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(); + } + + } else { + throw new RuntimeException( + String.format("Cannot parse dependency into PackageUrl from line = \"%s\"", dep)); + } + 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("-")); + dependencyAggregator.scope = parts[parts.length - 1].substring(0, endOfLine).trim(); + } + return dependencyAggregator.toPurl(); + } - PackageURL parseDep(String dep) { - // root package - DependencyAggregator dependencyAggregator = new DependencyAggregator(); - // in case line in dependency tree text starts with a letter ( for root artifact). - if (dep.matches("^\\w.*")) { - dependencyAggregator = new DependencyAggregator(); - String[] parts = dep.split(":"); - dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId = parts[1]; - dependencyAggregator.version = parts[3]; - - return dependencyAggregator.toPurl(); - } - int firstDash = dep.indexOf("-"); - String dependency = dep.substring(++firstDash).trim(); - 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 scopeLength; - if (dependency.indexOf(":compile") > -1) { - scopeLength = ":compile".length(); - } else { - scopeLength = ":test".length(); - } - dependency = dependency.substring(0, endIndex + scopeLength); - String[] parts = dependency.split(":"); - // contains only GAV + packaging + scope - if (parts.length == 5) { - dependencyAggregator.groupId = parts[0]; - 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(); - } - } - // 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) { - dependencyAggregator.groupId = parts[0]; - 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(); - } - - } else { - throw new RuntimeException( - String.format("Cannot parse dependency into PackageUrl from line = \"%s\"", dep)); - } - 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("-")); - dependencyAggregator.scope = - parts[parts.length - 1].substring(0, endOfLine).trim(); - } - return dependencyAggregator.toPurl(); + int getDepth(String line) { + if (line == null || line.trim().equals("")) { + return -1; } - int getDepth(String line) { - if (line == null || line.trim().equals("")) { - return -1; - } + if (line.matches("^\\w.*")) { + return 0; + } - 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 - // 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, */ + static final class DependencyAggregator { + String scope = "*"; + String groupId; + String artifactId; + String version; + boolean ignored = false; /** - * Aggregator class for aggregating Dependency data over stream iterations, - **/ - static final class DependencyAggregator { - String scope = "*"; - String groupId; - String artifactId; - String version; - boolean ignored = false; - - /** - * 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); - } + * 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); + } - boolean isValid() { - return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); - } + boolean isValid() { + return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); + } - boolean isTestDependency() { - return scope.trim().equals("test"); - } + boolean isTestDependency() { + return scope.trim().equals("test"); + } - PackageURL toPurl() { - try { - 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); - } - } + PackageURL toPurl() { + try { + 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); + } + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof DependencyAggregator)) return false; - var that = (DependencyAggregator) 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); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DependencyAggregator)) return false; + var that = (DependencyAggregator) 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); + } - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId, version); - } + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); } + } } diff --git a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java index 080edc4c..80f3fc88 100644 --- a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java @@ -43,486 +43,532 @@ import java.util.stream.Collectors; /** - * 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()); - private static final String goHostArchitectureEnvName = "GOHOSTARCH"; - private static final String goHostOperationSystemEnvName = "GOHOSTOS"; - public static final String defaultMainVersion = "v0.0.0"; - private final TreeMap goEnvironmentVariableForPurl; - private final TreeMap goEnvironmentVariablesForRef; - - public String getMainModuleVersion() { - return mainModuleVersion; - } - - private String mainModuleVersion; - - 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("/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"); - try { - // provider.provideStack(path); - byte[] bytes = Files.readAllBytes(path); - provider.provideComponent(bytes); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public GoModulesProvider() { - super(Type.GOLANG); - 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); + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + private static final String goHostArchitectureEnvName = "GOHOSTARCH"; + private static final String goHostOperationSystemEnvName = "GOHOSTOS"; + public static final String defaultMainVersion = "v0.0.0"; + private final TreeMap goEnvironmentVariableForPurl; + private final TreeMap goEnvironmentVariablesForRef; + + public String getMainModuleVersion() { + return mainModuleVersion; + } + + private String mainModuleVersion; + + 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("/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"); + try { + // provider.provideStack(path); + byte[] bytes = Files.readAllBytes(path); + provider.provideComponent(bytes); + } catch (IOException e) { + throw new RuntimeException(e); } - - @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); - } - - @Override - public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException( - "provideComponent with file system path for GoModules package manager not implemented yet"); + } + + public GoModulesProvider() { + super(Type.GOLANG); + 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); + } + + @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); + } + + @Override + public Content provideComponent(Path manifestPath) throws IOException { + throw new IllegalArgumentException( + "provideComponent with file system path for GoModules package manager not implemented yet"); + } + + private Sbom getDependenciesSbomCa(byte[] manifestContent) { + Sbom sbom; + try { + Path tempRepository = Files.createTempDirectory("exhort-go"); + Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "go.mod"); + Files.deleteIfExists(path); + Path manifestPath = Files.createFile(path); + Files.write(manifestPath, manifestContent); + sbom = getDependenciesSbom(manifestPath, false); + + Files.delete(manifestPath); + Files.delete(tempRepository); + } catch (IOException e) { + throw new RuntimeException(e); } - - private Sbom getDependenciesSbomCa(byte[] manifestContent) { - Sbom sbom; - try { - Path tempRepository = Files.createTempDirectory("exhort-go"); - Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "go.mod"); - Files.deleteIfExists(path); - Path manifestPath = Files.createFile(path); - Files.write(manifestPath, manifestContent); - sbom = getDependenciesSbom(manifestPath, false); - - Files.delete(manifestPath); - Files.delete(tempRepository); - } catch (IOException e) { - throw new RuntimeException(e); + return sbom; + } + + 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) { + String[] splitParts = dependency.split(delimiter); + if (splitParts.length == 2) { + 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 sbom; + } + 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); + } + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException( + "Unable to parse golang module package : " + dependency, e); } - - 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) { - String[] splitParts = dependency.split(delimiter); - if (splitParts.length == 2) { - 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); - } - } - 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); - } - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse golang module package : " + dependency, e); - } + } + + 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() + String[] goModGraphLines = goModulesResult.split(System.lineSeparator()); + performManifestVersionsCheck(goModGraphLines, manifestPath); } - - 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() - String[] goModGraphLines = goModulesResult.split(System.lineSeparator()); - performManifestVersionsCheck(goModGraphLines, manifestPath); - } - if (!buildTree) { - sbom = buildSbomFromList(goModulesResult, ignoredDeps); - } else { - sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath); - } - // List ignoredDeps = getIgnoredDeps(manifestPath); - // sbom.filterIgnoredDeps(ignoredDeps); - return sbom; + if (!buildTree) { + sbom = buildSbomFromList(goModulesResult, ignoredDeps); + } else { + sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath); } - - private void performManifestVersionsCheck(String[] goModGraphLines, Path manifestPath) { - try { - 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) -> { + // List ignoredDeps = getIgnoredDeps(manifestPath); + // sbom.filterIgnoredDeps(ignoredDeps); + return sbom; + } + + private void performManifestVersionsCheck(String[] goModGraphLines, Path manifestPath) { + try { + 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)); - } - } - }); - }); - } catch (IOException e) { - 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()); - - // 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(); - int endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")"); - int currentIndex = 0; - while (currentIndex < endOfBlockIndex) { - int endOfLinePosition = depsInsideRequirementsBlock.indexOf(System.lineSeparator(), currentIndex); - String dependency = depsInsideRequirementsBlock - .substring(currentIndex, endOfLinePosition) - .trim(); - result.add(dependency); - currentIndex = endOfLinePosition + 1; - } - currentSegmentOfGoMod = - currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim(); - requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); - } - - return result; + 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!"); } - - private Map decideRequireBlockIndex(String currentSegmentOfGoMod) { - int index = currentSegmentOfGoMod.indexOf("require("); - int length = "require(".length(); - if (index == -1) { - index = currentSegmentOfGoMod.indexOf("require ("); - length = "require (".length(); - if (index == -1) { - index = currentSegmentOfGoMod.indexOf("require ("); - length = "require (".length(); - } - } - return Map.of("index", index, "length", length); + } + + 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()); + + // 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(); + int endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")"); + int currentIndex = 0; + while (currentIndex < endOfBlockIndex) { + int endOfLinePosition = + depsInsideRequirementsBlock.indexOf(System.lineSeparator(), currentIndex); + String dependency = + depsInsideRequirementsBlock.substring(currentIndex, endOfLinePosition).trim(); + result.add(dependency); + currentIndex = endOfLinePosition + 1; + } + currentSegmentOfGoMod = currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim(); + requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); } - public void determineMainModuleVersion(Path directory) { - this.calculateMainModuleVersion(directory); + return result; + } + + private Map decideRequireBlockIndex(String currentSegmentOfGoMod) { + int index = currentSegmentOfGoMod.indexOf("require("); + int length = "require(".length(); + if (index == -1) { + index = currentSegmentOfGoMod.indexOf("require ("); + length = "require (".length(); + if (index == -1) { + index = currentSegmentOfGoMod.indexOf("require ("); + length = "require (".length(); + } } - - 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()); - } - } - } - } - - 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()); - - 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. - 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) { - targetLines = Arrays.copyOfRange(lines, startingIndex, EndingIndex); - } - } + return Map.of("index", index, "length", length); + } + + 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(); } - // DEBUG - // System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); - boolean goMvsLogicEnabled = getBooleanValueEnvironment("EXHORT_GO_MVS_LOGIC_ENABLED", "false"); - if (goMvsLogicEnabled) { - edges = getFinalPackagesVersionsForModule(edges, manifestPath); + } else { + if (!latestTagInfo.getCurrentCommitDigest().trim().equals("")) { + this.mainModuleVersion = + vcs.getPseudoVersion(latestTagInfo, getDefaultMainModuleVersion()); } - // Build Sbom - String rootPackage = getParentVertex(lines[0]); - - PackageURL root = toPurl(rootPackage, "@", this.goEnvironmentVariableForPurl); - 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()); - sbom.filterIgnoredDeps(ignoredDepsPurl); - ArrayList ignoredDepsByName = new ArrayList<>(); - ignoredDeps.forEach(purl -> { - if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { - ignoredDepsByName.add(purl.getName()); - } - }); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(ignoredDepsByName); - - 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)); - 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); - }); - - return listWithModifiedVersions; - } - - 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) { - 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 { - return packagePlusVersion; + } + + 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()); + + 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. + 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) { + targetLines = Arrays.copyOfRange(lines, startingIndex, EndingIndex); } + } } - - private boolean dependencyNotToBeIgnored(List ignoredDeps, PackageURL checkedPurl) { - 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()); + // DEBUG + // System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); + boolean goMvsLogicEnabled = getBooleanValueEnvironment("EXHORT_GO_MVS_LOGIC_ENABLED", "false"); + if (goMvsLogicEnabled) { + edges = getFinalPackagesVersionsForModule(edges, manifestPath); } + // Build Sbom + String rootPackage = getParentVertex(lines[0]); + + PackageURL root = toPurl(rootPackage, "@", this.goEnvironmentVariableForPurl); + 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()); + sbom.filterIgnoredDeps(ignoredDepsPurl); + ArrayList ignoredDepsByName = new ArrayList<>(); + ignoredDeps.forEach( + purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } + }); + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(ignoredDepsByName); + + 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)); + 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); + }); - private static TreeMap getQualifiers(boolean includeOsAndArch) { - - if (includeOsAndArch) { - var go = Operations.getCustomPathOrElse("go"); - 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")); + return listWithModifiedVersions; + } + + 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) { + 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 { + return packagePlusVersion; } - - 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()); - String envValue = goEnvironmentVariables.substring(beginIndex).substring(0, endOfLineIndex); - return envValue.replaceAll("\"", ""); + } + + private boolean dependencyNotToBeIgnored(List ignoredDeps, PackageURL checkedPurl) { + 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()); + } + + private static TreeMap getQualifiers(boolean includeOsAndArch) { + + if (includeOsAndArch) { + var go = Operations.getCustomPathOrElse("go"); + 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)); } - private String buildGoModulesDependencies(Path manifestPath) throws JsonMappingException, JsonProcessingException { - var go = Operations.getCustomPathOrElse("go"); - String[] goModulesDeps; - 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)); - } - return goModulesOutput; + return new TreeMap(Map.of("type", "module")); + } + + 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()); + String envValue = goEnvironmentVariables.substring(beginIndex).substring(0, endOfLineIndex); + return envValue.replaceAll("\"", ""); + } + + private String buildGoModulesDependencies(Path manifestPath) + throws JsonMappingException, JsonProcessingException { + var go = Operations.getCustomPathOrElse("go"); + String[] goModulesDeps; + 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)); } - - private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) { - String[] allModulesFlat = golangDeps.split(System.lineSeparator()); - String parentVertex = getParentVertex(allModulesFlat[0]); - 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.addRoot(root); - deps.forEach(dep -> { - PackageURL targetPurl = toPurl(dep, "@", this.goEnvironmentVariableForPurl); - if (dependencyNotToBeIgnored(ignoredDeps, targetPurl)) { - sbom.addDependency(root, targetPurl); - } + return goModulesOutput; + } + + private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) { + String[] allModulesFlat = golangDeps.split(System.lineSeparator()); + String parentVertex = getParentVertex(allModulesFlat[0]); + 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.addRoot(root); + 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()); - } + List ignoredDepsByName = new ArrayList<>(); + ignoredDeps.forEach( + purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } }); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(ignoredDepsByName); - return sbom; - } - - 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()); - return ignored; - } - - private String extractPackageName(String line) { + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(ignoredDepsByName); + return sbom; + } + + 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()); + return ignored; + } + + private String extractPackageName(String line) { + String trimmedRow = line.trim(); + int firstRemarkNotationOccurrence = trimmedRow.indexOf("//"); + 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(); - int firstRemarkNotationOccurrence = trimmedRow.indexOf("//"); - 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; - } - } - } + // 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) { - String[] edgeParts = edge.trim().split(" "); - return edgeParts[0]; - } + 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]; - } + String[] edgeParts = edge.trim().split(" "); + return edgeParts[1]; + } - private static String getDefaultMainModuleVersion() { - return defaultMainVersion; - } + 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 e0eb9029..27b6387e 100644 --- a/src/main/java/com/redhat/exhort/providers/GradleProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GradleProvider.java @@ -42,209 +42,214 @@ 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()); + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + + public GradleProvider() { + super(Type.GRADLE); + } - public GradleProvider() { - super(Type.GRADLE); + @Override + 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)); } + Map propertiesMap = extractProperties(manifestPath); - @Override - 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)); - } - Map propertiesMap = extractProperties(manifestPath); + var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, "runtimeClasspath"); + var ignored = getIgnoredDeps(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()); + + // Process each ignored dependency + for (String dependency : ignoredLines) { + String ignoredDepInfo; + if (isNotation(dependency)) { + ignoredDepInfo = getDepFromNotation(dependency, manifestPath); + } else { + ignoredDepInfo = getDepInfo(dependency); + } + + if (ignoredDepInfo != null) { + ignored.add(ignoredDepInfo); + } } - 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()); - - // Process each ignored dependency - for (String dependency : ignoredLines) { - String ignoredDepInfo; - if (isNotation(dependency)) { - ignoredDepInfo = getDepFromNotation(dependency, manifestPath); - } else { - ignoredDepInfo = getDepInfo(dependency); - } - - if (ignoredDepInfo != null) { - ignored.add(ignoredDepInfo); - } - } + return ignored; + } - return ignored; + private String getDepInfo(String dependencyLine) { + // Check if the line contains "group:", "name:", and "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; + + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2); + + switch (key) { + case "group": + groupId = value; + break; + case "name": + artifactId = value; + break; + case "version": + version = value; + break; + } + } + 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("['\"](.*?)['\"]"); + Matcher matcher = pattern.matcher(dependencyLine); + // Check if the matcher finds a match + if (matcher.find()) { + // Get the matched string inside single or double quotes + String dependency = matcher.group(1); + String[] dependencyParts = dependency.split(":"); + if (dependencyParts.length == 3) { + // Extract groupId, artifactId, and version + String groupId = dependencyParts[0]; + String artifactId = dependencyParts[1]; + String version = dependencyParts[2]; + + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); + } + } } + return null; + } - private String getDepInfo(String dependencyLine) { - // Check if the line contains "group:", "name:", and "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; - - while (matcher.find()) { - String key = matcher.group(1); - String value = matcher.group(2); - - switch (key) { - case "group": - groupId = value; - break; - case "name": - artifactId = value; - break; - case "version": - version = value; - break; - } - } - 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("['\"](.*?)['\"]"); - Matcher matcher = pattern.matcher(dependencyLine); - // Check if the matcher finds a match - if (matcher.find()) { - // Get the matched string inside single or double quotes - String dependency = matcher.group(1); - String[] dependencyParts = dependency.split(":"); - if (dependencyParts.length == 3) { - // Extract groupId, artifactId, and version - String groupId = dependencyParts[0]; - String artifactId = dependencyParts[1]; - String version = dependencyParts[2]; - - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); - } - } - } - return null; + private String getDepFromNotation(String dependency, Path manifestPath) throws IOException { + // Extract everything after "libs." + String alias = dependency.substring(dependency.indexOf("libs.") + "libs.".length()); + alias = alias.replace(".", "-"); + // Read and parse the TOML file + TomlParseResult toml = Toml.parse(getLibsVersionsTomlPath(manifestPath)); + TomlTable librariesTable = toml.getTable("libraries"); + TomlTable dependencyTable = librariesTable.getTable(alias); + 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")); + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); } - private String getDepFromNotation(String dependency, Path manifestPath) throws IOException { - // Extract everything after "libs." - String alias = dependency.substring(dependency.indexOf("libs.") + "libs.".length()); - alias = alias.replace(".", "-"); - // Read and parse the TOML file - TomlParseResult toml = Toml.parse(getLibsVersionsTomlPath(manifestPath)); - TomlTable librariesTable = toml.getTable("libraries"); - TomlTable dependencyTable = librariesTable.getTable(alias); - 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")); - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); - } + return null; + } - return null; - } + private Path getLibsVersionsTomlPath(Path manifestPath) { + return manifestPath.getParent().resolve("gradle/libs.versions.toml"); + } - private Path getLibsVersionsTomlPath(Path manifestPath) { - return manifestPath.getParent().resolve("gradle/libs.versions.toml"); + public PackageURL toPurl(String groupId, String artifactId, String version) { + try { + return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, null, null); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL", e); } + } - public PackageURL toPurl(String groupId, String artifactId, String version) { - try { - return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, null, null); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL", e); + public static boolean isNotation(String line) { + int colonCount = 0; + for (char c : line.toCharArray()) { + if (c == ':') { + colonCount++; + if (colonCount > 1) { + return false; // Likely full dependency with group and artifact } + } } + return true; // Potentially a notation + } - public static boolean isNotation(String line) { - int colonCount = 0; - for (char c : line.toCharArray()) { - if (c == ':') { - colonCount++; - if (colonCount > 1) { - return false; // Likely full dependency with group and artifact - } - } - } - return true; // Potentially a notation - } + private boolean isIgnoredLine(String line) { + return line.contains("exhortignore"); + } - private boolean isIgnoredLine(String line) { - return line.contains("exhortignore"); + private String extractPackageName(String line) { + String packageName = line.trim(); + // Extract the package name before the comment + int commentIndex = packageName.indexOf("//"); + if (commentIndex != -1) { + packageName = packageName.substring(0, commentIndex).trim(); } - - private String extractPackageName(String line) { - String packageName = line.trim(); - // Extract the package name before the comment - int commentIndex = packageName.indexOf("//"); - if (commentIndex != -1) { - packageName = packageName.substring(0, commentIndex).trim(); - } - // Remove any other trailing comments or spaces - commentIndex = packageName.indexOf("/*"); - if (commentIndex != -1) { - packageName = packageName.substring(0, commentIndex).trim(); - } - return packageName; + // Remove any other trailing comments or spaces + commentIndex = packageName.indexOf("/*"); + if (commentIndex != -1) { + packageName = packageName.substring(0, commentIndex).trim(); } + return packageName; + } - private static Path getDependencies(Path manifestPath) throws IOException { - // check for custom gradle executable - var gradle = Operations.getCustomPathOrElse("gradle"); - // create a temp file for storing the dependency tree in - var tempFile = Files.createTempFile("exhort_graph_", null); - // the command will create the dependency tree in the temp file - String gradleCommand = gradle + " dependencies"; + private static Path getDependencies(Path manifestPath) throws IOException { + // check for custom gradle executable + var gradle = Operations.getCustomPathOrElse("gradle"); + // create a temp file for storing the dependency tree in + var tempFile = Files.createTempFile("exhort_graph_", null); + // the command will create the dependency tree in the temp file + String gradleCommand = gradle + " dependencies"; - String[] cmdList = gradleCommand.split("\\s+"); - String gradleOutput = - Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); - Files.writeString(tempFile, gradleOutput); + String[] cmdList = gradleCommand.split("\\s+"); + String gradleOutput = + Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); + Files.writeString(tempFile, gradleOutput); - return tempFile; - } + return tempFile; + } - private Path getProperties(Path manifestPath) throws IOException { - Path propsTempFile = Files.createTempFile("propsfile", ".txt"); - var gradle = Operations.getCustomPathOrElse("gradle"); - String propCmd = gradle + " properties"; - String[] propCmdList = propCmd.split("\\s+"); - String properties = - Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList); - // Create a temporary file - Files.writeString(propsTempFile, properties); - - return propsTempFile; - } + private Path getProperties(Path manifestPath) throws IOException { + Path propsTempFile = Files.createTempFile("propsfile", ".txt"); + var gradle = Operations.getCustomPathOrElse("gradle"); + String propCmd = gradle + " properties"; + String[] propCmdList = propCmd.split("\\s+"); + 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 { - var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - String root = getRoot(textFormatFile, propertiesMap); + private Sbom buildSbomFromTextFormat( + Path textFormatFile, Map propertiesMap, String configName) + throws IOException { + var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + String root = getRoot(textFormatFile, propertiesMap); var rootPurl = parseDep(root); sbom.addRoot(rootPurl); @@ -264,102 +269,106 @@ private Sbom buildSbomFromTextFormat(Path textFormatFile, Map pr return sbom; } - 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; - return root; - } + 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; + return root; + } - private String extractRootProjectValue(Path inputFilePath) throws IOException { - List lines = Files.readAllLines(inputFilePath); - for (String line : lines) { - if (line.contains("Root project")) { - Pattern pattern = Pattern.compile("Root project '(.+)'"); - Matcher matcher = pattern.matcher(line); - if (matcher.find()) { - return matcher.group(1); - } - } + private String extractRootProjectValue(Path inputFilePath) throws IOException { + List lines = Files.readAllLines(inputFilePath); + for (String line : lines) { + if (line.contains("Root project")) { + Pattern pattern = Pattern.compile("Root project '(.+)'"); + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return matcher.group(1); } - return null; + } } + return null; + } - private Map extractProperties(Path manifestPath) throws IOException { - Path propsTempFile = getProperties(manifestPath); - String content = Files.readString(propsTempFile); - // Define the regular expression pattern for key-value pairs - Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)"); - Matcher matcher = pattern.matcher(content); - // Create a Map to store key-value pairs - Map keyValueMap = new HashMap<>(); - - // Iterate through matches and add them to the map - while (matcher.find()) { - String key = matcher.group(1).trim(); - String value = matcher.group(2).trim(); - keyValueMap.put(key, value); - } - // Check if any key-value pairs were found - if (!keyValueMap.isEmpty()) { - return keyValueMap; - } else { - return null; - } + private Map extractProperties(Path manifestPath) throws IOException { + Path propsTempFile = getProperties(manifestPath); + String content = Files.readString(propsTempFile); + // Define the regular expression pattern for key-value pairs + Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)"); + Matcher matcher = pattern.matcher(content); + // Create a Map to store key-value pairs + Map keyValueMap = new HashMap<>(); + + // Iterate through matches and add them to the map + while (matcher.find()) { + String key = matcher.group(1).trim(); + String value = matcher.group(2).trim(); + keyValueMap.put(key, value); } - - private List extractLines(Path inputFilePath, String startMarker) throws IOException { - List lines = Files.readAllLines(inputFilePath); - List extractedLines = new ArrayList<>(); - boolean startFound = false; - - for (String line : lines) { - // If the start marker is found, set startFound to true - if (line.startsWith(startMarker)) { - startFound = true; - continue; // Skip the line containing the startMarker - } - // If startFound is true and the line is not empty, add it to the extractedLines list - if (startFound && !line.trim().isEmpty()) { - extractedLines.add(line); - } - // If an empty line is encountered, break out of the loop - if (startFound && line.trim().isEmpty()) { - break; - } - } - return extractedLines; + // Check if any key-value pairs were found + if (!keyValueMap.isEmpty()) { + return keyValueMap; + } else { + return null; } + } - @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."); + private List extractLines(Path inputFilePath, String startMarker) throws IOException { + List lines = Files.readAllLines(inputFilePath); + List extractedLines = new ArrayList<>(); + boolean startFound = false; + + for (String line : lines) { + // If the start marker is found, set startFound to true + if (line.startsWith(startMarker)) { + startFound = true; + continue; // Skip the line containing the startMarker + } + // If startFound is true and the line is not empty, add it to the extractedLines list + if (startFound && !line.trim().isEmpty()) { + extractedLines.add(line); + } + // If an empty line is encountered, break out of the loop + if (startFound && line.trim().isEmpty()) { + break; + } } + return extractedLines; + } - @Override - public Content provideComponent(Path manifestPath) throws IOException { - - Path tempFile = getDependencies(manifestPath); - Map propertiesMap = extractProperties(manifestPath); + @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."); + } - String[] configurationNames = {"api", "implementation", "compile"}; + @Override + public Content provideComponent(Path manifestPath) throws IOException { - String configName = null; - for (String configurationName : configurationNames) { - List directDependencies = extractLines(tempFile, configurationName); + Path tempFile = getDependencies(manifestPath); + Map propertiesMap = extractProperties(manifestPath); - // Check if dependencies are found for the current configuration - if (!directDependencies.isEmpty()) { - configName = configurationName; - break; - } - } + String[] configurationNames = {"api", "implementation", "compile"}; - var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, configName); - var ignored = getIgnoredDeps(manifestPath); + String configName = null; + for (String configurationName : configurationNames) { + List directDependencies = extractLines(tempFile, configurationName); - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + // Check if dependencies are found for the current configuration + if (!directDependencies.isEmpty()) { + configName = configurationName; + break; + } } + + var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, configName); + var ignored = getIgnoredDeps(manifestPath); + + 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 96dbaf45..26290196 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java @@ -42,358 +42,374 @@ 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()); + 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"); - LocalDateTime start = LocalDateTime.now(); - System.out.print(start); - Content content = javaMavenProvider.provideStack(Path.of("/tmp/devfile-sample-java-springboot-basic/pom.xml")); + 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"); + LocalDateTime start = LocalDateTime.now(); + System.out.print(start); + 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); - 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)); - } + // 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); - } + 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 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()); - } + @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 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()); + } }; - // 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()); - // execute the tree command - Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs); - if (debugLoggingIsNeeded()) { - String stackAnalysisDependencyTree = Files.readString(tmpFile); - 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); + // 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()); + // execute the tree command + Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs); + if (debugLoggingIsNeeded()) { + String stackAnalysisDependencyTree = Files.readString(tmpFile); + log.info( + String.format( + "Package Manager Maven Stack Analysis Dependency Tree Output: %s %s", + System.lineSeparator(), stackAnalysisDependencyTree)); } - private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { - var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - List lines = Files.readAllLines(textFormatFile); - var root = lines.get(0); - var rootPurl = parseDep(root); - sbom.addRoot(rootPurl); - lines.remove(0); - String[] array = new String[lines.size()]; - lines.toArray(array); - // createSbomIteratively(lines,sbom); - parseDependencyTree(root, 0, array, sbom); - return sbom; - } + 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); + } - private PackageURL txtPkgToPurl(String dotPkg) { - 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); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse dot package: " + dotPkg, e); - } - } - throw new IllegalArgumentException("Invalid dot package format: " + dotPkg); - } + private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { + var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + List lines = Files.readAllLines(textFormatFile); + var root = lines.get(0); + var rootPurl = parseDep(root); + sbom.addRoot(rootPurl); + lines.remove(0); + String[] array = new String[lines.size()]; + lines.toArray(array); + // createSbomIteratively(lines,sbom); + parseDependencyTree(root, 0, array, sbom); + return sbom; + } - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - // save content in temporary file - var originPom = Files.createTempFile("exhort_orig_pom_", ".xml"); - Files.write(originPom, manifestContent); - // build effective pom command - Content content = generateSbomFromEffectivePom(originPom); - Files.delete(originPom); - return content; + private PackageURL txtPkgToPurl(String dotPkg) { + 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); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse dot package: " + dotPkg, e); + } } + throw new IllegalArgumentException("Invalid dot package format: " + dotPkg); + } - 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() - }; - // execute the effective pom command - Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs()); - if (debugLoggingIsNeeded()) { - String CaEffectivePoM = Files.readString(tmpEffPom); - 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 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)); + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + // save content in temporary file + var originPom = Files.createTempFile("exhort_orig_pom_", ".xml"); + Files.write(originPom, manifestContent); + // build effective pom command + Content content = generateSbomFromEffectivePom(originPom); + Files.delete(originPom); + return content; + } - // build and return content for constructing request to the backend - return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + 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() + }; + // execute the effective pom command + Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs()); + if (debugLoggingIsNeeded()) { + String CaEffectivePoM = Files.readString(tmpEffPom); + 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 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)); - @Override - public Content provideComponent(Path manifestPath) throws IOException { - Content content = generateSbomFromEffectivePom(manifestPath); - return content; - } + // build and return content for constructing request to the backend + return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + } - private PackageURL getRoot(final Path manifestPath) throws IOException { - XMLStreamReader reader = null; - try { - reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); - DependencyAggregator dependencyAggregator = null; - boolean isRoot = false; - while (reader.hasNext()) { - reader.next(); // get the next event - if (reader.isStartElement() && "project".equals(reader.getLocalName())) { - isRoot = true; - dependencyAggregator = new DependencyAggregator(); - continue; - } - if (!Objects.isNull(dependencyAggregator)) { - if (reader.isStartElement()) { - switch (reader.getLocalName()) { - case "groupId": // starting "groupId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.groupId = reader.getText(); - break; - case "artifactId": // starting "artifactId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.artifactId = reader.getText(); - break; - case "version": // starting "version" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.version = reader.getText(); - break; - } - } - if (isRoot && dependencyAggregator.isValid()) { - return dependencyAggregator.toPurl(); - } - } - } - } catch (XMLStreamException exc) { - throw new IOException(exc); - } finally { - if (!Objects.isNull(reader)) { - try { - reader.close(); // close stream if open - } catch (XMLStreamException e) { - // - } + @Override + public Content provideComponent(Path manifestPath) throws IOException { + Content content = generateSbomFromEffectivePom(manifestPath); + return content; + } + + private PackageURL getRoot(final Path manifestPath) throws IOException { + XMLStreamReader reader = null; + try { + reader = + XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); + DependencyAggregator dependencyAggregator = null; + boolean isRoot = false; + while (reader.hasNext()) { + reader.next(); // get the next event + if (reader.isStartElement() && "project".equals(reader.getLocalName())) { + isRoot = true; + dependencyAggregator = new DependencyAggregator(); + continue; + } + if (!Objects.isNull(dependencyAggregator)) { + if (reader.isStartElement()) { + switch (reader.getLocalName()) { + case "groupId": // starting "groupId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.groupId = reader.getText(); + break; + case "artifactId": // starting "artifactId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.artifactId = reader.getText(); + break; + case "version": // starting "version" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.version = reader.getText(); + break; } + } + if (isRoot && dependencyAggregator.isValid()) { + return dependencyAggregator.toPurl(); + } } - - throw new IllegalStateException("Unable to retrieve Root dependency from effective pom"); + } + } catch (XMLStreamException exc) { + throw new IOException(exc); + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); // close stream if open + } catch (XMLStreamException e) { + // + } + } } - private List getDependencies(final Path manifestPath) throws IOException { - List deps = new ArrayList<>(); - XMLStreamReader reader = null; - try { - // 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 - DependencyAggregator dependencyAggregator = null; - 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; - } + throw new IllegalStateException("Unable to retrieve Root dependency from effective pom"); + } - // if dependency aggregator haven't been initiated, - // we're currently not iterating over a "dependency" tag - no need for further parsing - 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())) { - dependencyAggregator.ignored = true; - continue; - } + private List getDependencies(final Path manifestPath) throws IOException { + List deps = new ArrayList<>(); + XMLStreamReader reader = null; + try { + // 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 + DependencyAggregator dependencyAggregator = null; + 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; + } - if (reader.isStartElement()) { - // NOTE if we want to include "scope" tags in ignore, - // add a case here and a property in DependencyIgnore - switch (reader.getLocalName()) { - case "groupId": // starting "groupId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.groupId = reader.getText(); - break; - case "artifactId": // starting "artifactId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.artifactId = reader.getText(); - break; + // if dependency aggregator haven't been initiated, + // we're currently not iterating over a "dependency" tag - no need for further parsing + 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())) { + dependencyAggregator.ignored = true; + continue; + } - case "scope": - reader.next(); - dependencyAggregator.scope = reader.getText() != null - ? reader.getText().trim() - : "*"; - break; - case "version": // starting "version" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.version = reader.getText(); - break; - } - } + if (reader.isStartElement()) { + // NOTE if we want to include "scope" tags in ignore, + // add a case here and a property in DependencyIgnore + switch (reader.getLocalName()) { + case "groupId": // starting "groupId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.groupId = reader.getText(); + break; + case "artifactId": // starting "artifactId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.artifactId = reader.getText(); + break; - if (reader.isEndElement() && "dependency".equals(reader.getLocalName())) { - // add object to list and reset dependency aggregator - deps.add(dependencyAggregator); - dependencyAggregator = null; - } - } - } - } catch (XMLStreamException exc) { - throw new IOException(exc); - } finally { - if (!Objects.isNull(reader)) { - try { - reader.close(); // close stream if open - } catch (XMLStreamException e) { - // - } + case "scope": + reader.next(); + dependencyAggregator.scope = + reader.getText() != null ? reader.getText().trim() : "*"; + break; + case "version": // starting "version" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.version = reader.getText(); + break; } - } + } - return deps; + if (reader.isEndElement() && "dependency".equals(reader.getLocalName())) { + // add object to list and reset dependency aggregator + deps.add(dependencyAggregator); + dependencyAggregator = null; + } + } + } + } catch (XMLStreamException exc) { + throw new IOException(exc); + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); // close stream if open + } catch (XMLStreamException e) { + // + } + } } - Map getMvnExecEnvs() { - var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME", ""); - if (javaHome != null && !javaHome.isBlank()) { - return Collections.singletonMap("JAVA_HOME", javaHome); - } - return null; + return deps; + } + + Map getMvnExecEnvs() { + var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME", ""); + if (javaHome != null && !javaHome.isBlank()) { + return Collections.singletonMap("JAVA_HOME", javaHome); } + return null; + } - // 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 static final class DependencyAggregator { - private String scope = "*"; - private String groupId; - private String artifactId; - private String version; - boolean ignored = false; + // 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 static final class DependencyAggregator { + private String scope = "*"; + private String groupId; + private String artifactId; + private String version; + boolean ignored = false; - /** - * 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); - } + /** + * 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); + } - public boolean isValid() { - return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); - } + public boolean isValid() { + return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); + } - public boolean isTestDependency() { - return scope.trim().equals("test"); - } + 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); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL", e); - } - } + public PackageURL toPurl() { + try { + 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); + } + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof DependencyAggregator)) return false; - var that = (DependencyAggregator) 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); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DependencyAggregator)) return false; + var that = (DependencyAggregator) 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); + } - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId, version); - } + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); } + } } diff --git a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java index 1427a752..54e4e7d4 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java @@ -43,197 +43,204 @@ 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); - } - - @Override - 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); + private System.Logger log = System.getLogger(this.getClass().getName()); + + public JavaScriptNpmProvider() { + super(Type.NPM); + } + + @Override + 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); + } + + @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); + } + + @Override + public Content provideComponent(Path manifestPath) throws IOException { + 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")); + Files.write(path, manifestContent); + sbom = getDependencySbom(path, false, true); + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); } - - @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 sbom; + } + + private PackageURL getRoot(JsonNode jsonDependenciesNpm) throws MalformedPackageURLException { + return toPurl( + jsonDependenciesNpm.get("name").asText(), jsonDependenciesNpm.get("version").asText()); + } + + private PackageURL toPurl(String name, String version) throws MalformedPackageURLException { + String[] parts = name.split("/"); + if (parts.length == 2) { + return new PackageURL(Ecosystem.Type.NPM.getType(), parts[0], parts[1], version, null, null); } - - @Override - public Content provideComponent(Path manifestPath) throws IOException { - 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")); - Files.write(path, manifestContent); - sbom = getDependencySbom(path, false, true); - Files.delete(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - return sbom; - } - - private PackageURL getRoot(JsonNode jsonDependenciesNpm) throws MalformedPackageURLException { - return toPurl( - jsonDependenciesNpm.get("name").asText(), - jsonDependenciesNpm.get("version").asText()); - } - - private PackageURL toPurl(String name, String version) throws MalformedPackageURLException { - String[] parts = name.split("/"); - if (parts.length == 2) { - return new PackageURL(Ecosystem.Type.NPM.getType(), parts[0], parts[1], version, null, null); - } - return new PackageURL(Ecosystem.Type.NPM.getType(), null, parts[0], version, null, null); - } - - private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies) - throws MalformedPackageURLException { - Iterator> fields = dependencies.fields(); - while (fields.hasNext()) { - Entry e = fields.next(); - String name = e.getKey(); - JsonNode versionNode = e.getValue().get("version"); - if (versionNode == null) { - continue; // ignore optional dependencies - } - String version = versionNode.asText(); - PackageURL purl = toPurl(name, version); - sbom.addDependency(from, purl); - JsonNode transitiveDeps = e.getValue().findValue("dependencies"); - if (transitiveDeps != null) { - addDependenciesOf(sbom, purl, transitiveDeps); - } - } - } - - 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; + return new PackageURL(Ecosystem.Type.NPM.getType(), null, parts[0], version, null, null); + } + + private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies) + throws MalformedPackageURLException { + Iterator> fields = dependencies.fields(); + while (fields.hasNext()) { + Entry e = fields.next(); + String name = e.getKey(); + JsonNode versionNode = e.getValue().get("version"); + if (versionNode == null) { + continue; // ignore optional dependencies + } + String version = versionNode.asText(); + PackageURL purl = toPurl(name, version); + sbom.addDependency(from, purl); + JsonNode transitiveDeps = e.getValue().findValue("dependencies"); + if (transitiveDeps != null) { + addDependenciesOf(sbom, purl, transitiveDeps); + } } - - 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[] { + } + + 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) + 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() + }; + // execute the clean command + Operations.runProcess(createPackageLock, npmEnvs); + String[] npmAllDeps; + Path workDir = null; + if (!manifestPath.getParent().toString().trim().contains(" ")) { + + npmAllDeps = + new String[] { npm, - "i", + "ls", + includeTransitive ? "--all" : "", + "--omit=dev", "--package-lock-only", + "--json", "--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" - }; - 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)); - } 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 (!includeTransitive) { - if (deletePackageLock) { - try { - Files.delete(packageLockJson); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - return objectMapper.readTree(npmOutput); + }; + } else { + npmAllDeps = + new String[] { + npm, + "ls", + includeTransitive ? "--all" : "", + "--omit=dev", + "--package-lock-only", + "--json" + }; + workDir = manifestPath.getParent(); } - - private Sbom buildSbom(JsonNode npmListResult) { - Sbom sbom = SbomFactory.newInstance(); + // 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)); + } 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 (!includeTransitive) { + if (deletePackageLock) { try { - PackageURL root = getRoot(npmListResult); - sbom.addRoot(root); - JsonNode dependencies = npmListResult.get("dependencies"); - addDependenciesOf(sbom, root, dependencies); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse NPM Json", e); + Files.delete(packageLockJson); + } catch (IOException e) { + throw new RuntimeException(e); } - return sbom; + } } - - private List getIgnoredDeps(Path manifestPath) throws IOException { - var ignored = new ArrayList(); - var root = new ObjectMapper().readTree(Files.newInputStream(manifestPath)); - var ignoredNode = root.withArray("exhortignore"); - if (ignoredNode == null) { - return ignored; - } - for (JsonNode n : ignoredNode) { - ignored.add(n.asText()); - } - return ignored; + return objectMapper.readTree(npmOutput); + } + + private Sbom buildSbom(JsonNode npmListResult) { + Sbom sbom = SbomFactory.newInstance(); + try { + PackageURL root = getRoot(npmListResult); + sbom.addRoot(root); + JsonNode dependencies = npmListResult.get("dependencies"); + addDependenciesOf(sbom, root, dependencies); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse NPM Json", e); } - - Map getNpmExecEnv() { - String nodeHome = System.getProperty("NODE_HOME"); - if (nodeHome != null && !nodeHome.isBlank()) { - String path = System.getenv("PATH"); - if (path != null) { - return Collections.singletonMap("PATH", path + File.pathSeparator + nodeHome); - } else { - return Collections.singletonMap("PATH", nodeHome); - } - } - return null; + return sbom; + } + + private List getIgnoredDeps(Path manifestPath) throws IOException { + var ignored = new ArrayList(); + var root = new ObjectMapper().readTree(Files.newInputStream(manifestPath)); + var ignoredNode = root.withArray("exhortignore"); + if (ignoredNode == null) { + return ignored; + } + for (JsonNode n : ignoredNode) { + ignored.add(n.asText()); + } + return ignored; + } + + Map getNpmExecEnv() { + String nodeHome = System.getProperty("NODE_HOME"); + if (nodeHome != null && !nodeHome.isBlank()) { + String path = System.getenv("PATH"); + if (path != null) { + return Collections.singletonMap("PATH", path + File.pathSeparator + nodeHome); + } else { + return Collections.singletonMap("PATH", nodeHome); + } } + return null; + } } diff --git a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java index d286555e..da3a83e7 100644 --- a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java +++ b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java @@ -41,239 +41,274 @@ public final class PythonPipProvider extends Provider { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - public void setPythonController(PythonControllerBase pythonController) { - this.pythonController = pythonController; - } + public void setPythonController(PythonControllerBase pythonController) { + this.pythonController = pythonController; + } - private PythonControllerBase 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")); - String s = new String(content.buffer); - System.out.print(s); - } catch (IOException e) { - throw new RuntimeException(e); - } + 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")); + String s = new String(content.buffer); + System.out.print(s); + } catch (IOException e) { + throw new RuntimeException(e); } + } - public PythonPipProvider() { - super(Ecosystem.Type.PYTHON); - } + public PythonPipProvider() { + super(Ecosystem.Type.PYTHON); + } - @Override - public Content provideStack(Path manifestPath) throws IOException { - PythonControllerBase pythonController = getPythonController(); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), true); - printDependenciesTree(dependencies); - 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); - }); - 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. - sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + @Override + public Content provideStack(Path manifestPath) throws IOException { + PythonControllerBase pythonController = getPythonController(); + List> dependencies = + pythonController.getDependencies(manifestPath.toString(), true); + printDependenciesTree(dependencies); + 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); + }); + 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. + sbom.removeRootComponent(); + return new Content( + sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + } - private void addAllDependencies(PackageURL source, Map component, Sbom sbom) { + private void addAllDependencies(PackageURL source, Map component, Sbom sbom) { - 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 -> { + 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); - }); - // - // } + toPurl((String) component.get("name"), (String) component.get("version")), + dep, + sbom); + }); + // + // } - } + } - @Override - 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"); - Files.deleteIfExists(path); - Path manifestPath = Files.createFile(path); - Files.write(manifestPath, manifestContent); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), false); - printDependenciesTree(dependencies); - Sbom sbom = SbomFactory.newInstance(); - try { - sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); - } - 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. - sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + @Override + 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"); + Files.deleteIfExists(path); + Path manifestPath = Files.createFile(path); + Files.write(manifestPath, manifestContent); + List> dependencies = + pythonController.getDependencies(manifestPath.toString(), false); + printDependenciesTree(dependencies); + Sbom sbom = SbomFactory.newInstance(); + try { + sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); } + 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. + sbom.removeRootComponent(); + 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()); + 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. - 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) { - 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 -> { + // 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) { + 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(); + return new PackageURL((String) purlString).getName(); } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } - }) - .collect(Collectors.toSet()); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(deps); - } + }) + .collect(Collectors.toSet()); + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(deps); } + } - private Set getIgnoredDependencies(String requirementsDeps) { + 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()); + 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; - } + 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]+))?")) { - result = nameVersion.split("=="); - } else { - String dependencyName = PythonControllerBase.getDependencyName(nameVersion); - result = new String[] {dependencyName, "*"}; - } - return result; + 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]+))?")) { + result = nameVersion.split("=="); + } else { + String dependencyName = PythonControllerBase.getDependencyName(nameVersion); + result = new String[] {dependencyName, "*"}; } + return result; + } - private static String extractDepFull(String requirementLine) { - return requirementLine.substring(0, requirementLine.indexOf("#")).trim(); - } + private static String extractDepFull(String requirementLine) { + return requirementLine.substring(0, requirementLine.indexOf("#")).trim(); + } - private PackageURL toPurl(String name, String version) { + private PackageURL toPurl(String name, String version) { - try { - return new PackageURL(Ecosystem.Type.PYTHON.getType(), null, name, version, null, null); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); - } + try { + return new PackageURL(Ecosystem.Type.PYTHON.getType(), null, name, version, null, null); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); } + } - private PythonControllerBase getPythonController() { - String pythonPipBinaries; - String useVirtualPythonEnv; - if (!getStringValueEnvironment("EXHORT_PIP_SHOW", "").trim().equals("") - && !getStringValueEnvironment("EXHORT_PIP_FREEZE", "").trim().equals("")) { - pythonPipBinaries = "python;;pip"; - useVirtualPythonEnv = "false"; - } else { - pythonPipBinaries = getPythonPipBinaries(); - 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")); - PythonControllerBase pythonController; - if (this.pythonController == null) { - if (Boolean.parseBoolean(useVirtualPythonEnv)) { - pythonController = new PythonControllerVirtualEnv(python); - } else { - pythonController = new PythonControllerRealEnv(python, pip); - } - } else { - pythonController = this.pythonController; - } - return pythonController; + private PythonControllerBase getPythonController() { + String pythonPipBinaries; + String useVirtualPythonEnv; + if (!getStringValueEnvironment("EXHORT_PIP_SHOW", "").trim().equals("") + && !getStringValueEnvironment("EXHORT_PIP_FREEZE", "").trim().equals("")) { + pythonPipBinaries = "python;;pip"; + useVirtualPythonEnv = "false"; + } else { + pythonPipBinaries = getPythonPipBinaries(); + useVirtualPythonEnv = + Objects.requireNonNullElseGet( + System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), + () -> + Objects.requireNonNullElse( + System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); } - private static String getPythonPipBinaries() { - var python = Operations.getCustomPathOrElse("python3"); - var pip = Operations.getCustomPathOrElse("pip3"); - try { - Operations.runProcess(python, "--version"); - Operations.runProcess(pip, "--version"); - } catch (Exception e) { - python = Operations.getCustomPathOrElse("python"); - pip = Operations.getCustomPathOrElse("pip"); - Operations.runProcess(python, "--version"); - Operations.runProcess(pip, "--version"); - } - return String.format("%s;;%s", python, pip); + 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")); + PythonControllerBase pythonController; + if (this.pythonController == null) { + if (Boolean.parseBoolean(useVirtualPythonEnv)) { + pythonController = new PythonControllerVirtualEnv(python); + } else { + pythonController = new PythonControllerRealEnv(python, pip); + } + } else { + pythonController = this.pythonController; } + return pythonController; + } - @Override - public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException( - "provideComponent with file system path for Python pip package manager is not supported"); + private static String getPythonPipBinaries() { + var python = Operations.getCustomPathOrElse("python3"); + var pip = Operations.getCustomPathOrElse("pip3"); + try { + Operations.runProcess(python, "--version"); + Operations.runProcess(pip, "--version"); + } catch (Exception e) { + python = Operations.getCustomPathOrElse("python"); + pip = Operations.getCustomPathOrElse("pip"); + Operations.runProcess(python, "--version"); + Operations.runProcess(pip, "--version"); } + return String.format("%s;;%s", python, pip); + } + + @Override + public Content provideComponent(Path manifestPath) throws IOException { + 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 bf1809cc..891b63c5 100644 --- a/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java +++ b/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java @@ -35,257 +35,270 @@ 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 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 BiPredicate belongingCriteriaBinaryAlgorithm; - private Predicate genericComparator(BiPredicate binaryBelongingCriteriaAlgorithm, X container) { - return dep -> binaryBelongingCriteriaAlgorithm.test(container, dep); - } + 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"; - } + 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()); - } + 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()); - } + private BiPredicate getBelongingConditionByPurl() { + 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; - } + 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; + } - 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")) { - return filterIgnoredDepsInsensitive(ignoredDeps); - } else { - return filterIgnoredDepsSensitive(ignoredDeps); - } + @Override + public Sbom filterIgnoredDeps(Collection ignoredDeps) { + String exhortIgnoreMethod = + Objects.requireNonNullElse(getExhortIgnoreMethod(), this.exhortIgnoreMethod); + if (exhortIgnoreMethod.equals("insensitive")) { + return filterIgnoredDepsInsensitive(ignoredDeps); + } else { + return filterIgnoredDepsSensitive(ignoredDeps); } + } - private String getExhortIgnoreMethod() { - boolean result; - return System.getenv("EXHORT_IGNORE_METHOD") != null - ? System.getenv("EXHORT_IGNORE_METHOD").trim().toLowerCase() - : getExhortIgnoreProperty(); - } + private String getExhortIgnoreMethod() { + 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; - } + private String getExhortIgnoreProperty() { + 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; - } + 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; + } - 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) { - try { - return new PackageURL(component.getPurl()); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); - } + private PackageURL componentToPurl(Component component) { + try { + return new PackageURL(component.getPurl()); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); } + } - 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() - .filter(c -> !refsToIgnore.contains(c.getBomRef())) - .collect(Collectors.toList())); - 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() + private Sbom removeIgnoredDepsFromSbom(List refsToIgnore) { + bom.setComponents( + bom.getComponents().stream() + .filter(c -> !refsToIgnore.contains(c.getBomRef())) + .collect(Collectors.toList())); + 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); - } - }); - return this; - } + } + }); + return this; + } - private Sbom filterIgnoredDepsSensitive(Collection ignoredDeps) { + private Sbom filterIgnoredDepsSensitive(Collection ignoredDeps) { - List refsToIgnore = bom.getComponents().stream() - .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm, ignoredDeps) - .test(c)) - .map(Component::getBomRef) - .collect(Collectors.toList()); - return removeIgnoredDepsFromSbom(refsToIgnore); - } + 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)) { - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } else if (belongingCondition.equals(BelongingCondition.PURL)) { - belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); - } + @Override + public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition) { + if (belongingCondition.equals(BelongingCondition.NAME)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + } 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()) { - 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()); + @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()) { + 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()); - result = allDirectDeps.stream() - .filter(dep -> dep.getName().equals(name)) - .count() - > 0; - } - return result; + 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())); - bom.getMetadata().setComponent(null); - } + @Override + 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 3729c8cb..45a49750 100644 --- a/src/main/java/com/redhat/exhort/sbom/Sbom.java +++ b/src/main/java/com/redhat/exhort/sbom/Sbom.java @@ -20,34 +20,34 @@ public interface Sbom { - public Sbom addRoot(PackageURL root); + public Sbom addRoot(PackageURL root); - public PackageURL getRoot(); + public PackageURL getRoot(); - public Sbom filterIgnoredDeps(Collection ignoredDeps); + public Sbom filterIgnoredDeps(Collection ignoredDeps); - public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef); + public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef); - public String getAsJsonString(); + public String getAsJsonString(); - public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition); + public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition); - public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name); + public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name); - void removeRootComponent(); + void removeRootComponent(); - public enum BelongingCondition { - NAME("name"), - PURL("purl"); + public enum BelongingCondition { + NAME("name"), + PURL("purl"); - String belongingCondition; + String belongingCondition; - BelongingCondition(String belongingCondition) { - this.belongingCondition = belongingCondition; - } + BelongingCondition(String belongingCondition) { + this.belongingCondition = belongingCondition; + } - public String getBelongingCondition() { - return 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 2157df54..5d45b771 100644 --- a/src/main/java/com/redhat/exhort/sbom/SbomFactory.java +++ b/src/main/java/com/redhat/exhort/sbom/SbomFactory.java @@ -17,11 +17,12 @@ public class SbomFactory { - public static Sbom newInstance() { - return new CycloneDXSbom(); - } + 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( + 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 ba3b3eaa..e6fb90d1 100644 --- a/src/main/java/com/redhat/exhort/tools/Ecosystem.java +++ b/src/main/java/com/redhat/exhort/tools/Ecosystem.java @@ -23,62 +23,62 @@ import com.redhat.exhort.providers.PythonPipProvider; 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"); + public enum Type { + MAVEN("maven"), + NPM("npm"), + GOLANG("golang"), + PYTHON("pypi"), + GRADLE("gradle"); - String type; + String type; - public String getType() { - return type; - } - - Type(String type) { - this.type = type; - } + public String getType() { + return type; } - private Ecosystem() { - // constructor not required for a utility class + Type(String type) { + this.type = type; } + } - /** - * Utility function for instantiating {@link Provider} implementations. - * - * @param manifestPath the manifest Path - * @return a Manifest record - */ - public static Provider getProvider(final Path manifestPath) { - return Ecosystem.getProvider(manifestPath.getFileName().toString()); - } + private Ecosystem() { + // constructor not required for a utility class + } + + /** + * Utility function for instantiating {@link Provider} implementations. + * + * @param manifestPath the manifest Path + * @return a Manifest record + */ + public static Provider getProvider(final Path manifestPath) { + return Ecosystem.getProvider(manifestPath.getFileName().toString()); + } - /** - * Utility function for instantiating {@link Provider} implementations. - * - * @param manifestType the type (filename + type) of the manifest - * @return a Manifest record - */ - public static Provider getProvider(final String manifestType) { - switch (manifestType) { - case "pom.xml": - return new JavaMavenProvider(); - case "package.json": - return new JavaScriptNpmProvider(); - case "go.mod": - return new GoModulesProvider(); - case "requirements.txt": - return new PythonPipProvider(); - case "build.gradle": - return new GradleProvider(); + /** + * Utility function for instantiating {@link Provider} implementations. + * + * @param manifestType the type (filename + type) of the manifest + * @return a Manifest record + */ + public static Provider getProvider(final String manifestType) { + switch (manifestType) { + case "pom.xml": + return new JavaMavenProvider(); + case "package.json": + return new JavaScriptNpmProvider(); + case "go.mod": + return new GoModulesProvider(); + case "requirements.txt": + return new PythonPipProvider(); + case "build.gradle": + return new GradleProvider(); - default: - throw new IllegalStateException(String.format("Unknown manifest file %s", manifestType)); - } + default: + 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 fa82a6f0..4a4a3d13 100644 --- a/src/main/java/com/redhat/exhort/tools/Operations.java +++ b/src/main/java/com/redhat/exhort/tools/Operations.java @@ -27,199 +27,212 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -/** 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() { - // constructor not required for a utility class + 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. + * + * @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 executableKey = String.format("EXHORT_%s_PATH", target); + return Objects.requireNonNullElseGet( + 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. + * + * @param cmdList list of command parts + */ + public static void runProcess(final String... cmdList) { + runProcess(cmdList, null); + } + + public static void runProcess(final String[] cmdList, final Map envMap) { + var processBuilder = new ProcessBuilder(); + processBuilder.command(cmdList); + if (envMap != null) { + processBuilder.environment().putAll(envMap); } - - /** - * 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. - * - * @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 executableKey = String.format("EXHORT_%s_PATH", target); - return Objects.requireNonNullElseGet( - System.getenv(executableKey), - () -> Objects.requireNonNullElse(System.getProperty(executableKey), defaultExecutable)); + // create a process builder or throw a runtime exception + Process process = null; + try { + process = processBuilder.start(); + } catch (final IOException e) { + throw new RuntimeException( + String.format( + "failed to build process for '%s' got %s", join(" ", cmdList), e.getMessage())); } - /** - * 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 - */ - public static void runProcess(final String... cmdList) { - runProcess(cmdList, null); - } + // execute the command or throw runtime exception if failed + int exitCode = 0; + try { + exitCode = process.waitFor(); - public static void runProcess(final String[] cmdList, final Map envMap) { - var processBuilder = new ProcessBuilder(); - processBuilder.command(cmdList); - if (envMap != null) { - processBuilder.environment().putAll(envMap); + } catch (final InterruptedException e) { + throw new RuntimeException( + 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())); + if (errMsg.isEmpty()) { + 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)); + } 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())); + } + } + } + + public static String runProcessGetOutput(Path dir, final String... cmdList) { + return runProcessGetOutput(dir, cmdList, null); + } + + public static String runProcessGetOutput(Path dir, final String[] cmdList, String[] envList) { + StringBuilder sb = new StringBuilder(); + try { + Process process; + InputStream inputStream; + if (dir == null) { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList)); } - // create a process builder or throw a runtime exception - Process process = null; - try { - process = processBuilder.start(); - } catch (final IOException e) { - throw new RuntimeException( - String.format("failed to build process for '%s' got %s", join(" ", cmdList), e.getMessage())); + } else { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); } + } - // execute the command or throw runtime exception if failed - int exitCode = 0; - try { - exitCode = process.waitFor(); + inputStream = process.getInputStream(); - } catch (final InterruptedException e) { - throw new RuntimeException( - String.format("built process for '%s' interrupted, got %s", join(" ", cmdList), e.getMessage())); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + if (!line.endsWith(System.lineSeparator())) { + sb.append("\n"); } - // 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())); - if (errMsg.isEmpty()) { - 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)); - } 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())); - } + } + if (sb.toString().trim().equals("")) { + inputStream = process.getErrorStream(); + reader = new BufferedReader(new InputStreamReader(inputStream)); + while ((line = reader.readLine()) != null) { + sb.append(line); + if (!line.endsWith(System.lineSeparator())) { + sb.append("\n"); + } } + } + } catch (IOException e) { + throw new RuntimeException( + String.format("Failed to execute command '%s' ", join(" ", cmdList)), e); } - - public static String runProcessGetOutput(Path dir, final String... cmdList) { - return runProcessGetOutput(dir, cmdList, null); - } - - public static String runProcessGetOutput(Path dir, final String[] cmdList, String[] envList) { - StringBuilder sb = new StringBuilder(); - try { - Process process; - InputStream inputStream; - if (dir == null) { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList)); - } - } else { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); - } - } - - inputStream = process.getInputStream(); - - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - if (!line.endsWith(System.lineSeparator())) { - sb.append("\n"); - } - } - if (sb.toString().trim().equals("")) { - inputStream = process.getErrorStream(); - reader = new BufferedReader(new InputStreamReader(inputStream)); - while ((line = reader.readLine()) != null) { - sb.append(line); - if (!line.endsWith(System.lineSeparator())) { - sb.append("\n"); - } - } - } - } catch (IOException 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) { + try { + Process process; + if (dir == null) { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList)); } - return sb.toString(); - } - - public static ProcessExecOutput runProcessGetFullOutput(Path dir, final String[] cmdList, String[] envList) { - try { - Process process; - if (dir == null) { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList)); - } - } else { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); - } - } - - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder output = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - output.append(line); - if (!line.endsWith(System.lineSeparator())) { - output.append("\n"); - } - } - - reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - StringBuilder error = new StringBuilder(); - while ((line = reader.readLine()) != null) { - error.append(line); - if (!line.endsWith(System.lineSeparator())) { - error.append("\n"); - } - } - - process.waitFor(30L, TimeUnit.SECONDS); - - 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); + } else { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); + } + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line); + if (!line.endsWith(System.lineSeparator())) { + output.append("\n"); } + } + + reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + StringBuilder error = new StringBuilder(); + while ((line = reader.readLine()) != null) { + error.append(line); + if (!line.endsWith(System.lineSeparator())) { + error.append("\n"); + } + } + + process.waitFor(30L, TimeUnit.SECONDS); + + 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); } + } - public static class ProcessExecOutput { - private String output; - private String error; - private int exitCode; + public static class ProcessExecOutput { + private String output; + private String error; + private int exitCode; - public ProcessExecOutput(String output, String error, int exitCode) { - this.output = output; - this.error = error; - this.exitCode = exitCode; - } + public ProcessExecOutput(String output, String error, int exitCode) { + this.output = output; + this.error = error; + this.exitCode = exitCode; + } - public String getOutput() { - return output; - } + public String getOutput() { + return output; + } - public String getError() { - return error; - } + public String getError() { + return error; + } - public int getExitCode() { - return exitCode; - } + public int getExitCode() { + return exitCode; } + } } 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 5166212d..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 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 971fe9a0..4dd1834d 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java @@ -33,415 +33,470 @@ import java.util.stream.Stream; public abstract class PythonControllerBase { - public static void main(String[] args) { - - PythonControllerBase pythonController; - // pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); - LocalDateTime start = LocalDateTime.now(); - List> dependencies; - // 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"); - 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); - } catch (PackageNotInstalledException e) { - System.out.println(e.getMessage()); - dependencies = null; - } - end = 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)); - - ObjectMapper om = new ObjectMapper(); - try { - String json = om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); - System.out.println(json); - // System.out.println(pythonController.counter); - - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + public static void main(String[] args) { + + PythonControllerBase pythonController; + // pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); + LocalDateTime start = LocalDateTime.now(); + List> dependencies; + // 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"); + 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); + } catch (PackageNotInstalledException e) { + System.out.println(e.getMessage()); + dependencies = null; + } + end = 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)); + + ObjectMapper om = new ObjectMapper(); + try { + String json = om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); + System.out.println(json); + // 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; + + protected String pathToPythonBin; + + protected String pipBinaryLocation; + + // public int counter =0; + + public abstract void prepareEnvironment(String pathToPythonBin); + + public abstract boolean automaticallyInstallPackageOnEnvironment(); + + public abstract boolean isRealEnv(); + + 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()) { + 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 { + installingRequirementsOneByOne(pathToRequirements); } + } + // + else { + installPackages(pathToRequirements); + } } - - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - protected Path pythonEnvironmentDir; - protected Path pipBinaryDir; - - protected String pathToPythonBin; - - protected String pipBinaryLocation; - - // public int counter =0; - - public abstract void prepareEnvironment(String pathToPythonBin); - - public abstract boolean automaticallyInstallPackageOnEnvironment(); - - public abstract boolean isRealEnv(); - - void installPackages(String pathToRequirements) { - Operations.runProcess(pipBinaryLocation, "install", "-r", pathToRequirements); - Operations.runProcess(pipBinaryLocation, "freeze"); + List> dependencies = + getDependenciesImpl(pathToRequirements, includeTransitive); + if (isVirtualEnv()) { + cleanEnvironment(false); } - 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()) { - 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 { - installingRequirementsOneByOne(pathToRequirements); + return dependencies; + } + + 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())); } - } - // - else { - installPackages(pathToRequirements); - } - } - List> dependencies = getDependenciesImpl(pathToRequirements, includeTransitive); - if (isVirtualEnv()) { - cleanEnvironment(false); - } + }); - return dependencies; + } 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()); } - - 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())); - } - }); - - } 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()); - } + } + + 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); + log.info(freezeMessage); } - - 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); - log.info(freezeMessage); - } - String[] deps = freeze.split(System.lineSeparator()); - 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); - log.info(pipShowMessage); - } - List allPipShowLines = splitPipShowLines(pipShowOutput); - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - Map CachedTree = new HashMap<>(); - List linesOfRequirements; - try { - - 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); - }); - ObjectMapper om = new ObjectMapper(); - String tree; - try { - tree = om.writerWithDefaultPrettyPrinter().writeValueAsString(CachedTree); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - linesOfRequirements.stream().forEach(dep -> { - if (matchManifestVersions) { + String[] deps = freeze.split(System.lineSeparator()); + 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); + log.info(pipShowMessage); + } + List allPipShowLines = splitPipShowLines(pipShowOutput); + boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); + Map CachedTree = new HashMap<>(); + List linesOfRequirements; + try { + + 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); + }); + ObjectMapper om = new ObjectMapper(); + String tree; + try { + tree = om.writerWithDefaultPrettyPrinter().writeValueAsString(CachedTree); + } 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)); - } + 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); - }); - - return dependencies; - } - - private String getPipShowFromEnvironment(String 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"); - } + } + List path = new ArrayList<>(); + String depName = getDependencyName(dep.toLowerCase()); + path.add(depName); + bringAllDependencies(dependencies, depName, CachedTree, includeTransitive, path); + }); - private String getPipCommandInvokedOrDecodedFromEnvironment(String EnvVar, String... cmdList) { - return getStringValueEnvironment(EnvVar, "").trim().equals("") - ? Operations.runProcessGetOutput(pythonEnvironmentDir, cmdList) - : new String(Base64.getDecoder().decode(getStringValueEnvironment(EnvVar, ""))); + return dependencies; + } + + private String getPipShowFromEnvironment(String 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"); + } + + private String getPipCommandInvokedOrDecodedFromEnvironment(String EnvVar, String... cmdList) { + 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) { + + 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)); } - - private void bringAllDependencies( - List> dependencies, - String depName, - Map cachedTree, - boolean includeTransitive, - List path) { - - 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)); - } - 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<>(); - directDeps.stream().forEach(dep -> { - if (!path.contains(dep.toLowerCase())) { + 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<>(); + 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())); + 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()); + } + 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); }); - entry.put("dependencies", targetDeps); - }); + } + + protected List getDepsList(String pipShowOutput) { + int requiresKeyIndex = pipShowOutput.indexOf("Requires:"); + String requiresToken = pipShowOutput.substring(requiresKeyIndex + 9); + int endOfLine = requiresToken.indexOf(System.lineSeparator()); + String listOfDeps; + if (endOfLine > -1) { + listOfDeps = requiresToken.substring(0, endOfLine).trim(); + } else { + listOfDeps = requiresToken; } - - protected List getDepsList(String pipShowOutput) { - int requiresKeyIndex = pipShowOutput.indexOf("Requires:"); - String requiresToken = pipShowOutput.substring(requiresKeyIndex + 9); - int endOfLine = requiresToken.indexOf(System.lineSeparator()); - String listOfDeps; - if (endOfLine > -1) { - listOfDeps = requiresToken.substring(0, endOfLine).trim(); - } else { - listOfDeps = requiresToken; - } - 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(); - } - - 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(); - } - - public static String getDependencyName(String dep) { - int rightTriangleBracket = dep.indexOf(">"); - int leftTriangleBracket = dep.indexOf("<"); - int equalsSign = dep.indexOf("="); - int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); - String depName; - if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { - depName = dep; - } 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); - } - - static List splitPipShowLines(String pipShowOutput) { - return Arrays.stream(pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())) - .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(); + } + + 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(); + } + + public static String getDependencyName(String dep) { + int rightTriangleBracket = dep.indexOf(">"); + int leftTriangleBracket = dep.indexOf("<"); + int equalsSign = dep.indexOf("="); + int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); + String depName; + if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { + depName = dep; + } 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); + } + + static List splitPipShowLines(String pipShowOutput) { + 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 9cc8db44..ff693425 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java @@ -19,48 +19,48 @@ import java.nio.file.Path; public class PythonControllerRealEnv extends PythonControllerBase { - public PythonControllerRealEnv(String pathToPythonBin, String pathToPip) { - Path pipPath = Path.of(pathToPip); - this.pipBinaryDir = pipPath.getParent(); - if (this.pipBinaryDir == null) { - this.pipBinaryDir = pipPath; - } - this.pythonEnvironmentDir = Path.of(System.getProperty("user.dir")); - this.pathToPythonBin = pathToPythonBin; + public PythonControllerRealEnv(String pathToPythonBin, String pathToPip) { + Path pipPath = Path.of(pathToPip); + this.pipBinaryDir = pipPath.getParent(); + if (this.pipBinaryDir == null) { + this.pipBinaryDir = pipPath; } + this.pythonEnvironmentDir = Path.of(System.getProperty("user.dir")); + this.pathToPythonBin = pathToPythonBin; + } - @Override - public void prepareEnvironment(String pathToPythonBin) { - String envBinDir = pipBinaryDir.toString(); - 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 { - this.pipBinaryLocation = envBinDir; - } + @Override + public void prepareEnvironment(String pathToPythonBin) { + String envBinDir = pipBinaryDir.toString(); + 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 { + this.pipBinaryLocation = envBinDir; } + } - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return false; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return false; + } - @Override - public boolean isRealEnv() { - return true; - } + @Override + public boolean isRealEnv() { + return true; + } - @Override - public boolean isVirtualEnv() { - return false; - } + @Override + public boolean isVirtualEnv() { + return false; + } - @Override - public void cleanEnvironment(boolean cleanEnvironment) { + @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 de2db76a..740e4506 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java @@ -19,26 +19,29 @@ 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); - } + // private System.Logger log = System.getLogger("name"); + public PythonControllerTestEnv(String pathToPythonBin, String pathToPip) { + super(pathToPythonBin, pathToPip); + } - @Override - 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); - } + @Override + 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); + } - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return true; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return true; + } - @Override - public boolean isVirtualEnv() { - return false; - } + @Override + 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 f60176ef..7c8db244 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java @@ -24,76 +24,82 @@ public class PythonControllerVirtualEnv extends PythonControllerBase { - // 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.pathToPythonBin = pathToPythonBin; - } + // 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.pathToPythonBin = pathToPythonBin; + } - @Override - public void prepareEnvironment(String pathToPythonBin) { - try { - 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 envBinDir = pipBinaryDir.toString(); - if (pathToPythonBin.contains("python3")) { - this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); - } else { - this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); - } + @Override + public void prepareEnvironment(String pathToPythonBin) { + try { + if (!Files.exists(pythonEnvironmentDir)) { + Files.createDirectory(pythonEnvironmentDir); + } + } catch (IOException e) { + throw new RuntimeException(e); } - - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return true; + 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(); } + } - @Override - public boolean isRealEnv() { - return false; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return true; + } - @Override - public boolean isVirtualEnv() { - return true; - } + @Override + public boolean isRealEnv() { + return false; + } + + @Override + public boolean isVirtualEnv() { + return true; + } - @Override - 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); - } - }); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - Path envRequirements = Path.of(pythonEnvironmentDir.toString(), "requirements.txt"); - try { - Files.deleteIfExists(envRequirements); - String freezeOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); - Files.createFile(envRequirements); - Files.write(envRequirements, freezeOutput.getBytes()); - Operations.runProcessGetOutput( - pythonEnvironmentDir, pipBinaryLocation, "uninstall", "-y", "-r", "requirements.txt"); + @Override + 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); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + Path envRequirements = Path.of(pythonEnvironmentDir.toString(), "requirements.txt"); + try { + Files.deleteIfExists(envRequirements); + String freezeOutput = + Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); + Files.createFile(envRequirements); + Files.write(envRequirements, freezeOutput.getBytes()); + Operations.runProcessGetOutput( + pythonEnvironmentDir, pipBinaryLocation, "uninstall", "-y", "-r", "requirements.txt"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + } catch (IOException e) { + throw new RuntimeException(e); + } } + } } diff --git a/src/main/java/com/redhat/exhort/utils/StringInsensitive.java b/src/main/java/com/redhat/exhort/utils/StringInsensitive.java index 5b7c1195..4eb092fb 100644 --- a/src/main/java/com/redhat/exhort/utils/StringInsensitive.java +++ b/src/main/java/com/redhat/exhort/utils/StringInsensitive.java @@ -19,30 +19,30 @@ public class StringInsensitive { - public StringInsensitive(String value) { - this.value = value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StringInsensitive that = (StringInsensitive) o; - return Objects.equals(value.toLowerCase(), that.value.toLowerCase()); - } - - @Override - public int hashCode() { - return Objects.hash(value.toLowerCase()); - } - - private String value; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } + public StringInsensitive(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StringInsensitive that = (StringInsensitive) o; + return Objects.equals(value.toLowerCase(), that.value.toLowerCase()); + } + + @Override + public int hashCode() { + return Objects.hash(value.toLowerCase()); + } + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java index eac9ed2e..867c0130 100644 --- a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java +++ b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java @@ -24,117 +24,129 @@ public class GitVersionControlSystemImpl implements VersionControlSystem { - private String gitBinary; - - 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") + private String gitBinary; + + 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)) { + 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); + tagInfo.setCommitTimestamp(commitTimestamp); + + // go get last annotated tag + 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(); - 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); - tagInfo.setCommitTimestamp(commitTimestamp); - - // go get last annotated tag - 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(); - fetchLatestTag(tagInfo, resultFromInvocation); - } else { - if (resultFromInvocation.startsWith("fatal: No names found")) { - tagInfo.setCurrentCommitPointedByTag(false); - tagInfo.setTagName(""); - } - // fetch last annotated tag - else { - fetchLatestTag(tagInfo, resultFromInvocation); - } - } + fetchLatestTag(tagInfo, resultFromInvocation); + } else { + if (resultFromInvocation.startsWith("fatal: No names found")) { + tagInfo.setCurrentCommitPointedByTag(false); + tagInfo.setTagName(""); } - - // empty git repo with no commits + // fetch last annotated tag else { - tagInfo.setTagName(""); - tagInfo.setCurrentCommitPointedByTag(false); - tagInfo.setCommitTimestamp( - LocalDateTime.parse(LocalDateTime.MIN.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - tagInfo.setCurrentCommitDigest(""); + fetchLatestTag(tagInfo, resultFromInvocation); } - return tagInfo; + } } - @Override - public boolean isDirectoryRepo(Path repoLocation) { - - String resultFromInvocation = - Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "--is-inside-work-tree"); - return resultFromInvocation.trim().equals("true"); + // empty git repo with no commits + else { + tagInfo.setTagName(""); + tagInfo.setCurrentCommitPointedByTag(false); + tagInfo.setCommitTimestamp( + LocalDateTime.parse(LocalDateTime.MIN.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + tagInfo.setCurrentCommitDigest(""); } - - @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; + return tagInfo; + } + + @Override + public boolean isDirectoryRepo(Path repoLocation) { + + 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()); + } } - - 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); + 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); + } + + private static void fetchLatestTag(TagInfo tagInfo, String resultFromInvocation) { + String[] parts = resultFromInvocation.split("-"); + if (parts.length > 1) { + analyzeGitDescribeResult(tagInfo, parts); + + } else { + tagInfo.setCurrentCommitPointedByTag(true); + tagInfo.setTagName(parts[0]); } - - private static void fetchLatestTag(TagInfo tagInfo, String resultFromInvocation) { - String[] parts = resultFromInvocation.split("-"); - if (parts.length > 1) { - analyzeGitDescribeResult(tagInfo, parts); - - } 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])) { - String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-", tagNameParts)); - tagInfo.setCurrentCommitDigest(parts[parts.length - 1].replace("g", "")); - tagInfo.setCurrentCommitPointedByTag(false); - } else { - String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-", tagNameParts)); - tagInfo.setCurrentCommitPointedByTag(true); - } + } + + 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])) { + String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); + tagInfo.setTagName(String.join("-", tagNameParts)); + tagInfo.setCurrentCommitDigest(parts[parts.length - 1].replace("g", "")); + tagInfo.setCurrentCommitPointedByTag(false); + } else { + String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); + tagInfo.setTagName(String.join("-", tagNameParts)); + tagInfo.setCurrentCommitPointedByTag(true); } + } } diff --git a/src/main/java/com/redhat/exhort/vcs/TagInfo.java b/src/main/java/com/redhat/exhort/vcs/TagInfo.java index e1bb4f2e..41ace28f 100644 --- a/src/main/java/com/redhat/exhort/vcs/TagInfo.java +++ b/src/main/java/com/redhat/exhort/vcs/TagInfo.java @@ -19,41 +19,41 @@ public class TagInfo { - private String tagName; - private boolean currentCommitPointedByTag; - private String currentCommitDigest; + private String tagName; + private boolean currentCommitPointedByTag; + private String currentCommitDigest; - public LocalDateTime getCommitTimestamp() { - return commitTimestamp; - } + public LocalDateTime getCommitTimestamp() { + return commitTimestamp; + } - public void setCommitTimestamp(LocalDateTime commitTimestamp) { - this.commitTimestamp = commitTimestamp; - } + public void setCommitTimestamp(LocalDateTime commitTimestamp) { + this.commitTimestamp = commitTimestamp; + } - private LocalDateTime commitTimestamp; + private LocalDateTime commitTimestamp; - public String getTagName() { - return tagName; - } + public String getTagName() { + return tagName; + } - public void setTagName(String tagName) { - this.tagName = tagName; - } + public void setTagName(String tagName) { + this.tagName = tagName; + } - public boolean isCurrentCommitPointedByTag() { - return currentCommitPointedByTag; - } + public boolean isCurrentCommitPointedByTag() { + return currentCommitPointedByTag; + } - public void setCurrentCommitPointedByTag(boolean currentCommitPointedByTag) { - this.currentCommitPointedByTag = currentCommitPointedByTag; - } + public void setCurrentCommitPointedByTag(boolean currentCommitPointedByTag) { + this.currentCommitPointedByTag = currentCommitPointedByTag; + } - public String getCurrentCommitDigest() { - return currentCommitDigest; - } + public String getCurrentCommitDigest() { + return currentCommitDigest; + } - public void setCurrentCommitDigest(String currentCommitDigest) { - this.currentCommitDigest = currentCommitDigest; - } + public void setCurrentCommitDigest(String currentCommitDigest) { + this.currentCommitDigest = currentCommitDigest; + } } diff --git a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java index 03e48e3f..1a09850d 100644 --- a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java +++ b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java @@ -18,33 +18,31 @@ import java.nio.file.Path; 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 - * @param repoLocation - the repo directory path with inner .git directory - * @return {@link TagInfo} containing the latest tag - */ - TagInfo getLatestTag(Path repoLocation); + /** + * 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 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 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); + /** + * @param tagInfo - object that contains the tag info and current commit data + * @param newTagVersion + * @return + */ + String getPseudoVersion(TagInfo tagInfo, String newTagVersion); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 3e690a0e..91a436a9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,37 +1,37 @@ module com.redhat.exhort { - requires java.net.http; - requires com.fasterxml.jackson.annotation; - requires com.fasterxml.jackson.core; - requires transitive com.fasterxml.jackson.databind; - requires jakarta.annotation; - requires java.xml; - requires jakarta.mail; - requires cyclonedx.core.java; - requires transitive packageurl.java; - requires org.tomlj; + requires java.net.http; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires transitive com.fasterxml.jackson.databind; + requires jakarta.annotation; + requires java.xml; + requires jakarta.mail; + requires cyclonedx.core.java; + 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; - exports com.redhat.exhort.api.serialization; - exports com.redhat.exhort.impl; - exports com.redhat.exhort.sbom; - exports com.redhat.exhort.tools; + exports com.redhat.exhort; + exports com.redhat.exhort.api; + exports com.redhat.exhort.api.serialization; + exports com.redhat.exhort.impl; + 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; + 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; + 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 c7d08dd7..dca86b7c 100644 --- a/src/test/java/com/redhat/exhort/ExhortTest.java +++ b/src/test/java/com/redhat/exhort/ExhortTest.java @@ -23,61 +23,62 @@ public class ExhortTest { - protected String getStringFromFile(String... list) { - byte[] bytes = new byte[0]; - try { - InputStream resourceAsStream = getResourceAsStreamDecision(this.getClass(), list); - bytes = resourceAsStream.readAllBytes(); - resourceAsStream.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return new String(bytes); + protected String getStringFromFile(String... list) { + byte[] bytes = new byte[0]; + try { + InputStream resourceAsStream = getResourceAsStreamDecision(this.getClass(), list); + bytes = resourceAsStream.readAllBytes(); + resourceAsStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); } - 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)); - } - return resourceAsStreamFromModule; + return new String(bytes); + } + + 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)); } + return resourceAsStreamFromModule; + } - protected String getFileFromResource(String fileName, String... pathList) { - Path tmpFile; - try { - var tmpDir = Files.createTempDirectory("exhort_test_"); - tmpFile = Files.createFile(tmpDir.resolve(fileName)); - try (var is = getResourceAsStreamDecision(this.getClass(), pathList)) { - if (Objects.nonNull(is)) { - Files.write(tmpFile, is.readAllBytes()); - } else { - InputStream resourceIs = - getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); - Files.write(tmpFile, resourceIs.readAllBytes()); - resourceIs.close(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } catch (IOException e) { - throw new RuntimeException(e); + protected String getFileFromResource(String fileName, String... pathList) { + Path tmpFile; + try { + var tmpDir = Files.createTempDirectory("exhort_test_"); + tmpFile = Files.createFile(tmpDir.resolve(fileName)); + try (var is = getResourceAsStreamDecision(this.getClass(), pathList)) { + if (Objects.nonNull(is)) { + Files.write(tmpFile, is.readAllBytes()); + } else { + InputStream resourceIs = + getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); + Files.write(tmpFile, resourceIs.readAllBytes()); + resourceIs.close(); } - return tmpFile.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (IOException e) { + throw new RuntimeException(e); } + return tmpFile.toString(); + } - 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()); + 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()); - } catch (IOException e) { - throw new RuntimeException(e); - } - return tmpFile.toString(); + } 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 2ee3d06f..ba670fa6 100644 --- a/src/test/java/com/redhat/exhort/image/ImageRefTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageRefTest.java @@ -36,62 +36,77 @@ class ImageRefTest extends ExhortTest { - @Test - void test_imageRef() throws MalformedPackageURLException { - 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(purl, imageRef.getPackageURL()); - - var imageRefPurl = new ImageRef(purl); - assertEquals(imageRef, imageRefPurl); - assertTrue(imageRef.equals(imageRefPurl)); - assertEquals(imageRef.hashCode(), imageRefPurl.hashCode()); - } - - @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 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.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.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"); - - assertEquals(expectedImageRef, imageRef); - } + @Test + void test_imageRef() throws MalformedPackageURLException { + 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(purl, imageRef.getPackageURL()); + + var imageRefPurl = new ImageRef(purl); + assertEquals(imageRef, imageRefPurl); + assertTrue(imageRef.equals(imageRefPurl)); + assertEquals(imageRef.hashCode(), imageRefPurl.hashCode()); + } + + @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 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.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.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"); + + 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 bbe5a312..c4733feb 100644 --- a/src/test/java/com/redhat/exhort/image/ImageTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageTest.java @@ -26,468 +26,503 @@ 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")); - } + 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")); + } - @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) { + @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) { - var image = givenTag == null ? new Image(fullName) : new Image(fullName, givenTag); + var image = givenTag == null ? new Image(fullName) : new Image(fullName, givenTag); - assertEquals(expectedRegistry, image.getRegistry()); - assertEquals(expectedRepository, image.getRepository()); - assertEquals(expectedTag, image.getTag()); - assertEquals(expectedDigest, image.getDigest()); - assertEquals(expectedString, image.toString()); - assertEquals(expectedHasRegistry, image.hasRegistry()); - assertEquals(expectedNameWithoutTag, image.getNameWithoutTag()); - assertEquals(expectedNameWithoutTagOptionalRegistry, image.getNameWithoutTag(optionalRegistry)); - assertEquals(expectedFullName, image.getFullName()); - assertEquals(expectedFullNameOptionalRegistry, image.getFullName(optionalRegistry)); - assertEquals(expectedUser, image.getUser()); - assertEquals(expectedSimpleName, image.getSimpleName()); - assertEquals(expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); - assertEquals(expectedFullName, image.getNameWithOptionalRepository(null)); + assertEquals(expectedRegistry, image.getRegistry()); + assertEquals(expectedRepository, image.getRepository()); + assertEquals(expectedTag, image.getTag()); + assertEquals(expectedDigest, image.getDigest()); + assertEquals(expectedString, image.toString()); + assertEquals(expectedHasRegistry, image.hasRegistry()); + assertEquals(expectedNameWithoutTag, image.getNameWithoutTag()); + assertEquals(expectedNameWithoutTagOptionalRegistry, image.getNameWithoutTag(optionalRegistry)); + assertEquals(expectedFullName, image.getFullName()); + assertEquals(expectedFullNameOptionalRegistry, image.getFullName(optionalRegistry)); + assertEquals(expectedUser, image.getUser()); + assertEquals(expectedSimpleName, image.getSimpleName()); + assertEquals( + expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); + assertEquals(expectedFullName, image.getNameWithOptionalRepository(null)); - image.setDigest(optionalDigest); - assertEquals(expectedFullNameOptionalDigest, image.getFullName()); - } + image.setDigest(optionalDigest); + assertEquals(expectedFullNameOptionalDigest, image.getFullName()); + } - @Test - void test_equals() { - var image1 = new Image("test-image"); - var image2 = new Image("test-image:latest"); - var image3 = new Image("test-image:old"); + @Test + void test_equals() { + var image1 = new Image("test-image"); + var image2 = new Image("test-image:latest"); + var image3 = new Image("test-image:old"); - assertTrue(image1.equals(image2)); - assertFalse(image2.equals(image3)); - } + assertTrue(image1.equals(image2)); + assertFalse(image2.equals(image3)); + } - @Test - void test_hashCode() { - var image1 = new Image("test-image"); - var image2 = new Image("test-image:latest"); - var image3 = new Image("test-image:old"); + @Test + void test_hashCode() { + var image1 = new Image("test-image"); + var image2 = new Image("test-image:latest"); + var image3 = new Image("test-image:old"); - assertEquals(image1.hashCode(), image2.hashCode()); - assertNotEquals(image2.hashCode(), image3.hashCode()); - } + assertEquals(image1.hashCode(), image2.hashCode()); + assertNotEquals(image2.hashCode(), image3.hashCode()); + } - @Test - void test_image_null() { - var expectedMessage = "Image name must not be null"; + @Test + void test_image_null() { + var expectedMessage = "Image name must not be null"; - var exception1 = assertThrows(NullPointerException.class, () -> { - new Image(null); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = + assertThrows( + NullPointerException.class, + () -> { + new Image(null); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(NullPointerException.class, () -> { - new Image(null, "test"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = + assertThrows( + NullPointerException.class, + () -> { + new Image(null, "test"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(NullPointerException.class, () -> { - Image.validate(null); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + var exception3 = + assertThrows( + NullPointerException.class, + () -> { + Image.validate(null); + }); + assertEquals(expectedMessage, exception3.getMessage()); + } - @Test - void test_image_invalid() { - var imageName = ""; - var expectedMessage = imageName + " is not a proper image name ([registry/][repo][:port]"; + @Test + 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); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "test"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName, "test"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + 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"; + @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 exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "&^*&"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = + assertThrows( + IllegalArgumentException.class, + () -> { + new Image(imageName, "&^*&"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + 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 2788d0df..708a8cdf 100644 --- a/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java @@ -62,895 +62,973 @@ class ImageUtilsTest extends ExhortTest { - 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"; - static final String mockSyftConfig = "test-path/syft-config"; - static final String mockSyftSource = "registry"; - static final String mockSkopeoPath = "test-path/skopeo"; - static final String mockSkopeoConfig = "test-path/skopeo-config"; - static final String mockSkopeoDaemon = "test-path/daemon-host"; - static final String mockDockerPath = "test-path/docker"; - static final String mockPodmanPath = "test-path/podman"; - static final String mockPath = "test-path"; - static final String mockOs = "linux"; - static final String mockArch = "arm"; - static final String mockVariant = "v7"; - - 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", ""), "")); + 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"; + static final String mockSyftConfig = "test-path/syft-config"; + static final String mockSyftSource = "registry"; + static final String mockSkopeoPath = "test-path/skopeo"; + static final String mockSkopeoConfig = "test-path/skopeo-config"; + static final String mockSkopeoDaemon = "test-path/daemon-host"; + static final String mockDockerPath = "test-path/docker"; + static final String mockPodmanPath = "test-path/podman"; + static final String mockPath = "test-path"; + static final String mockOs = "linux"; + static final String mockArch = "arm"; + static final String mockVariant = "v7"; + + 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", ""), "")); + } + + 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", ""), "")); + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") + @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 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); + + 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())); + + assertEquals(node, sbom); } - - 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", ""), "")); - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") - @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 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); - - 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())); - - assertEquals(node, sbom); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @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 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")); - 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); - - var digests = ImageUtils.getImageDigests(mockImageRef); - - var expectedDigests = Collections.singletonMap( - Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); - - assertEquals(expectedDigests, digests); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @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 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); - - 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"); - - assertEquals(expectedDigests, digests); - } - } - - @Test - @SetEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM, value = mockImagePlatform) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) - void test_get_image_platform() { - var platform = ImageUtils.getImagePlatform(); - assertEquals(new Platform(mockImagePlatform), platform); - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) - void test_get_image_platform_no_default() { - var platform = ImageUtils.getImagePlatform(); - assertEquals(new Platform(mockOs, mockArch, mockVariant), platform); - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @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.runProcessGetFullOutput( - isNull(), aryEq(new String[] {"podman", "info"}), isNull())) - .thenReturn(new Operations.ProcessExecOutput("", "", 0)); - - var platform = ImageUtils.getImagePlatform(); - assertNull(platform); - } - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") - @ClearEnvironmentVariable(key = EXHORT_IMAGE_OS) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_ARCH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_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.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); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @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 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")); + 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); + + var digests = ImageUtils.getImageDigests(mockImageRef); + + var expectedDigests = + Collections.singletonMap( + Platform.EMPTY_PLATFORM, + "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + + assertEquals(expectedDigests, digests); } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @SetEnvironmentVariable(key = "EXHORT_SYFT_PATH", value = mockSyftPath) - @SetEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH, value = mockSyftConfig) - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - 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); - - assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @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 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); + + 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"); + + assertEquals(expectedDigests, digests); } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) - 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); - - assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); - } + } + + @Test + @SetEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM, value = mockImagePlatform) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) + void test_get_image_platform() { + var platform = ImageUtils.getImagePlatform(); + assertEquals(new Platform(mockImagePlatform), platform); + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) + void test_get_image_platform_no_default() { + var platform = ImageUtils.getImagePlatform(); + assertEquals(new Platform(mockOs, mockArch, mockVariant), platform); + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @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.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"podman", "info"}), isNull())) + .thenReturn(new Operations.ProcessExecOutput("", "", 0)); + + var platform = ImageUtils.getImagePlatform(); + assertNull(platform); } - - @Test - @ClearEnvironmentVariable(key = "PATH") - void test_get_syft_envs() { - var envs1 = ImageUtils.getSyftEnvs("", ""); - assertTrue(envs1.isEmpty()); - - var envs2 = ImageUtils.getSyftEnvs("test-docker-path", ""); - var expected_envs2 = new ArrayList<>(); - expected_envs2.add("PATH=test-docker-path"); - assertEquals(expected_envs2, envs2); - - var envs3 = ImageUtils.getSyftEnvs("", "test-podman-path"); - var expected_envs3 = new ArrayList<>(); - expected_envs3.add("PATH=test-podman-path"); - assertEquals(expected_envs3, envs3); - - var envs4 = ImageUtils.getSyftEnvs("test-docker-path", "test-podman-path"); - var expected_envs4 = new ArrayList<>(); - expected_envs4.add("PATH=test-docker-path" + File.pathSeparator + "test-podman-path"); - assertEquals(expected_envs4, envs4); + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") + @ClearEnvironmentVariable(key = EXHORT_IMAGE_OS) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_ARCH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_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.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); } - - @Test - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_update_PATH_env() { - var path = ImageUtils.updatePATHEnv("test-exec-path"); - assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path); + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @SetEnvironmentVariable(key = "EXHORT_SYFT_PATH", value = mockSyftPath) + @SetEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH, value = mockSyftConfig) + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + 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); + + assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); } - - @Test - @ClearEnvironmentVariable(key = "PATH") - void test_update_PATH_env_no_PATH() { - var path = ImageUtils.updatePATHEnv("test-exec-path"); - assertEquals("PATH=test-exec-path", path); + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) + 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); + + assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); } - - @Test - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - 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.runProcessGetFullOutput( - isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) - .thenReturn(output); - - assertThat(ImageUtils.hostInfo("docker", "info")).isEqualTo("test-output"); - } + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + void test_get_syft_envs() { + var envs1 = ImageUtils.getSyftEnvs("", ""); + assertTrue(envs1.isEmpty()); + + var envs2 = ImageUtils.getSyftEnvs("test-docker-path", ""); + var expected_envs2 = new ArrayList<>(); + expected_envs2.add("PATH=test-docker-path"); + assertEquals(expected_envs2, envs2); + + var envs3 = ImageUtils.getSyftEnvs("", "test-podman-path"); + var expected_envs3 = new ArrayList<>(); + expected_envs3.add("PATH=test-podman-path"); + assertEquals(expected_envs3, envs3); + + var envs4 = ImageUtils.getSyftEnvs("test-docker-path", "test-podman-path"); + var expected_envs4 = new ArrayList<>(); + expected_envs4.add("PATH=test-docker-path" + File.pathSeparator + "test-podman-path"); + assertEquals(expected_envs4, envs4); + } + + @Test + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_update_PATH_env() { + var path = ImageUtils.updatePATHEnv("test-exec-path"); + assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path); + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + void test_update_PATH_env_no_PATH() { + var path = ImageUtils.updatePATHEnv("test-exec-path"); + assertEquals("PATH=test-exec-path", path); + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + 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.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); + + assertThat(ImageUtils.hostInfo("docker", "info")).isEqualTo("test-output"); } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - 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.runProcessGetFullOutput( - isNull(), aryEq(new String[] {"docker", "info"}), isNull())) - .thenReturn(output); - - var exception = assertThrows(RuntimeException.class, () -> { + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + 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.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"docker", "info"}), isNull())) + .thenReturn(output); + + var exception = + assertThrows( + RuntimeException.class, + () -> { ImageUtils.hostInfo("docker", "info"); - }); - assertEquals("test-error", exception.getMessage()); - } + }); + assertEquals("test-error", exception.getMessage()); } + } - @Test - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_docker_get_os() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("OSType: test-output", "", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + 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"); - } + assertThat(ImageUtils.dockerGetOs()).isEqualTo("test-output"); } + } - @ParameterizedTest(name = "{0}") - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - @MethodSource("dockerArchSources") - void test_docker_get_arch(String sysArch, String arch) { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); + @ParameterizedTest(name = "{0}") + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + @MethodSource("dockerArchSources") + 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); - } + assertThat(ImageUtils.dockerGetArch()).isEqualTo(arch); } + } - @ParameterizedTest(name = "{0}") - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - @MethodSource("dockerVariantSources") - void test_docker_get_variant(String sysArch, String variant) { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); + @ParameterizedTest(name = "{0}") + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + @MethodSource("dockerVariantSources") + 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); - } + assertThat(ImageUtils.dockerGetVariant()).isEqualTo(variant); } + } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_host_info_podman() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("info: test-output\nabcdesss", "", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + 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"); - } + assertThat(ImageUtils.hostInfo("podman", "info")).isEqualTo("test-output"); } + } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_podman_get_os() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("os: test-output", "", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + 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"); - } + assertThat(ImageUtils.podmanGetOs()).isEqualTo("test-output"); } + } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_podman_get_arch() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("arch: test-output", "", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + 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"); - } + assertThat(ImageUtils.podmanGetArch()).isEqualTo("test-output"); } + } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - 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); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_podman_get_variant() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("variant: test-output", "", 0); - mock.when(() -> Operations.runProcessGetFullOutput( - isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) - .thenReturn(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - assertThat(ImageUtils.podmanGetVariant()).isEqualTo("test-output"); - } - } - - @Test - void test_docker_podman_info() { - var info = ImageUtils.dockerPodmanInfo(() -> "docker", () -> "podman"); - assertEquals("docker", info); + mock.when( + () -> + Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); - info = ImageUtils.dockerPodmanInfo(() -> "", () -> "podman"); - assertEquals("podman", info); + assertThat(ImageUtils.podmanGetVariant()).isEqualTo("test-output"); } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } + } + + @Test + void test_docker_podman_info() { + var info = ImageUtils.dockerPodmanInfo(() -> "docker", () -> "podman"); + assertEquals("docker", info); + + info = ImageUtils.dockerPodmanInfo(() -> "", () -> "podman"); + assertEquals("podman", info); + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); - } + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); - } + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - 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); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); - } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + 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); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); } - - @Test - void test_get_multi_image_digests() throws IOException { - 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"); - - assertEquals(expectedDigests, digests); - } + } + + @Test + void test_get_multi_image_digests() throws IOException { + 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"); + + assertEquals(expectedDigests, digests); } + } - @Test - void test_get_multi_image_digests_empty() { - var node = new TextNode("root"); + @Test + void test_get_multi_image_digests_empty() { + var node = new TextNode("root"); - var digests = ImageUtils.getMultiImageDigests(node); - Map expectedDigests = Collections.emptyMap(); + var digests = ImageUtils.getMultiImageDigests(node); + Map expectedDigests = Collections.emptyMap(); - assertEquals(expectedDigests, digests); - } + assertEquals(expectedDigests, digests); + } - @Test - void test_filter_mediaType() throws IOException { - try (var is = getResourceAsStreamDecision( - this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + void test_filter_mediaType() throws IOException { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); - assertTrue(ImageUtils.filterMediaType(node.get("manifests").get(0))); - } + assertTrue(ImageUtils.filterMediaType(node.get("manifests").get(0))); } + } - @Test - void test_filter_digest() throws IOException { - try (var is = getResourceAsStreamDecision( - this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + void test_filter_digest() throws IOException { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); - assertTrue(ImageUtils.filterDigest(node.get("manifests").get(0))); - } + assertTrue(ImageUtils.filterDigest(node.get("manifests").get(0))); } + } - @Test - void test_filter_platform() throws IOException { - try (var is = getResourceAsStreamDecision( - this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + void test_filter_platform() throws IOException { + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); - assertTrue(ImageUtils.filterPlatform(node.get("manifests").get(0))); - } + assertTrue(ImageUtils.filterPlatform(node.get("manifests").get(0))); } - - @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 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); - - var digests = ImageUtils.getSingleImageDigest(mockImageRef); - Map expectedDigests = Collections.singletonMap( - Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); - - assertEquals(expectedDigests, digests); - } + } + + @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 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); + + var digests = ImageUtils.getSingleImageDigest(mockImageRef); + Map expectedDigests = + Collections.singletonMap( + Platform.EMPTY_PLATFORM, + "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + + assertEquals(expectedDigests, digests); } - - @Test - void test_get_single_image_digest_empty() throws JsonProcessingException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var mapper = new ObjectMapper(); - 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); - - var digests = ImageUtils.getSingleImageDigest(mockImageRef); - Map expectedDigests = Collections.emptyMap(); - - assertEquals(expectedDigests, digests); - } + } + + @Test + void test_get_single_image_digest_empty() throws JsonProcessingException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var mapper = new ObjectMapper(); + 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); + + var digests = ImageUtils.getSingleImageDigest(mockImageRef); + Map expectedDigests = Collections.emptyMap(); + + assertEquals(expectedDigests, digests); } + } } diff --git a/src/test/java/com/redhat/exhort/image/PlatformTest.java b/src/test/java/com/redhat/exhort/image/PlatformTest.java index fbce17e4..16803b3b 100644 --- a/src/test/java/com/redhat/exhort/image/PlatformTest.java +++ b/src/test/java/com/redhat/exhort/image/PlatformTest.java @@ -26,80 +26,134 @@ 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)); - } + 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)); + } - @ParameterizedTest(name = "{0}") - @MethodSource("PlatformSources") - void test_platform( - String platform, String os, String arch, String variant, String platformStr, boolean variantRequired) { - var p = new Platform(platform); + @ParameterizedTest(name = "{0}") + @MethodSource("PlatformSources") + void test_platform( + String platform, + String os, + String arch, + String variant, + String platformStr, + boolean variantRequired) { + var p = new Platform(platform); - assertEquals(os, p.getOs()); - assertEquals(arch, p.getArchitecture()); - assertEquals(variant, p.getVariant()); - assertEquals(platformStr, p.toString()); - assertEquals(variantRequired, Platform.isVariantRequired(p.getOs(), p.getArchitecture())); + assertEquals(os, p.getOs()); + assertEquals(arch, p.getArchitecture()); + assertEquals(variant, p.getVariant()); + assertEquals(platformStr, p.toString()); + assertEquals(variantRequired, Platform.isVariantRequired(p.getOs(), p.getArchitecture())); - var pf = new Platform(os, arch, variant); - assertTrue(p.equals(pf)); - assertEquals(p.hashCode(), pf.hashCode()); - } + var pf = new Platform(os, arch, variant); + assertTrue(p.equals(pf)); + assertEquals(p.hashCode(), pf.hashCode()); + } - @Test - void test_platform_invalid() { - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Platform(null); - }); - assertEquals("Invalid platform: null", exception1.getMessage()); + @Test + void test_platform_invalid() { + 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"); - }); - assertEquals("Invalid platform: linux/arm/v8/a", exception2.getMessage()); + 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"); - }); - assertEquals("Image platform is not supported: linux/abc", exception3.getMessage()); + 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, ""); - }); - assertEquals("Invalid platform arch: null", exception4.getMessage()); + 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"); - }); - assertEquals("Image platform is not supported: linux/arm/v8", exception5.getMessage()); + 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); - }); - assertEquals("Image platform is not supported: null/arm/null", exception6.getMessage()); - } + 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 5b9ad56e..c35a9644 100644 --- a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java +++ b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java @@ -66,278 +66,305 @@ @ExtendWith(MockitoExtension.class) class ExhortApiIT extends ExhortTest { - private static Api api; - private static Map ecoSystemsManifestNames; + private static Api api; + private static Map ecoSystemsManifestNames; - private MockedStatic mockedOperations; + 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"); - } + @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"); + } - @Tag("IntegrationTest") - @AfterAll - static void afterAll() { - System.clearProperty("RHDA_SOURCE"); - System.clearProperty("EXHORT_DEV_MODE"); - api = null; - } + @Tag("IntegrationTest") + @AfterAll + static void afterAll() { + System.clearProperty("RHDA_SOURCE"); + 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 { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - 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. - // locally it's not recreated with same versions - mockMavenDependencyTree(packageManager); - AnalysisReport analysisReportResult = api.stackAnalysis(pathToManifest).get(); - handleJsonResponse(analysisReportResult, true); - releaseStaticMock(packageManager); - } + @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 { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + 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. + // locally it's not recreated with same versions + mockMavenDependencyTree(packageManager); + AnalysisReport analysisReportResult = api.stackAnalysis(pathToManifest).get(); + handleJsonResponse(analysisReportResult, true); + releaseStaticMock(packageManager); + } - private void releaseStaticMock(Ecosystem.Type packageManager) { - if (packageManager.equals(Ecosystem.Type.MAVEN)) { - this.mockedOperations.close(); - } + private void releaseStaticMock(Ecosystem.Type packageManager) { + 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 { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - 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. - // 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); - 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_Mixed(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + 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. + // 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); + 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 { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - 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. - // locally it's not recreated with same versions - mockMavenDependencyTree(packageManager); - String analysisReportHtml = - new String(api.stackAnalysisHtml(pathToManifest).get()); - releaseStaticMock(packageManager); - handleHtmlResponse(analysisReportHtml); - } + @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 { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + 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. + // locally it's not recreated with same versions + mockMavenDependencyTree(packageManager); + String analysisReportHtml = new String(api.stackAnalysisHtml(pathToManifest).get()); + releaseStaticMock(packageManager); + 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 { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - byte[] manifestContent = getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName) - .getBytes(); - preparePythonEnvironment(packageManager); - AnalysisReport analysisReportResult = - api.componentAnalysis(manifestFileName, manifestContent).get(); - handleJsonResponse(analysisReportResult, false); - } + @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 { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + byte[] manifestContent = + getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName) + .getBytes(); + preparePythonEnvironment(packageManager); + 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 { + @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) { + } catch (InterruptedException | ExecutionException | IOException e) { throw new RuntimeException(e); - } - }); + } + }); - assertEquals(1, result.size()); - handleJsonResponse(new ArrayList<>(result.values()).get(0), false); - } + assertEquals(1, result.size()); + handleJsonResponse(new ArrayList<>(result.values()).get(0), false); + } - @Tag("IntegrationTest") - @Test - void Integration_Test_End_To_End_Image_Analysis_Html() throws IOException { - var result = testImageAnalysis(i -> { - try { + @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) { + } catch (InterruptedException | ExecutionException | IOException e) { throw new RuntimeException(e); - } - }); + } + }); - handleHtmlResponseForImage(new String(result)); - } + handleHtmlResponseForImage(new String(result)); + } - 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"})) { + 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 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); + 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.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.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)); - } + 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 { - System.clearProperty("EXHORT_PYTHON_VIRTUAL_ENV"); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } + 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 { + 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); - }); - 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)); + 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); + }); + 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()); - } + if (positiveNumberOfTransitives) { + assertTrue(analysisReportResult.getScanned().getTransitive() > 0); + } else { + assertEquals(0, analysisReportResult.getScanned().getTransitive()); } + } - private void handleHtmlResponse(String analysisReportHtml) throws JsonProcessingException { - ObjectMapper om = new ObjectMapper(); - assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); - int jsonStart = analysisReportHtml.indexOf("\"report\":"); - int jsonEnd = analysisReportHtml.indexOf("}}}}}"); - if (jsonEnd == -1) { - jsonEnd = analysisReportHtml.indexOf("}}}}"); - } - 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); - assertTrue(scannedNode.get("transitive").asInt(0) > 0); - 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 handleHtmlResponse(String analysisReportHtml) throws JsonProcessingException { + ObjectMapper om = new ObjectMapper(); + assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); + int jsonStart = analysisReportHtml.indexOf("\"report\":"); + int jsonEnd = analysisReportHtml.indexOf("}}}}}"); + if (jsonEnd == -1) { + jsonEnd = analysisReportHtml.indexOf("}}}}"); } + 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); + assertTrue(scannedNode.get("transitive").asInt(0) > 0); + 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 { - 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); - JsonNode jsonInHtml = om.readTree(embeddedJson); - JsonNode scannedNode = jsonInHtml.findValue("scanned"); - assertTrue(scannedNode.get("total").asInt(0) > 0); - assertTrue(scannedNode.get("transitive").asInt(0) >= 0); - JsonNode status = jsonInHtml.findValue("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 { + 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); + JsonNode jsonInHtml = om.readTree(embeddedJson); + JsonNode scannedNode = jsonInHtml.findValue("scanned"); + assertTrue(scannedNode.get("total").asInt(0) > 0); + assertTrue(scannedNode.get("transitive").asInt(0) >= 0); + JsonNode status = jsonInHtml.findValue("providers").get("osv-nvd").get("status"); + assertTrue(status.get("code").asInt(0) == 200); + assertTrue(status.get("ok").asBoolean(false)); + } - private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOException { - 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"})) { - depTree = new String(is.readAllBytes()); - } - mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"); - }); - } + private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOException { + 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"})) { + depTree = new String(is.readAllBytes()); + } + 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 { - 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()) { - String outputFile = outputFileArg.get(); - outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); - Files.writeString(Path.of(outputFilePath), outputFileContent); - } - return outputFilePath; + 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()) { + 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 d844c0a0..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 @@ -80,734 +80,807 @@ @ClearEnvironmentVariable(key = "RHDA_SOURCE") @SuppressWarnings("unchecked") class Exhort_Api_Test extends ExhortTest { - @Mock - Provider mockProvider; + @Mock Provider mockProvider; + + @Mock HttpClient mockHttpClient; + + @InjectMocks ExhortApi exhortApiSut; + + @AfterEach + void cleanup() { + System.clearProperty("EXHORT_SNYK_TOKEN"); + } + + @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 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"})) { + Files.write(tmpFile, is.readAllBytes()); + } - @Mock - HttpClient mockHttpClient; + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .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"); + + // 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"})) { + expectedHtml = is.readAllBytes(); + } - @InjectMocks - ExhortApi exhortApiSut; + // mock and http response object and stub it to return a fake body + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(expectedHtml); + given(mockHttpResponse.statusCode()).willReturn(200); - @AfterEach - void cleanup() { - System.clearProperty("EXHORT_SNYK_TOKEN"); - } + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); - @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 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"})) { - 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")); - - // 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"); - - // 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"})) { - expectedHtml = is.readAllBytes(); - } - - // mock and http response object and stub it to return a fake body - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(expectedHtml); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - 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)); - - // when invoking the api for a html stack analysis report - var htmlTxt = exhortApiSut.stackAnalysisHtml(tmpFile.toString()); - // verify we got the correct html response - then(htmlTxt.get()).isEqualTo(expectedHtml); - } - // cleanup - Files.deleteIfExists(tmpFile); - } + // 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)); - @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") - void stackAnalysis_with_pom_xml_should_return_json_object_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"})) { - 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")); - - // 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"); - - // 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"})) { - expectedAnalysis = mapper.readValue(is, AnalysisReport.class); - } - - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - 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)); - - // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString()); - // verify we got the correct analysis report - then(responseAnalysis.get()).isEqualTo(expectedAnalysis); - } - // cleanup - Files.deleteIfExists(tmpFile); + // when invoking the api for a html stack analysis report + var htmlTxt = exhortApiSut.stackAnalysisHtml(tmpFile.toString()); + // verify we got the correct html response + then(htmlTxt.get()).isEqualTo(expectedHtml); } - - @Test - void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // load pom.xml - byte[] targetPom; - 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")); - - // we expect this to picked up because no env var to take precedence - System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); - System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); - 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"); - - // 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"})) { - expectedReport = mapper.readValue(is, AnalysisReport.class); - } - - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - try (var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml")).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)); - - // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); - // verify we got the correct analysis report - then(responseAnalysis.get()).isEqualTo(expectedReport); - } + // cleanup + Files.deleteIfExists(tmpFile); + } + + @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") + void stackAnalysis_with_pom_xml_should_return_json_object_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"})) { + Files.write(tmpFile, is.readAllBytes()); } - @Test - 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"})) { - 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"})) { - 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"})) { - 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")); - - // 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"); - - // 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"})) { - mixedResponse = is.readAllBytes(); - } - - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mixedResponse); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - 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)); - - // when invoking the api for a json stack analysis mixed report - var responseAnalysis = - exhortApiSut.stackAnalysisMixed(tmpFile.toString()).get(); - // verify we got the correct mixed report - then(new String(responseAnalysis.html).trim()).isEqualTo(new String(expectedHtml).trim()); - then(responseAnalysis.json).isEqualTo(expectedJson); - } - // cleanup - Files.deleteIfExists(tmpFile); + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .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"); + + // 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"})) { + expectedAnalysis = mapper.readValue(is, AnalysisReport.class); } - @Test - void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_backend() - 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"})) { - 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")); - - // 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"); - - // 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"})) { - expectedReport = mapper.readValue(is, AnalysisReport.class); - } - - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - 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)); - - // 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 - Files.deleteIfExists(tmpFile); - } - } + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis)); + given(mockHttpResponse.statusCode()).willReturn(200); - @AfterEach - void afterEach() { - System.clearProperty("EXHORT_DEV_MODE"); - System.clearProperty("DEV_EXHORT_BACKEND_URL"); - System.clearProperty("RHDA_TOKEN"); - System.clearProperty("RHDA_SOURCE"); - } + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); - @Test - @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); - } + // 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)); - @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); + // when invoking the api for a json stack analysis report + var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString()); + // verify we got the correct analysis report + then(responseAnalysis.get()).isEqualTo(expectedAnalysis); } - - @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); + // cleanup + Files.deleteIfExists(tmpFile); + } + + @Test + void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // load pom.xml + byte[] targetPom; + try (var is = + getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + targetPom = is.readAllBytes(); } - @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); + // stub the mocked provider with a fake content object + given(mockProvider.provideComponent(targetPom)) + .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"); + System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); + 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"); + + // 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"})) { + expectedReport = mapper.readValue(is, AnalysisReport.class); } - @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() { - System.setProperty("EXHORT_DEV_MODE", "true"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); + given(mockHttpResponse.statusCode()).willReturn(200); - @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() { - System.setProperty("EXHORT_DEV_MODE", "true"); - 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")); - } + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml")).thenReturn(mockProvider); - @Test - 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); - } + // 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)); - @Test - 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); + // when invoking the api for a json stack analysis report + var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); + // verify we got the correct analysis report + then(responseAnalysis.get()).isEqualTo(expectedReport); } - - @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() { - System.setProperty("EXHORT_DEV_MODE", "true"); - System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo("http://dummy-route2"); - then(exhortApi.getEndpoint()).isEqualTo("http://dummy-route"); + } + + @Test + 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"})) { + expectedJson = mapper.readValue(is, AnalysisReport.class); } - @Test - void check_Exhort_Url_When_Nothing_Set_Then_Default_Exhort_URL_Selected() { - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + // 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"})) { + expectedHtml = is.readAllBytes(); } - @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 { - 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 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")); - - 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"); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .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")))); - } + // 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"})) { + Files.write(tmpFile, is.readAllBytes()); } - @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 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 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")); - - 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"); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); - - var result = exhortApiSut.imageAnalysisHtml(Set.of(imageRef)); - assertEquals(jsonReport, result.get()); - } + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .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"); + + // 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"})) { + mixedResponse = is.readAllBytes(); } - @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_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"); - var sboms = new HashMap(); - sboms.put(imageRef.getPackageURL().canonicalize(), new ObjectMapper().readTree(is)); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .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"); - - assertEquals(expectedResult, result.get()); - } + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mixedResponse); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + 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)); + + // when invoking the api for a json stack analysis mixed report + var responseAnalysis = exhortApiSut.stackAnalysisMixed(tmpFile.toString()).get(); + // verify we got the correct mixed report + then(new String(responseAnalysis.html).trim()).isEqualTo(new String(expectedHtml).trim()); + then(responseAnalysis.json).isEqualTo(expectedJson); + } + // cleanup + Files.deleteIfExists(tmpFile); + } + + @Test + void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_backend() + 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"})) { + Files.write(tmpFile, is.readAllBytes()); } - @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 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); - - 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())); - - var map = new HashMap(); - map.put(imageRef.getPackageURL().canonicalize(), node); - - assertEquals(map, sboms); - } + // stub the mocked provider with a fake content object + given(mockProvider.provideComponent(tmpFile)) + .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"); + + // 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"})) { + expectedReport = mapper.readValue(is, AnalysisReport.class); } - @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")); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - when(httpResponse.body()).thenReturn(json); - - 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")))); - } + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + 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)); + + // 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 + 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") + 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); + } + + @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); + } + + @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() { + 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() { + System.setProperty("EXHORT_DEV_MODE", "true"); + 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") + @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"); + 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() { + 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() { + 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() { + System.setProperty("EXHORT_DEV_MODE", "true"); + System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo("http://dummy-route2"); + then(exhortApi.getEndpoint()).isEqualTo("http://dummy-route"); + } + + @Test + 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 { + 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 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")); + + 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"); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .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")))); + } + } + + @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 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 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")); + + 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"); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .thenReturn(CompletableFuture.completedFuture(httpResponse)); + + var result = exhortApiSut.imageAnalysisHtml(Set.of(imageRef)); + assertEquals(jsonReport, result.get()); } + } + + @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_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"); + var sboms = new HashMap(); + sboms.put(imageRef.getPackageURL().canonicalize(), new ObjectMapper().readTree(is)); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .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"); + + assertEquals(expectedResult, result.get()); + } + } + + @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 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); + + 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())); + + var map = new HashMap(); + map.put(imageRef.getPackageURL().canonicalize(), node); + + assertEquals(map, sboms); + } + } + + @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")); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(json); + + 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")))); + } + } - @Test - void test_get_batch_image_analysis_reports_error_response() { - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(400); + @Test + void test_get_batch_image_analysis_reports_error_response() { + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(400); - var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); - assertTrue(reports.isEmpty()); - } + var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); + assertTrue(reports.isEmpty()); + } - @Test - void test_get_batch_analysis_reports_from_response() { - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); + @Test + void test_get_batch_analysis_reports_from_response() { + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); - var responseGenerator = mock(Function.class); + 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)); - } + 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 8ab9bbee..31833a0b 100644 --- a/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java +++ b/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java @@ -28,76 +28,84 @@ @Tag("gitTest") class GoModulesMainModuleVersionTest { - private Path noGitRepo; - 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"); - this.noGitRepo = Files.createTempDirectory("exhort_tmp"); - } catch (IOException e) { - throw new RuntimeException(e); - } + private Path noGitRepo; + 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"); + this.noGitRepo = Files.createTempDirectory("exhort_tmp"); + } catch (IOException e) { + throw new RuntimeException(e); } + } - @AfterEach - void tearDown() { - try { - FileUtils.deleteDirectory(this.testGitRepo.toFile()); - FileUtils.deleteDirectory(this.noGitRepo.toFile()); + @AfterEach + void tearDown() { + try { + FileUtils.deleteDirectory(this.testGitRepo.toFile()); + FileUtils.deleteDirectory(this.noGitRepo.toFile()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Test - void determine_Main_Module_Version_NoRepo() { - goModulesProvider.determineMainModuleVersion(noGitRepo); - 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", "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"); - - 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", "tag", "v1.0.0"); - 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())); + } catch (IOException e) { + throw new RuntimeException(e); } + } + + @Test + void determine_Main_Module_Version_NoRepo() { + goModulesProvider.determineMainModuleVersion(noGitRepo); + 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", "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"); + + 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", "tag", "v1.0.0"); + 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())); + } } 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 71090fd9..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 @@ -34,148 +34,170 @@ @ExtendWith(HelperExtension.class) class Golang_Modules_Provider_Test extends ExhortTest { - // test folder are located at src/test/resources/tst_manifests/npm - // each folder should contain: - // - package.json: the target manifest for testing - // - 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"); + // test folder are located at src/test/resources/tst_manifests/npm + // each folder should contain: + // - package.json: the target manifest for testing + // - 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"); + } + + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // 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"})) { + Files.write(tmpGolangFile, is.readAllBytes()); } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // 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"})) { - 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"})) { - expectedSbom = new String(is.readAllBytes()); - } - // when providing stack content for our pom - var content = new GoModulesProvider().provideStack(tmpGolangFile); - // cleanup - 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)); - } - - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - 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"})) { - 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)); + // load expected SBOM + String expectedSbom; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] { + "tst_manifests", "golang", testFolder, "expected_sbom_stack_analysis.json" + })) { + expectedSbom = new String(is.readAllBytes()); } - - @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"); + // when providing stack content for our pom + var content = new GoModulesProvider().provideStack(tmpGolangFile); + // cleanup + 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)); + } + + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + targetPom = is.readAllBytes(); } - - @ParameterizedTest - @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); - } + // load expected SBOM + String expectedSbom = ""; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + new String[] { + "tst_manifests", "golang", testFolder, "expected_sbom_component_analysis.json" + })) { + expectedSbom = new String(is.readAllBytes()); } - - @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"); - 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); - - 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); - } - - private String dropIgnored(String s) { - return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); - } - - private String dropIgnoredKeepFormat(String s) { - return s.replaceAll("\"timestamp\" : \"[a-zA-Z0-9\\-\\:]+\",\n ", ""); + // 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)); + } + + @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"); + } + + @ParameterizedTest + @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); } + } + + @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"); + 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); + + 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); + } + + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + } + + 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 36d2f6d7..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 @@ -38,178 +38,220 @@ @ExtendWith(HelperExtension.class) @ExtendWith(MockitoExtension.class) class Gradle_Provider_Test extends ExhortTest { - // 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"); + // 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"); + } + + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(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"))) { + 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"))) { + 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"))) { + 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"))) { + expectedSbom = new String(is.readAllBytes()); + } + String depTree; + 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"))) { + gradleProperties = new String(is.readAllBytes()); } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(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"))) { - 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"))) { - 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"))) { - 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"))) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - 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"))) { - gradleProperties = new String(is.readAllBytes()); - } + MockedStatic mockedOperations = mockStatic(Operations.class); + ArgumentMatcher gradle = string -> string.equals("gradle"); + 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); - MockedStatic mockedOperations = mockStatic(Operations.class); - ArgumentMatcher gradle = string -> string.equals("gradle"); - 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); + // when providing stack content for our pom + var content = new GradleProvider().provideStack(tmpGradleFile); + // cleanup + Files.deleteIfExists(tmpGradleFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - // when providing stack content for our pom - var content = new GradleProvider().provideStack(tmpGradleFile); - // cleanup - Files.deleteIfExists(tmpGradleFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + @ParameterizedTest + @MethodSource("testFolders") + 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"))) { + targetGradleBuild = is.readAllBytes(); } - @ParameterizedTest - @MethodSource("testFolders") - 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"))) { - 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."); + } - 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."); + @ParameterizedTest + @MethodSource("testFolders") + 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"))) { + 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"))) { + 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"))) { + 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"))) { + expectedSbom = new String(is.readAllBytes()); + } + String depTree; + 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"))) { + gradleProperties = new String(is.readAllBytes()); } - @ParameterizedTest - @MethodSource("testFolders") - 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"))) { - 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"))) { - 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"))) { - 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"))) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - 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"))) { - gradleProperties = new String(is.readAllBytes()); - } - - MockedStatic mockedOperations = mockStatic(Operations.class); - ArgumentMatcher gradle = string -> string.equals("gradle"); - 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); + MockedStatic mockedOperations = mockStatic(Operations.class); + ArgumentMatcher gradle = string -> string.equals("gradle"); + 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); - // when providing component content for our pom - var content = new GradleProvider().provideComponent(tmpGradleFile); - // cleanup - Files.deleteIfExists(tmpGradleFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); - } + // when providing component content for our pom + var content = new GradleProvider().provideComponent(tmpGradleFile); + // cleanup + Files.deleteIfExists(tmpGradleFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - private String dropIgnored(String s) { - return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); - } + private String dropIgnored(String s) { + 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 578fb8d7..5464dbe8 100644 --- a/src/test/java/com/redhat/exhort/providers/HelperExtension.java +++ b/src/test/java/com/redhat/exhort/providers/HelperExtension.java @@ -18,42 +18,43 @@ import java.util.List; import org.junit.jupiter.api.extension.*; -public class HelperExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { - - private System.Logger log = System.getLogger(this.getClass().getName()); - - // 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!!"); - } - - @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())); - } - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - - 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())); - } +public class HelperExtension + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { + + private System.Logger log = System.getLogger(this.getClass().getName()); + + // 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!!"); + } + + @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())); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + + 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())); + } } 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 de3c7f22..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 @@ -25,24 +25,24 @@ public class Java_Envs_Test { - @Test - @SetEnvironmentVariable(key = "JAVA_HOME", value = "test-java-home") - void test_java_get_envs() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs); - } + @Test + @SetEnvironmentVariable(key = "JAVA_HOME", value = "test-java-home") + void test_java_get_envs() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs); + } - @Test - @SetEnvironmentVariable(key = "JAVA_HOME", value = "") - void test_java_get_envs_empty_java_home() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertNull(envs); - } + @Test + @SetEnvironmentVariable(key = "JAVA_HOME", value = "") + void test_java_get_envs_empty_java_home() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertNull(envs); + } - @Test - @ClearEnvironmentVariable(key = "JAVA_HOME") - void test_java_get_envs_no_java_home() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertNull(envs); - } + @Test + @ClearEnvironmentVariable(key = "JAVA_HOME") + void test_java_get_envs_no_java_home() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertNull(envs); + } } 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 6b20b650..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 @@ -39,148 +39,174 @@ @ExtendWith(MockitoExtension.class) public class Java_Maven_Provider_Test extends ExhortTest { - // 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"); + // 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"); + } + + @ParameterizedTest + @MethodSource("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"})) { + Files.write(tmpPomFile, is.readAllBytes()); } - - @ParameterizedTest - @MethodSource("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"})) { - 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"})) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - 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"); - }); - - // when providing stack content for our pom - var content = new JavaMavenProvider().provideStack(tmpPomFile); - // cleanup - Files.deleteIfExists(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)); + // load expected SBOM + String expectedSbom; + 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"})) { + depTree = new String(is.readAllBytes()); } - 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()) { - String outputFile = outputFileArg.get(); - outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); - Files.writeString(Path.of(outputFilePath), outputFileContent); - } - return outputFilePath; + MockedStatic mockedOperations = mockStatic(Operations.class); + 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); + // cleanup + Files.deleteIfExists(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)); + } + + 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()) { + String outputFile = outputFileArg.get(); + outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); + Files.writeString(Path.of(outputFilePath), outputFileContent); + } + return outputFilePath; + } + + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + targetPom = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = + getResourceAsStreamDecision( + getClass(), + new String[] {"tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); } - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - targetPom = is.readAllBytes(); - } - // load expected SBOM - String expectedSbom = ""; - 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"})) { - effectivePom = new String(is.readAllBytes()); - } - - MockedStatic mockedOperations = mockStatic(Operations.class); - 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)); + String effectivePom; + try (var is = + getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "effectivePom.xml"})) { + effectivePom = new String(is.readAllBytes()); } - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - 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"})) { - expectedSbom = new String(is.readAllBytes()); - } - - String effectivePom; - 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"); - }); - - // 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)); + MockedStatic mockedOperations = mockStatic(Operations.class); + 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)); + } + + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + 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"})) { + expectedSbom = new String(is.readAllBytes()); } - private String dropIgnored(String s) { - return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + String effectivePom; + 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"); + }); + + // 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)); + } + + private String dropIgnored(String s) { + 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 8d3645ca..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 @@ -27,35 +27,37 @@ import org.junitpioneer.jupiter.SetSystemProperty; 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); - } + @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); + } - @Test - @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") - @ClearEnvironmentVariable(key = "PATH") - void test_javascript_get_envs_no_path() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertEquals(Collections.singletonMap("PATH", "test-node-home"), envs); - } + @Test + @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") + @ClearEnvironmentVariable(key = "PATH") + void test_javascript_get_envs_no_path() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertEquals(Collections.singletonMap("PATH", "test-node-home"), envs); + } - @Test - @SetSystemProperty(key = "NODE_HOME", value = "") - @SetEnvironmentVariable(key = "PATH", value = "test-path") - void test_javascript_get_envs_empty_java_home() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertNull(envs); - } + @Test + @SetSystemProperty(key = "NODE_HOME", value = "") + @SetEnvironmentVariable(key = "PATH", value = "test-path") + void test_javascript_get_envs_empty_java_home() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertNull(envs); + } - @Test - @ClearSystemProperty(key = "NODE_HOME") - @SetEnvironmentVariable(key = "PATH", value = "test-path") - void test_javascript_get_envs_no_java_home() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertNull(envs); - } + @Test + @ClearSystemProperty(key = "NODE_HOME") + @SetEnvironmentVariable(key = "PATH", value = "test-path") + void test_javascript_get_envs_no_java_home() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertNull(envs); + } } 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 a5a8b4bc..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 @@ -33,149 +33,172 @@ @ExtendWith(HelperExtension.class) class Javascript_Npm_Provider_Test extends ExhortTest { - // test folder are located at src/test/resources/tst_manifests/npm - // each folder should contain: - // - 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"); - } + // test folder are located at src/test/resources/tst_manifests/npm + // each folder should contain: + // - 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"); + } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut package.json - 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"})) { - Files.write(tmpNpmFile, is.readAllBytes()); - } + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut package.json + 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"})) { + Files.write(tmpNpmFile, is.readAllBytes()); + } - 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"})) { - expectedSbom = new String(is.readAllBytes()); - } - String npmListingStack; - 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()) - ArgumentMatcher matchPath = path -> path == null; - 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 - Files.deleteIfExists(tmpNpmFile); - Files.deleteIfExists(tmpLockFile); - Files.deleteIfExists(tmpNpmFolder); - mockedOperations.close(); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + 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"})) { + expectedSbom = new String(is.readAllBytes()); } + String npmListingStack; + 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()) + ArgumentMatcher matchPath = path -> path == null; + 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 + Files.deleteIfExists(tmpNpmFile); + Files.deleteIfExists(tmpLockFile); + Files.deleteIfExists(tmpNpmFolder); + mockedOperations.close(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - targetPom = is.readAllBytes(); - } - // load expected SBOM - 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()); - } + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + targetPom = is.readAllBytes(); + } + // load expected SBOM + 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()); + } - // 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; + // 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); - // when providing component content for our pom - var content = new JavaScriptNpmProvider().provideComponent(targetPom); - mockedOperations.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)); - } + 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(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent_with_Path(String testFolder) throws Exception { - // load the pom target pom file + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent_with_Path(String testFolder) throws Exception { + // load the pom target pom file - // 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"})) { - 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; - 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); - // 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)); + // 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()); } - - private String dropIgnored(String s) { - return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + 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; + 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); + // 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)); + } + + private String dropIgnored(String s) { + 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 25ad5557..f94d773d 100644 --- a/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java +++ b/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java @@ -22,83 +22,87 @@ import org.junit.jupiter.api.extension.*; public class PythonEnvironmentExtension - implements BeforeAllCallback, - AfterAllCallback, - BeforeEachCallback, - AfterEachCallback, - ParameterResolver, - BeforeTestExecutionCallback { + 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()); + 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; + 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"); - pythonController.cleanEnvironment(true); - } + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + 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())); - } + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + 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"); - 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); + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + 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); - } + } - @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { - log.log( - System.Logger.Level.INFO, - String.format("About to Start Test Method: %s", extensionContext.getRequiredTestMethod())); - } + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + 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); - } + @Override + 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 { - return this.pythonController; - } + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return this.pythonController; + } - @Override - public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { - // Method requiredTestMethod = extensionContext.getRequiredTestInstances(); - } + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + // 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 0f919234..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 @@ -35,146 +35,168 @@ @ExtendWith(PythonEnvironmentExtension.class) class Python_Provider_Test extends ExhortTest { - static Stream testFolders() { - return Stream.of("pip_requirements_txt_no_ignore", "pip_requirements_txt_ignore"); - } + static Stream testFolders() { + 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; - this.pythonPipProvider = new PythonPipProvider(); - this.pythonPipProvider.setPythonController(pythonController); - } + public Python_Provider_Test(PythonControllerBase pythonController) { + this.pythonController = pythonController; + this.pythonPipProvider = new PythonPipProvider(); + this.pythonPipProvider.setPythonController(pythonController); + } - private PythonControllerBase pythonController; - private PythonPipProvider pythonPipProvider; + private PythonControllerBase pythonController; + private PythonPipProvider pythonPipProvider; - @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"})) { - 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"})) { - expectedSbom = new String(is.readAllBytes()); - } - // when providing stack content for our pom - var content = this.pythonPipProvider.provideStack(tmpPythonFile); - // cleanup - Files.deleteIfExists(tmpPythonFile); - 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)); + @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"})) { + Files.write(tmpPythonFile, is.readAllBytes()); } - - @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"})) { - targetRequirementsTxt = is.readAllBytes(); - } - // load expected SBOM - String expectedSbom = ""; - 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)); + // load expected SBOM + String expectedSbom; + 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); + // cleanup + Files.deleteIfExists(tmpPythonFile); + 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)); + } - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - 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"})) { - 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); - // cleanup - Files.deleteIfExists(tmpPythonFile); - Files.deleteIfExists(tmpPythonModuleDir); - 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)); + @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"})) { + targetRequirementsTxt = is.readAllBytes(); } - - @ParameterizedTest - @MethodSource("testFolders") - 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"})) { - targetRequirementsTxt = is.readAllBytes(); - } - // load expected SBOM - String expectedSbom = ""; - 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); - // 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)); - System.clearProperty("EXHORT_PIP_SHOW"); - System.clearProperty("EXHORT_PIP_FREEZE"); + // load expected SBOM + String expectedSbom = ""; + 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)); + } - @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"); + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + 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"})) { + 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); + // cleanup + Files.deleteIfExists(tmpPythonFile); + Files.deleteIfExists(tmpPythonModuleDir); + 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)); + } - private String dropIgnored(String s) { - return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + @ParameterizedTest + @MethodSource("testFolders") + 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"})) { + targetRequirementsTxt = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + 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); + // 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)); + 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"); + } + + private String dropIgnored(String s) { + 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 ddda0d03..4f728773 100644 --- a/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java @@ -24,15 +24,16 @@ 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)); - } + @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)); + } - @Test - 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); - } + @Test + 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 568ff357..8c011c05 100644 --- a/src/test/java/com/redhat/exhort/tools/OperationsTest.java +++ b/src/test/java/com/redhat/exhort/tools/OperationsTest.java @@ -23,26 +23,31 @@ class OperationsTest { - @Test - void when_running_process_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); - } + @Test + void when_running_process_for_existing_command_should_not_throw_exception() { + assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); + } - @Test - void when_running_process_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); - } + @Test + void when_running_process_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); + } - @Test - void when_running_process_get_full_output_for_existing_command_should_not_throw_exception() { - assertThatNoException() - .isThrownBy(() -> Operations.runProcessGetFullOutput(null, new String[] {"ls", "."}, null)); - } + @Test + void when_running_process_get_full_output_for_existing_command_should_not_throw_exception() { + 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"})); - } + @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"})); + } } 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 e10fcc6f..14f4b6e9 100644 --- a/src/test/java/com/redhat/exhort/tools/Operations_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Operations_Test.java @@ -26,43 +26,45 @@ import org.junitpioneer.jupiter.SetEnvironmentVariable; class Operations_Test { - @Nested - class Test_runProcess { - @Test - void when_running_process_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); - } + @Nested + class Test_runProcess { + @Test + void when_running_process_for_existing_command_should_not_throw_exception() { + assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); + } - @Test - void when_running_process_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); - } + @Test + void when_running_process_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); } + } - @Nested - @ClearEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH") - class Test_getCustomPathOrElse { - @AfterEach - void cleanup() { - System.clearProperty("EXHORT_MADE_UP_CMD_PATH"); - } + @Nested + @ClearEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH") + class Test_getCustomPathOrElse { + @AfterEach + void cleanup() { + System.clearProperty("EXHORT_MADE_UP_CMD_PATH"); + } - @Test - @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"); - } + @Test + @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"); + } - @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"); - } + @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"); + } - @Test - void when_no_custom_path_in_env_var_or_properties_should_return_the_default_executable() { - assertThat(Operations.getCustomPathOrElse("madeupcmd")).isEqualTo("madeupcmd"); - } + @Test + void when_no_custom_path_in_env_var_or_properties_should_return_the_default_executable() { + assertThat(Operations.getCustomPathOrElse("madeupcmd")).isEqualTo("madeupcmd"); } + } } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java index ad05957f..ea8ad87d 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java @@ -26,1968 +26,2020 @@ class PythonControllerBaseTest extends ExhortTest { - static ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { + static ArgumentMatcher matchCommandPipFreeze = + new ArgumentMatcher() { @Override public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); + 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 + + // 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; + return String[].class; } - }; + }; - static ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { + static ArgumentMatcher matchCommandPipShow = + new ArgumentMatcher() { @Override public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("show")); + return Arrays.stream(command).anyMatch(word -> word.contains("show")); } @Override public Class type() { - return String[].class; + 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); - } + @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; + 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"; - } + 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"; + } - private static final List EXPECTED_PIP_SHOW_RESULTS = new LinkedList<>(); + 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"); + 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: 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 f97315f1..8b004967 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java @@ -35,275 +35,304 @@ 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"); + 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; - // } - // - // }; + // 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"); - } + @BeforeEach + void setUp() { + pythonControllerRealEnv = new PythonControllerRealEnv("python3", "pip3"); + } - @AfterEach - void tearDown() {} + @AfterEach + void tearDown() {} - @ParameterizedTest - @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"); - 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"; + @ParameterizedTest + @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"); + 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\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."); - 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 { + 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."); + 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); - 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); - }); + 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); + }); - // 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()); + // 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()); - assertTrue(actualSetOfPackagesLC.containsAll(expectedSetOfPackagesLC)); - assertTrue(expectedSetOfPackagesLC.containsAll(actualSetOfPackagesLC)); - operationsMockedStatic.close(); - } + 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); - }); - } + 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); + }); } + } - @Test - void get_Dependencies_from_Cyclic_Tree() { - MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); - // ArgumentMatcher matchCommandPipFreeze = command -> Arrays.stream(command).anyMatch(word -> - // word.contains("freeze")); + @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); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - assertEquals(104, dependencies.size()); + 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()); - operationsMockedStatic.close(); - } + operationsMockedStatic.close(); + } - @Test - void get_Dependency_Name_requirements() { + @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()); - } + @Test + void automaticallyInstallPackageOnEnvironment() { + assertFalse(this.pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment()); + } - @Test - void isRealEnv() { + @Test + void isRealEnv() { - assertTrue(this.pythonControllerRealEnv.isRealEnv()); - } + assertTrue(this.pythonControllerRealEnv.isRealEnv()); + } - @Test - void isVirtualEnv() { - assertFalse(this.pythonControllerRealEnv.isVirtualEnv()); - } + @Test + 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 1ad54d69..920e5c8f 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java @@ -32,67 +32,73 @@ class PythonControllerVirtualEnvTest extends ExhortTest { - private static PythonControllerVirtualEnv pythonControllerVirtualEnv; - private static PythonControllerVirtualEnv spiedPythonControllerVirtualEnv; + private static PythonControllerVirtualEnv pythonControllerVirtualEnv; + private static PythonControllerVirtualEnv spiedPythonControllerVirtualEnv; - private ObjectMapper om = new ObjectMapper(); + private ObjectMapper om = new ObjectMapper(); - @BeforeAll - static void setUp() { + @BeforeAll + static void setUp() { - pythonControllerVirtualEnv = new PythonControllerVirtualEnv("python3"); - spiedPythonControllerVirtualEnv = Mockito.spy(pythonControllerVirtualEnv); - } + 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); + @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.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies)); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } + System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies)); + System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } - @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)); - assertTrue(runtimeException.getMessage().contains("Conflicting settings")); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - } + @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)); + assertTrue(runtimeException.getMessage().contains("Conflicting settings")); + System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); + } - @Test - void test_Virtual_Environment_Flow() throws IOException { - // Mockito - String requirementsTxt = "Jinja2==3.0.3"; - 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); - 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(); - } + @Test + void test_Virtual_Environment_Flow() throws IOException { + // Mockito + String requirementsTxt = "Jinja2==3.0.3"; + 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); + 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(); + } - @Test - void isRealEnv() { + @Test + void isRealEnv() { - assertFalse(this.spiedPythonControllerVirtualEnv.isRealEnv()); - } + assertFalse(this.spiedPythonControllerVirtualEnv.isRealEnv()); + } - @Test - void isVirtualEnv() { - assertTrue(this.spiedPythonControllerVirtualEnv.isVirtualEnv()); - } + @Test + void isVirtualEnv() { + assertTrue(this.spiedPythonControllerVirtualEnv.isVirtualEnv()); + } }