diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e0266407..a5482109 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,6 +1,16 @@ name: Build and test on: [push] +permissions: + # The GITHUB_TOKEN is used to download AppMap service + # binaries in addition to cloning the repository; by explicitly + # setting permissions, we ensure it has no unnecessary access. + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-check: name: Build and check @@ -9,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Setup Gradle @@ -31,9 +41,21 @@ jobs: annotation/build/libs/*.jar test-suite: + services: + oracle: + image: docker.io/gvenzl/oracle-free:slim-faststart + ports: + - 1521:1521 + env: + ORACLE_PASSWORD: oracle + options: >- + --health-cmd healthcheck.sh + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: - java: ['17', '11', '8'] + java: ['21', '17', '11', '8'] runs-on: ubuntu-latest name: Run test suite with Java ${{ matrix.java }} needs: build-and-check @@ -106,5 +128,11 @@ jobs: env: BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} TERM: xterm + # Github token is just to avoid rate limiting when IntelliJ tests + # are run and download the AppMap service binaries + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORACLE_URL: jdbc:oracle:thin:@localhost:1521 + ORACLE_USERNAME: system + ORACLE_PASSWORD: oracle working-directory: ./agent run: bin/test_run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e0cdf2e..b5323ba9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - uses: actions/setup-java@v5 with: - java-version: "17" + java-version: "21" distribution: "temurin" cache: gradle - name: Setup node diff --git a/.gitignore b/.gitignore index c8c96301..32dc79d3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ tmp # test output /.metadata/ + +# Log files +*.log diff --git a/agent/bin/test_install b/agent/bin/test_install index 878aed58..d686f41f 100755 --- a/agent/bin/test_install +++ b/agent/bin/test_install @@ -9,5 +9,3 @@ for d in build/fixtures/*; do ./mvnw package -quiet -DskipTests -Dcheckstyle.skip=true -Dspring-javaformat.skip=true cd - done - -../gradlew testClasses diff --git a/agent/bin/test_projects b/agent/bin/test_projects index 137147f1..541d20e1 100755 --- a/agent/bin/test_projects +++ b/agent/bin/test_projects @@ -1,15 +1,23 @@ #!/usr/bin/env bash -fixture_dir=$PWD/build/fixtures +set -eo pipefail + +AGENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +cd "${AGENT_DIR}" + mkdir -p build/fixtures +# shellcheck source=../test/helper.bash source "test/helper.bash" -export ANNOTATION_JAR="$(find_annotation_jar)" +ANNOTATION_JAR="$(find_annotation_jar)" +export ANNOTATION_JAR -function install_petclinic ( +function install_petclinic() { local repo="$1"; shift local ref=${1:-main} - local pkg="$(basename $repo)" + local pkg + pkg="$(basename "$repo")" if [[ -d "build/fixtures/${pkg}" ]]; then echo "Fixture already exists: ${pkg}" @@ -43,7 +51,7 @@ function install_petclinic ( cd ../../.. -) +} function install_scala_test_app { if [[ -d "test/scala/play-samples" ]]; then @@ -53,14 +61,13 @@ function install_scala_test_app { cd test/scala rm -rf play-samples local branch=3.0.x - case "${JAVA_VERSION}" in - 1.8*) - branch=2.8.x - ;; - 11.*) - branch=2.9.x - ;; - esac + if is_java 17; then + branch=3.0.x + elif is_java 11; then + branch=2.9.x + else + branch=2.8.x + fi git clone --no-checkout https://github.com/playframework/play-samples.git --depth 1 --branch $branch cd play-samples git sparse-checkout set play-scala-rest-api-example @@ -69,20 +76,16 @@ function install_scala_test_app { cd ../../.. } -case "${JAVA_VERSION}" in - 1.8*|11.*) - install_petclinic "land-of-apps/spring-petclinic" old-java-support - ;; - 17.*) - # The spring-petclinic main branch now requires Java 25. This is the last commit that supports Java 17. - install_petclinic "spring-projects/spring-petclinic" "3aa79e3944ab1b626288f5d0629e61643ab8fb4a" - install_petclinic "spring-petclinic/spring-framework-petclinic" - ;; - *) # For Java 25+ - install_petclinic "spring-projects/spring-petclinic" "main" - install_petclinic "spring-petclinic/spring-framework-petclinic" - ;; -esac +if is_java 25; then + install_petclinic "spring-projects/spring-petclinic" "main" + install_petclinic "spring-petclinic/spring-framework-petclinic" +elif is_java 17; then + # The spring-petclinic main branch now requires Java 25. This is the last commit that supports Java 17. + install_petclinic "spring-projects/spring-petclinic" "3aa79e3944ab1b626288f5d0629e61643ab8fb4a" + install_petclinic "spring-petclinic/spring-framework-petclinic" +else + install_petclinic "land-of-apps/spring-petclinic" old-java-support +fi patch -N -p1 -d build/fixtures/spring-petclinic < test/petclinic/pom.patch diff --git a/agent/bin/test_run b/agent/bin/test_run index cf2cd954..d62f7ab6 100755 --- a/agent/bin/test_run +++ b/agent/bin/test_run @@ -15,5 +15,5 @@ set -x # * just doing bats -r test doesn't discover a setup_suite.bash file correctly. http_client uses # one, so it needs to be run separately -bats -r test/!(http_client) -bats -r test/http_client +bats -r test/!(http_client)/ +bats -r test/http_client/ diff --git a/agent/build.gradle b/agent/build.gradle index 608bd024..b479bbb7 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -14,7 +14,6 @@ plugins { } repositories { - jcenter() mavenCentral() } @@ -52,12 +51,12 @@ dependencies { // result, you won't be able to add hooks for anything in those packages. implementation 'com.alibaba:fastjson:1.2.83' implementation "org.javassist:javassist:${javassistVersion}" - implementation 'org.reflections:reflections:0.9.11' + implementation 'org.reflections:reflections:0.10.2' implementation 'net.bytebuddy:byte-buddy:1.14.10' - implementation 'org.apache.commons:commons-lang3:3.10' + implementation 'org.apache.commons:commons-lang3:3.20.0' implementation 'commons-io:commons-io:2.15.1' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.2' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' implementation 'org.slf4j:slf4j-nop:1.7.30' implementation 'info.picocli:picocli:4.6.1' implementation 'org.apache.httpcomponents:httpcore-nio:4.4.15' @@ -75,6 +74,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'com.github.stefanbirkner:system-rules:1.19.0' testImplementation 'com.github.stefanbirkner:system-lambda:1.2.1' @@ -85,8 +85,7 @@ dependencies { } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + options.release = 8 } jar { @@ -97,11 +96,11 @@ jar { } } -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' shadowJar { - baseName = 'appmap' - classifier = '' + archiveBaseName = 'appmap' + archiveClassifier = '' minimize() { // tinylog computes the dependencies it needs at runtime, so don't exclude // anything. @@ -162,6 +161,7 @@ test { dependsOn cleanTest exclude 'com/appland/appmap/integration/**' // systemProperty "appmap.log.level", "debug" + jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED' } task relocateShadowJar(type: ShadowRelocation) { @@ -179,16 +179,16 @@ tasks.shadowJar.dependsOn tasks.relocateShadowJar jacocoTestReport { reports { - xml.enabled false - csv.enabled false - html.enabled true + xml.required = false + csv.required = false + html.required = true } } // extra artifacts used in publishing task sourcesJar(type: Jar) { from sourceSets.main.allJava - classifier = 'sources' + archiveClassifier = 'sources' } // for some reason this block generates empty Javadoc @@ -198,7 +198,7 @@ javadoc { } task mockJavadocJar(type: Jar) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } @@ -212,8 +212,8 @@ publishing { // 1. coordinates (parameterized) - groupId publishGroupId - artifactId publishArtifactId + groupId = publishGroupId + artifactId = publishArtifactId // version defined globally @@ -271,4 +271,4 @@ if (project.hasProperty("signingKey")) { } } -tasks.publishToMavenLocal.dependsOn(check, integrationTest) \ No newline at end of file +tasks.publishToMavenLocal.dependsOn(check, integrationTest) diff --git a/agent/src/main/java/com/appland/appmap/Agent.java b/agent/src/main/java/com/appland/appmap/Agent.java index 3de302b7..3de6be8c 100644 --- a/agent/src/main/java/com/appland/appmap/Agent.java +++ b/agent/src/main/java/com/appland/appmap/Agent.java @@ -72,8 +72,11 @@ public static void premain(String agentArgs, Instrumentation inst) { logger.info("Agent version {}, current time mills: {}", implementationVersion, start); logger.info("config: {}", AppMapConfig.get()); - logger.info("System properties: {}", System.getProperties()); - logger.debug(new Exception(), "whereAmI"); + logger.debug("System properties: {}", System.getProperties()); + + if (Agent.class.getClassLoader() == null) { + logger.warn("AppMap agent is running on the bootstrap classpath. This is not a recommended configuration and should only be used for troubleshooting. Git integration will be disabled."); + } addAgentJars(agentArgs, inst); @@ -163,12 +166,18 @@ private static void addAgentJars(String agentArgs, Instrumentation inst) { Path agentJarPath = null; try { Class agentClass = Agent.class; - URL resourceURL = agentClass.getClassLoader() - .getResource(agentClass.getName().replace('.', '/') + ".class"); + // When the agent is loaded by the bootstrap class loader (e.g., via -Xbootclasspath/a:), + // agentClass.getClassLoader() returns null, leading to a NullPointerException. To handle + // this, we use Class.getResource() which correctly resolves resources even when the + // class is loaded by the bootstrap class loader. The leading '/' in the resource name + // is crucial for absolute path resolution when using Class.getResource(). + URL resourceURL = agentClass.getResource("/" + agentClass.getName().replace('.', '/') + ".class"); + // During testing of the agent itself, classes get loaded from a directory, and will have the // protocol "file". The rest of the time (i.e. when it's actually deployed), they'll always - // come from a jar file. - if (resourceURL.getProtocol().equals("jar")) { + // come from a jar file. We must also check that resourceURL is not null before using it, + // as getResource() can return null if the resource is not found. + if (resourceURL != null && resourceURL.getProtocol().equals("jar")) { String resourcePath = resourceURL.getPath(); URL jarURL = new URL(resourcePath.substring(0, resourcePath.indexOf('!'))); logger.debug("jarURL: {}", jarURL); @@ -214,13 +223,14 @@ private static void setupRuntime(Path agentJarPath, JarFile agentJar, Instrument System.exit(1); } - // Adding the runtime jar to the boot class loader means the classes it - // contains will be available everywhere. This avoids issues caused by any - // filtering the app's class loader might be doing (e.g. the Scala runtime - // when running a Play app). + // It's critical to append the runtime JAR to the bootstrap class loader + // search path, not the system class loader search path. This ensures that + // AppMap's core runtime classes, such as HookFunctions, are available to + // all application classes, including those loaded by different class loaders + // (e.g., in web servers like Tomcat or other complex environments), which + // fixes `NoClassDefFoundError` for `HookFunctions`. JarFile runtimeJar = new JarFile(runtimeJarPath.toFile()); - inst.appendToSystemClassLoaderSearch(runtimeJar); - // inst.appendToBootstrapClassLoaderSearch(runtimeJar); + inst.appendToBootstrapClassLoaderSearch(runtimeJar); // HookFunctions can only be referenced after the runtime jar has been // appended to the boot class loader. diff --git a/agent/src/main/java/com/appland/appmap/config/AppMapConfig.java b/agent/src/main/java/com/appland/appmap/config/AppMapConfig.java index 846d962a..f896472d 100644 --- a/agent/src/main/java/com/appland/appmap/config/AppMapConfig.java +++ b/agent/src/main/java/com/appland/appmap/config/AppMapConfig.java @@ -143,15 +143,6 @@ static AppMapConfig load(Path configFile, boolean mustExist) { singleton.configFile = configFile; logger.debug("config: {}", singleton); - int count = singleton.packages.length; - count = Arrays.stream(singleton.packages).map(p -> p.exclude).reduce(count, - (acc, e) -> acc += e.length, Integer::sum); - - int pattern_threshold = Properties.PatternThreshold; - if (count > pattern_threshold) { - logger.warn("{} patterns found in config, startup performance may be impacted", count); - } - return singleton; } @@ -317,6 +308,8 @@ public static TaggedLogger configureLogging() { // tinylog freezes its configuration after the first call to any of its // methods other than those in Configuration. So, get everything ready // before returning the logger for this class; + Configuration.set("writer.format", "{date:yyyy-MM-dd HH:mm:ss} [{thread}] AppMap {level}: {message}"); + if (Properties.Debug) { Configuration.set("level", "debug"); } @@ -365,6 +358,25 @@ private static Path findDefaultOutputDirectory(FileSystem fs) { @Override public String toString() { - return JSON.toJSONString(this, true); + StringBuilder sb = new StringBuilder(); + sb.append("name: ").append(name).append("\n"); + if (configFile != null) { + sb.append("configFile: ").append(configFile).append("\n"); + } + sb.append("packages: "); + if (packages == null || packages.length == 0) { + sb.append("[]"); + } else { + for (AppMapPackage pkg : packages) { + sb.append("\n - path: ").append(pkg.path); + if (pkg.shallow) { + sb.append("\n shallow: true"); + } + if (pkg.exclude != null && pkg.exclude.length > 0) { + sb.append("\n exclude: ").append(Arrays.toString(pkg.exclude)); + } + } + } + return sb.toString(); } } diff --git a/agent/src/main/java/com/appland/appmap/config/AppMapPackage.java b/agent/src/main/java/com/appland/appmap/config/AppMapPackage.java index b5021987..1a6ff8cf 100644 --- a/agent/src/main/java/com/appland/appmap/config/AppMapPackage.java +++ b/agent/src/main/java/com/appland/appmap/config/AppMapPackage.java @@ -1,42 +1,96 @@ package com.appland.appmap.config; -import static com.appland.appmap.util.ClassUtil.safeClassForName; - import java.util.regex.Pattern; import org.tinylog.TaggedLogger; -import com.appland.appmap.transform.annotations.CtClassUtil; import com.appland.appmap.util.FullyQualifiedName; +import com.appland.appmap.util.PrefixTrie; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import javassist.CtBehavior; + +/** + * Represents a package configuration for AppMap recording. + * + *

+ * Configuration modes (mutually exclusive): + *

+ * + * @see AppMap + * Java Configuration + */ public class AppMapPackage { private static final TaggedLogger logger = AppMapConfig.getLogger(null); private static String tracePrefix = Properties.DebugClassPrefix; public String path; + public final String packagePrefix; public String[] exclude = new String[] {}; public boolean shallow = false; - public Boolean allMethods = true; + private final PrefixTrie excludeTrie = new PrefixTrie(); - public static class LabelConfig { + @JsonCreator + public AppMapPackage(@JsonProperty("path") String path, + @JsonProperty("exclude") String[] exclude, + @JsonProperty("shallow") Boolean shallow, + @JsonProperty("methods") LabelConfig[] methods) { + this.path = path; + this.exclude = exclude == null ? new String[] {} : exclude; + this.shallow = shallow != null && shallow; + this.methods = methods; + this.packagePrefix = this.path == null ? "!!dummy!!" : this.path + "."; + + // Warn if both exclude and methods are specified (methods takes precedence) + if (exclude != null && exclude.length > 0 && methods != null && methods.length > 0) { + logger.warn("Package '{}': both 'exclude' and 'methods' are specified. " + + "The 'exclude' field will be ignored when 'methods' is set.", path); + } + + // Build the exclusion trie only if we're in exclude mode + if (exclude != null && methods == null) { + for (String exclusion : exclude) { + // Allow exclusions to use both '.' and '#' as separators + // for backward compatibility + exclusion = exclusion.replace('#', '.'); + if (exclusion.startsWith(this.packagePrefix)) { + // Absolute path: strip the package prefix + this.excludeTrie.insert(exclusion.substring(this.packagePrefix.length())); + } else { + // Relative path: use as-is + this.excludeTrie.insert(exclusion); + } + } + } + } + /** + * Configuration for matching specific methods with labels. + * Used in "methods mode" to specify which methods to record. + */ + public static class LabelConfig { private Pattern className = null; private Pattern name = null; - private String[] labels = new String[] {}; - private Class cls; + /** Empty constructor for exclude mode (no labels). */ public LabelConfig() {} @JsonCreator - public LabelConfig(@JsonProperty("class") String className, @JsonProperty("name") String name, + public LabelConfig(@JsonProperty("class") String className, + @JsonProperty("name") String name, @JsonProperty("labels") String[] labels) { + // Anchor patterns to match whole symbols only this.className = Pattern.compile("\\A(" + className + ")\\z"); - this.cls = safeClassForName(Thread.currentThread().getContextClassLoader(), className); - logger.trace("this.cls: {}", this.cls); this.name = Pattern.compile("\\A(" + name + ")\\z"); this.labels = labels; } @@ -45,63 +99,126 @@ public String[] getLabels() { return this.labels; } - public boolean matches(FullyQualifiedName name) { - return matches(name.className, name.methodName); - } - - public boolean matches(String className, String methodName) { - boolean traceClass = tracePrefix == null || className.startsWith(tracePrefix); - Class cls = safeClassForName(Thread.currentThread().getContextClassLoader(), className); - - if (traceClass) { - logger.trace("this.cls: {} cls: {}, isChildOf?: {}", this.cls, cls, CtClassUtil.isChildOf(cls, this.cls)); + /** + * Checks if the given fully qualified name matches this configuration. + * Supports matching against both simple and fully qualified class names for + * flexibility. + * + * @param fqn the fully qualified name to check + * @return true if the patterns match + */ + public boolean matches(FullyQualifiedName fqn) { + // Try matching with simple class name (package-relative) + if (matches(fqn.className, fqn.methodName)) { + return true; } - return this.className.matcher(className).matches() && this.name.matcher(methodName).matches(); + // Also try matching with fully qualified class name for better UX + String fullyQualifiedClassName = fqn.getClassName(); + return matches(fullyQualifiedClassName, fqn.methodName); } + /** + * Checks if the given class name and method name match this configuration. + * + * @param className the class name (simple or fully qualified) + * @param methodName the method name + * @return true if both patterns match + */ + public boolean matches(String className, String methodName) { + return this.className.matcher(className).matches() + && this.name.matcher(methodName).matches(); + } } public LabelConfig[] methods = null; /** - * Check if a class/method is included in the configuration. - * - * @param canonicalName the canonical name of the class/method to be checked - * @return {@code true} if the class/method is included in the configuration. {@code false} if it - * is not included or otherwise explicitly excluded. + * Determines if a class/method should be recorded based on this package + * configuration. + * + *

+ * Behavior depends on configuration mode: + *

+ * + * @param canonicalName the fully qualified name of the method to check + * @return the label config if the method should be recorded, or null otherwise */ public LabelConfig find(FullyQualifiedName canonicalName) { - String className = canonicalName != null ? canonicalName.getClassName() : null; - boolean traceClass = tracePrefix == null || className.startsWith(tracePrefix); - if (traceClass) { - logger.trace(canonicalName); + // Early validation + if (this.path == null || canonicalName == null) { + return null; } - if (this.path == null) { - return null; + // Debug logging + if (tracePrefix == null || canonicalName.getClassName().startsWith(tracePrefix)) { + logger.trace("Checking {}", canonicalName); } - if (canonicalName == null) { - return null; + if (isExcludeMode()) { + return findInExcludeMode(canonicalName); + } else { + return findInMethodsMode(canonicalName); } + } - // If no method configs are set, use the old matching behavior. - if (this.methods == null) { - if (!canonicalName.toString().startsWith(this.path)) { + /** + * Checks if this package is configured in exclude mode (records everything + * except exclusions). + */ + private boolean isExcludeMode() { + return this.methods == null; + } + + /** + * Finds a method in exclude mode: match if in package and not excluded. + */ + private LabelConfig findInExcludeMode(FullyQualifiedName canonicalName) { + String canonicalString = canonicalName.toString(); + + // Check if the method is in this package or a subpackage + if (!canonicalString.startsWith(this.path)) { + return null; + } else if (canonicalString.length() > this.path.length()) { + // Must either equal the path exactly or start with "path." or "path#" + // The "#" check is needed for unnamed packages + // or when path specifies a class name + final char nextChar = canonicalString.charAt(this.path.length()); + if (nextChar != '.' && nextChar != '#') { return null; } + } - return this.excludes(canonicalName) ? null : new LabelConfig(); + // Check if it's explicitly excluded + if (this.excludes(canonicalName)) { + return null; } + // Include it (no labels in exclude mode) + return new LabelConfig(); + } + + /** + * Finds a method in methods mode: match only if it matches a configured + * pattern. + */ + private LabelConfig findInMethodsMode(FullyQualifiedName canonicalName) { + // Must be in the exact package (not subpackages) if (!canonicalName.packageName.equals(this.path)) { return null; } - for (LabelConfig ls : this.methods) { - if (ls.matches(canonicalName)) { - return ls; + // Check each method pattern + for (LabelConfig config : this.methods) { + if (config.matches(canonicalName)) { + return config; } } @@ -109,35 +226,56 @@ public LabelConfig find(FullyQualifiedName canonicalName) { } /** - * Returns whether or not the canonical name is explicitly excluded - * - * @param canonicalName the canonical name of the class/method to be checked + * Converts a fully qualified class name to a package-relative name. + * For example, "com.example.foo.Bar" with package "com.example" becomes + * "foo.Bar". + * + * @param fqcn the fully qualified class name + * @return the relative class name, or the original if it doesn't start with the + * package prefix + */ + private String getRelativeClassName(String fqcn) { + if (fqcn.startsWith(this.packagePrefix)) { + return fqcn.substring(this.packagePrefix.length()); + } + return fqcn; + } + + /** + * Checks whether a behavior is explicitly excluded by this package + * configuration. + * Only meaningful in exclude mode; in methods mode, use {@link #find} instead. + * + * @param behavior the behavior to check + * @return true if the behavior matches an exclusion pattern */ public Boolean excludes(CtBehavior behavior) { - FullyQualifiedName fqn = null; - for (String exclusion : this.exclude) { - if (behavior.getDeclaringClass().getName().startsWith(exclusion)) { - return true; - } else { - if (fqn == null) { - fqn = new FullyQualifiedName(behavior); - } - if (fqn.toString().startsWith(exclusion)) { - return true; - } - } + String fqClass = behavior.getDeclaringClass().getName(); + String relativeClassName = getRelativeClassName(fqClass); + + // Check if the class itself is excluded + if (this.excludeTrie.startsWith(relativeClassName)) { + return true; } - return false; + // Check if the specific method is excluded + String methodName = behavior.getName(); + String relativeMethodPath = String.format("%s.%s", relativeClassName, methodName); + return this.excludeTrie.startsWith(relativeMethodPath); } + /** + * Checks whether a fully qualified method name is explicitly excluded. + * Only meaningful in exclude mode; in methods mode, use {@link #find} instead. + * + * @param canonicalName the fully qualified method name + * @return true if the method matches an exclusion pattern + */ public Boolean excludes(FullyQualifiedName canonicalName) { - for (String exclusion : this.exclude) { - if (canonicalName.toString().startsWith(exclusion)) { - return true; - } - } - - return false; + String fqcn = canonicalName.toString(); + String relativeName = getRelativeClassName(fqcn); + // Convert # to . to match the format stored in the trie + relativeName = relativeName.replace('#', '.'); + return this.excludeTrie.startsWith(relativeName); } } diff --git a/agent/src/main/java/com/appland/appmap/config/Properties.java b/agent/src/main/java/com/appland/appmap/config/Properties.java index 5c4c168e..77766bcc 100644 --- a/agent/src/main/java/com/appland/appmap/config/Properties.java +++ b/agent/src/main/java/com/appland/appmap/config/Properties.java @@ -21,7 +21,12 @@ public class Properties { public static final String DebugClassPrefix = resolveProperty("appmap.debug.classPrefix", (String) null); public static final Boolean SaveInstrumented = resolveProperty("appmap.debug.saveInstrumented", false); - public static final Boolean DisableGit = resolveProperty("appmap.debug.disableGit", false); + public static final Boolean DisableGit = + // Git integration (JGit) uses resource bundles, which are not reliably available + // when the agent is loaded by the bootstrap class loader (i.e., when + // getClassLoader() returns null). In such cases, automatically disable Git + // to prevent NullPointerExceptions during initialization. + resolveProperty("appmap.debug.disableGit", Properties.class.getClassLoader() == null); public static final Boolean RecordingAuto = resolveProperty("appmap.recording.auto", false); public static final String RecordingName = resolveProperty("appmap.recording.name", (String) null); @@ -30,12 +35,12 @@ public class Properties { public static final Boolean RecordingRequests = resolveProperty("appmap.recording.requests", true); public static final String[] IgnoredPackages = resolveProperty("appmap.recording.ignoredPackages", new String[] {"java.", "jdk.", "sun."}); + public static final String[] ExcludedHooks = + resolveProperty("appmap.hooks.exclude", new String[0]); public static final String DefaultConfigFile = "appmap.yml"; public static final String ConfigFile = resolveProperty("appmap.config.file", (String) null); - public static final Integer PatternThreshold = - resolveProperty("appmap.config.patternThreshold", 10); public static final Boolean DisableValue = resolveProperty("appmap.event.disableValue", false); public static final Integer MaxValueSize = resolveProperty("appmap.event.valueSize", 1024); diff --git a/agent/src/main/java/com/appland/appmap/output/v1/Parameters.java b/agent/src/main/java/com/appland/appmap/output/v1/Parameters.java index dd3779fa..be04629e 100644 --- a/agent/src/main/java/com/appland/appmap/output/v1/Parameters.java +++ b/agent/src/main/java/com/appland/appmap/output/v1/Parameters.java @@ -7,11 +7,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javassist.bytecode.AttributeInfo; import org.tinylog.TaggedLogger; import com.appland.appmap.config.AppMapConfig; -import com.appland.appmap.config.Properties; -import com.appland.appmap.util.Logger; import javassist.CtBehavior; import javassist.CtClass; @@ -30,7 +29,7 @@ public class Parameters implements Iterable { private static final TaggedLogger logger = AppMapConfig.getLogger(null); - private final ArrayList values = new ArrayList(); + private final ArrayList values = new ArrayList<>(); public Parameters() { } @@ -48,7 +47,7 @@ public Parameters(CtBehavior behavior) { "." + behavior.getName() + methodInfo.getDescriptor(); - CtClass[] paramTypes = null; + CtClass[] paramTypes; try { paramTypes = behavior.getParameterTypes(); } catch (NotFoundException e) { @@ -71,51 +70,11 @@ public Parameters(CtBehavior behavior) { return; } - CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); - LocalVariableAttribute locals = null; - if (codeAttribute != null) { - locals = (LocalVariableAttribute) codeAttribute.getAttribute(javassist.bytecode.LocalVariableAttribute.tag); - } else { - logger.debug("No code attribute for {}", fqn); - } - + String[] paramNames = getParameterNames(methodInfo, paramTypes); int numParams = paramTypes.length; - String[] paramNames = new String[numParams]; - if (locals != null && numParams > 0) { - int numLocals = locals.tableLength(); - - // This is handy when debugging this code, but produces too much - // noise for general use. - if (Properties.DebugLocals) { - logger.debug("local variables for {}", fqn); - for (int idx = 0; idx < numLocals; idx++) { - logger.debug(" {} {} {}", idx, locals.variableName(idx), locals.index(idx)); - } - } - - // Iterate through the local variables to find the ones that match the argument slots. - // Arguments are pushed into consecutive slots, starting at 0 (for this or the first argument), - // and then incrementing by 1 for each argument, unless the argument is an unboxed long or double, - // in which case it takes up two slots. - int slot = Modifier.isStatic(behavior.getModifiers()) ? 0 : 1; // ignore `this` - for (int i = 0; i < numParams; i++) { - try { - // note that the slot index is not the same as the - // parameter index or the local variable index - paramNames[i] = locals.variableNameByIndex(slot); - } catch (Exception e) { - // the debug info might be corrupted or partial, let's not crash in this case - logger.debug(e, "Failed to get local variable name for slot {} in {}", slot, fqn); - } finally { - // note these only correspond to unboxed types — boxed double and long will still have width 1 - int width = paramTypes[i] == CtClass.doubleType || paramTypes[i] == CtClass.longType ? 2 : 1; - slot += width; - } - } - } Value[] paramValues = new Value[numParams]; - for (int i = 0; i < paramTypes.length; ++i) { + for (int i = 0; i < numParams; ++i) { // Use a real parameter name if we have it, a fake one if we // don't. String paramName = paramNames[i]; @@ -130,11 +89,61 @@ public Parameters(CtBehavior behavior) { paramValues[i] = param; } - for (int i = 0; i < paramValues.length; ++i) { - this.add(paramValues[i]); + for (Value paramValue : paramValues) { + this.add(paramValue); } } + /** + * Iterate through the LocalVariableTables to get parameter names. + * Local variable tables are debugging metadata containing information about local variables. + * Variables are organized into slots; first slots are used for parameters, then for local variables. + * + * @param methodInfo for the method + * @param paramTypes types of the parameters (used to calculate slot positions) + * @return Array of parameter names (ignoring this), with null for any names that could not be determined. + * Length of the array matches length of paramTypes. + * @see The Java Virtual Machine Specification: The LocalVariableTable Attribute + */ + private static String[] getParameterNames(MethodInfo methodInfo, CtClass[] paramTypes) { + String[] paramNames = new String[paramTypes.length]; + + CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); + if (codeAttribute != null) { + boolean isStatic = Modifier.isStatic(methodInfo.getAccessFlags()); + + // count number of slots taken by all the parameters + int slotCount = isStatic ? 0 : 1; // account for `this` + for (CtClass paramType : paramTypes) { + slotCount += (paramType == CtClass.doubleType || paramType == CtClass.longType) ? 2 : 1; + } + + String[] namesBySlot = new String[slotCount]; + + for (AttributeInfo attr : codeAttribute.getAttributes()) { + if (attr instanceof LocalVariableAttribute) { + LocalVariableAttribute localVarAttr = (LocalVariableAttribute) attr; + + for (int i = 0; i < localVarAttr.tableLength(); i++) { + int index = localVarAttr.index(i); + if (index < slotCount) { + namesBySlot[index] = localVarAttr.variableName(i); + } + } + } + } + + int slot = isStatic ? 0 : 1; // ignore `this` + for (int i = 0; i < paramTypes.length; i++) { + paramNames[i] = namesBySlot[slot]; + int width = paramTypes[i] == CtClass.doubleType || paramTypes[i] == CtClass.longType ? 2 : 1; + slot += width; + } + } + + return paramNames; + } + /** * Get an iterator for each {@link Value}. * @return A {@link Value} iterator @@ -172,26 +181,16 @@ public int size() { return this.values.size(); } - /** - * Clears the internal value array. - */ - public void clear() { - this.values.clear(); - } - - /** - * Gets a {@Value} object stored by this Parameters object by name/identifier. + * Gets a {@link Value} object stored by this Parameters object by name/identifier. * @param name The name or identifier of the @{link Value} to be returned * @return The {@link Value} object found - * @throws NoSuchElementException If no @{link Value} object is found + * @throws NoSuchElementException If no {@link Value} object is found */ public Value get(String name) throws NoSuchElementException { - if (this.values != null) { - for (Value param : this.values) { - if (param.name.equals(name)) { - return param; - } + for (Value param : this.values) { + if (param.name.equals(name)) { + return param; } } @@ -199,16 +198,12 @@ public Value get(String name) throws NoSuchElementException { } /** - * Gets a {@Value} object stored by this Parameters object by index. + * Gets a {@link Value} object stored by this Parameters object by index. * @param index The index of the @{link Value} to be returned * @return The {@link Value} object at the given index - * @throws NoSuchElementException if no @{link Value} object is found at the given index + * @throws NoSuchElementException if no {@link Value} object is found at the given index */ public Value get(Integer index) throws NoSuchElementException { - if (this.values == null) { - throw new NoSuchElementException(); - } - try { return this.values.get(index); } catch (NullPointerException | IndexOutOfBoundsException e) { @@ -233,10 +228,10 @@ public Boolean validate(Integer index, String type) { } /** - * Performs a deep copy of the Parameters object and all of its values. + * Creates a copy of the parameters object with the value types, kinds and names preserved. * @return A new Parameters object */ - public Parameters clone() { + public Parameters freshCopy() { Parameters clonedParams = new Parameters(); for (Value param : this.values) { clonedParams.add(new Value(param)); diff --git a/agent/src/main/java/com/appland/appmap/process/hooks/SqlQuery.java b/agent/src/main/java/com/appland/appmap/process/hooks/SqlQuery.java index 343ee265..51bd7efa 100644 --- a/agent/src/main/java/com/appland/appmap/process/hooks/SqlQuery.java +++ b/agent/src/main/java/com/appland/appmap/process/hooks/SqlQuery.java @@ -1,9 +1,12 @@ package com.appland.appmap.process.hooks; import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import com.appland.appmap.output.v1.Event; import com.appland.appmap.record.Recorder; @@ -21,342 +24,278 @@ public class SqlQuery { private static final Recorder recorder = Recorder.getInstance(); - // ================================================================================================ - // Calls - // ================================================================================================ - public static void recordSql(Event event, String databaseType, String sql) { event.setSqlQuery(databaseType, sql); event.setParameters(null); recorder.add(event); } - private static boolean isMock(Object o) { - final Class c = o.getClass(); - final Package p = c.getPackage(); - if (p == null) { - // If there's no package info, it's not a Mockito object. - return false; - } - - return p.getName().startsWith("org.mockito"); - } + private static final Map databases = Collections.synchronizedMap(new WeakHashMap()); private static String getDbName(Connection c) { - String dbname = ""; if (c == null) { - return dbname; + return null; + } + if (databases.containsKey(c)) { + return databases.get(c); } + String dbname = null; try { - DatabaseMetaData metadata; - if (isMock(c) || isMock(metadata = c.getMetaData())) { - return "[mocked]"; - } - - dbname = metadata.getDatabaseProductName(); - } catch (SQLException e) { + dbname = c.getMetaData().getDatabaseProductName(); + } catch (Throwable e) { Logger.println("WARNING, failed to get database name"); e.printStackTrace(System.err); + // fall through and put null to ensure we don't try again } + databases.put(c, dbname); return dbname; } private static String getDbName(Statement s) { - String dbname = ""; if (s == null) { - return dbname; + return null; + } + if (databases.containsKey(s)) { + return databases.get(s); } + String dbname = null; try { - if (isMock(s)) { - return "[mocked]"; - } - dbname = getDbName(s.getConnection()); - } catch (SQLException e) { + } catch (Throwable e) { Logger.println("WARNING, failed to get statement's connection"); e.printStackTrace(System.err); + // fall through and put null to ensure we don't try again } + databases.put(s, dbname); return dbname; } - public static void recordSql(Event event, Connection c, String sql) { - recordSql(event, getDbName(c), sql); - } - public static void recordSql(Event event, Statement s, String sql) { recordSql(event, getDbName(s), sql); } - @HookClass("java.sql.Connection") - public static void nativeSQL(Event event, Connection c, String sql) { - recordSql(event, c, sql); - } - - @HookClass("java.sql.Connection") - public static void prepareCall(Event event, Connection c, String sql) { - recordSql(event, c, sql); + public static void recordSql(Event event, Statement s, Object args[]) { + recordSql(event, getDbName(s), getSql(s, args)); } - @HookClass("java.sql.Connection") - public static void prepareCall(Event event, Connection c, String sql, int resultSetType, int resultSetConcurrency) { - recordSql(event, c, sql); - } + private static Map statements = Collections.synchronizedMap(new WeakHashMap()); - @HookClass("java.sql.Connection") - public static void prepareCall(Event event, Connection c, String sql, int resultSetType, int resultSetConcurrency, - int resultSetHoldability) { - recordSql(event, c, sql); + /** + * Get the SQL string based on the arguments or the prepared statement. + * + * If the first argument is a string, it is returned. + * If the statement is a prepared statement, the SQL string is returned. + * Otherwise, the last resort is to return "-- [unknown sql]". + * + * @param s The statement + * @param args The arguments + * @return The SQL string + */ + private static String getSql(Statement s, Object args[]) { + if (args.length > 0 && args[0] instanceof String) { + return (String) args[0]; + } + String sql = statements.get(s); + if (sql == null) { + // last resort, shouldn't happen + return "-- [unknown sql]"; + } + return sql; } - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql) { - recordSql(event, c, sql); - } + // ================================================================================================ + // Preparing calls and statements + // ================================================================================================ - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql, int autoGeneratedKeys) { - recordSql(event, c, sql); + @ArgumentArray + @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) + public static void prepareCall(Event event, Connection c, Object returnValue, Object[] args) { + databases.put(returnValue, getDbName(c)); + if (args.length > 0 && args[0] instanceof String) { + statements.put(returnValue, (String) args[0]); + } } - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql, int[] columnIndexes) { - recordSql(event, c, sql); + @ArgumentArray + @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) + public static void prepareStatement(Event event, Connection c, Object returnValue, Object[] args) { + databases.put(returnValue, getDbName(c)); + if (args.length > 0 && args[0] instanceof String) { + statements.put(returnValue, (String) args[0]); + } } - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql, int resultSetType, - int resultSetConcurrency) { - recordSql(event, c, sql); - } + // ================================================================================================ + // Batch manipulation + // ================================================================================================ - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql, int resultSetType, - int resultSetConcurrency, int resultSetHoldability) { - recordSql(event, c, sql); - } + private static final Map> batchStatements = new WeakHashMap<>(); - @HookClass("java.sql.Connection") - public static void prepareStatement(Event event, Connection c, String sql, String[] columnNames) { - recordSql(event, c, sql); + /** + * Pop the batch statements for the given statement. + * The batch statements are joined with ";\n". + * + * Note that this will remove the batch statements from the map. + * + * @param s The statement + * @return The batch statements + */ + private static String popBatchStatements(Statement s) { + synchronized (batchStatements) { + List statements = batchStatements.remove(s); + if (statements == null) { + return ""; + } + return String.join(";\n", statements); + } } + @ArgumentArray @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void addBatch(Event event, Statement s, String sql) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void execute(Event event, Statement s, String sql) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void execute(Event event, Statement s, String sql, int autoGeneratedKeys) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void execute(Event event, Statement s, String sql, int[] columnIndexes) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void execute(Event event, Statement s, String sql, String[] columnNames) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void executeQuery(Event event, Statement s, String sql) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void executeUpdate(Event event, Statement s, String sql) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void executeUpdate(Event event, Statement s, String sql, int autoGeneratedKeys) { - recordSql(event, s, sql); - } - - @HookClass("java.sql.Statement") - public static void executeUpdate(Event event, Statement s, String sql, int[] columnIndexes) { - recordSql(event, s, sql); + public static void addBatch(Event event, Statement s, Object returnValue, Object[] args) { + String sql = getSql(s, args); + synchronized (batchStatements) { + batchStatements.computeIfAbsent(s, k -> new ArrayList<>()).add(sql); + } } - @HookClass("java.sql.Statement") - public static void executeUpdate(Event event, Statement s, String sql, String[] columnNames) { - recordSql(event, s, sql); + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) + public static void clearBatch(Event event, Statement s, Object returnValue) { + synchronized (batchStatements) { + batchStatements.remove(s); + } } // ================================================================================================ - // Returns + // Statement.executeBatch // ================================================================================================ - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void nativeSQL(Event event, Connection c, Object returnValue, String sql) { - recorder.add(event); - } - - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareCall(Event event, Connection c, Object returnValue, String sql) { - recorder.add(event); - } - - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareCall(Event event, Connection c, Object returnValue, String sql, int resultSetType, - int resultSetConcurrency) { - recorder.add(event); - } - - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareCall(Event event, Connection c, Object returnValue, String sql, int resultSetType, - int resultSetConcurrency, int resultSetHoldability) { - recorder.add(event); - } - - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql) { - recorder.add(event); - } - - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql, - int autoGeneratedKeys) { - recorder.add(event); + @HookClass(value = "java.sql.Statement") + public static void executeBatch(Event event, Statement s) { + recordSql(event, s, popBatchStatements(s)); } - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql, int[] columnIndexes) { + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) + public static void executeBatch(Event event, Statement s, Object returnValue) { recorder.add(event); } - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql, int resultSetType, - int resultSetConcurrency) { + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) + public static void executeBatch(Event event, Statement s, Throwable exception) { + event.setException(exception); recorder.add(event); } - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql, int resultSetType, - int resultSetConcurrency, int resultSetHoldability) { - recorder.add(event); - } + // ================================================================================================ + // Statement.executeLargeBatch + // ================================================================================================ - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_RETURN) - public static void prepareStatement(Event event, Connection c, Object returnValue, String sql, String[] columnNames) { - recorder.add(event); + @HookClass(value = "java.sql.Statement") + public static void executeLargeBatch(Event event, Statement s) { + recordSql(event, s, popBatchStatements(s)); } @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void addBatch(Event event, Statement s, Object returnValue, String sql) { + public static void executeLargeBatch(Event event, Statement s, Object returnValue) { recorder.add(event); } - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void execute(Event event, Statement s, Object returnValue, String sql) { + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) + public static void executeLargeBatch(Event event, Statement s, Throwable exception) { + event.setException(exception); recorder.add(event); } - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void execute(Event event, Statement s, Object returnValue, String sql, int autoGeneratedKeys) { - recorder.add(event); - } + // ================================================================================================ + // Statement.execute + // ================================================================================================ - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void execute(Event event, Statement s, Object returnValue, String sql, int[] columnIndexes) { - recorder.add(event); + @HookClass("java.sql.Statement") + @ArgumentArray + public static void execute(Event event, Statement s, Object[] args) { + recordSql(event, s, args); } + @ArgumentArray @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void execute(Event event, Statement s, Object returnValue, String sql, String[] columnNames) { + public static void execute(Event event, Statement s, Object returnValue, Object[] args) { recorder.add(event); } - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void executeQuery(Event event, Statement s, Object returnValue, String sql) { + @ArgumentArray + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) + public static void execute(Event event, Statement s, Throwable exception, Object[] args) { + event.setException(exception); recorder.add(event); } - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void executeUpdate(Event event, Statement s, Object returnValue, String sql) { - recorder.add(event); - } + // ================================================================================================ + // Statement.executeQuery + // ================================================================================================ - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void executeUpdate(Event event, Statement s, Object returnValue, String sql, int autoGeneratedKeys) { - recorder.add(event); + @ArgumentArray + @HookClass("java.sql.Statement") + public static void executeQuery(Event event, Statement s, Object[] args) { + recordSql(event, s, args); } + @ArgumentArray @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void executeUpdate(Event event, Statement s, Object returnValue, String sql, int[] columnIndexes) { + public static void executeQuery(Event event, Statement s, Object returnValue, Object[] args) { recorder.add(event); } - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) - public static void executeUpdate(Event event, Statement s, Object returnValue, String sql, String[] columnNames) { + @ArgumentArray + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) + public static void executeQuery(Event event, Statement s, Throwable exception, Object[] args) { + event.setException(exception); recorder.add(event); } // ================================================================================================ - // Exceptions + // Statement.executeUpdate // ================================================================================================ - /* - * Many of the methods below are overloaded. However, the hook implementations - * don't make use of the arguments passed to the original method. So, take - * advantage of ArgumentArray's "feature" that causes it to match all - * overloaded mehods by name, and have the hook apply to each of them. - */ - - @ArgumentArray - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void nativeSQL(Event event, Connection c, Throwable exception, Object[] args) { - event.setException(exception); - recorder.add(event); - } - @ArgumentArray - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void prepareCall(Event event, Connection c, Throwable exception, Object[] args) { - event.setException(exception); - recorder.add(event); + @HookClass("java.sql.Statement") + public static void executeUpdate(Event event, Statement s, Object args[]) { + recordSql(event, s, args); } @ArgumentArray - @HookClass(value = "java.sql.Connection", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void prepareStatement(Event event, Connection c, Throwable exception, Object[] args) { - event.setException(exception); + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) + public static void executeUpdate(Event event, Statement s, Object returnValue, Object[] args) { recorder.add(event); } @ArgumentArray @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void addBatch(Event event, Statement s, Throwable exception, Object[] args) { + public static void executeUpdate(Event event, Statement s, Throwable exception, Object[] args) { event.setException(exception); recorder.add(event); } + // ================================================================================================ + // Statement.executeLargeUpdate + // ================================================================================================ + @ArgumentArray - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void execute(Event event, Statement s, Throwable exception, Object[] args) { - event.setException(exception); - recorder.add(event); + @HookClass("java.sql.Statement") + public static void executeLargeUpdate(Event event, Statement s, Object args[]) { + recordSql(event, s, args); } @ArgumentArray - @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void executeQuery(Event event, Statement s, Throwable exception, Object[] args) { - event.setException(exception); + @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_RETURN) + public static void executeLargeUpdate(Event event, Statement s, Object returnValue, Object[] args) { recorder.add(event); } @ArgumentArray @HookClass(value = "java.sql.Statement", methodEvent = MethodEvent.METHOD_EXCEPTION) - public static void executeUpdate(Event event, Statement s, Throwable exception, Object[] args) { + public static void executeLargeUpdate(Event event, Statement s, Throwable exception, Object[] args) { event.setException(exception); recorder.add(event); } diff --git a/agent/src/main/java/com/appland/appmap/process/hooks/remoterecording/ServletRequest.java b/agent/src/main/java/com/appland/appmap/process/hooks/remoterecording/ServletRequest.java index 25bdd7ec..a058fdc3 100644 --- a/agent/src/main/java/com/appland/appmap/process/hooks/remoterecording/ServletRequest.java +++ b/agent/src/main/java/com/appland/appmap/process/hooks/remoterecording/ServletRequest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import com.appland.appmap.record.Recording; import com.appland.appmap.reflect.HttpServletRequest; @@ -29,8 +30,8 @@ public void setStatus(int status) { } public void writeJson(String responseJson) throws IOException { - res.setContentType("application/json"); - res.setContentLength(responseJson.length()); + res.setContentType("application/json; charset=UTF-8"); + res.setContentLength(responseJson.getBytes(StandardCharsets.UTF_8).length); res.setStatus(HttpServletResponse.SC_OK); PrintWriter writer = res.getWriter(); @@ -39,7 +40,7 @@ public void writeJson(String responseJson) throws IOException { } public void writeRecording(Recording recording) throws IOException { - res.setContentType("application/json"); + res.setContentType("application/json; charset=UTF-8"); res.setContentLength(recording.size()); recording.readFully(true, res.getWriter()); } diff --git a/agent/src/main/java/com/appland/appmap/record/AppMapSerializer.java b/agent/src/main/java/com/appland/appmap/record/AppMapSerializer.java index 62f08ecb..7d7959d8 100644 --- a/agent/src/main/java/com/appland/appmap/record/AppMapSerializer.java +++ b/agent/src/main/java/com/appland/appmap/record/AppMapSerializer.java @@ -17,7 +17,7 @@ /** * Writes AppMap data to JSON. */ -public class AppMapSerializer { +public class AppMapSerializer implements AutoCloseable { public static class FileSections { public static final String Version = "version"; public static final String Metadata = "metadata"; @@ -37,10 +37,13 @@ private class SectionInfo { } private final JSONWriter json; + private final Writer underlyingWriter; private SectionInfo currentSection = null; private final HashSet sectionsWritten = new HashSet(); + private boolean closed = false; private AppMapSerializer(Writer writer) { + this.underlyingWriter = writer; this.json = new JSONWriter(writer); // The eventUpdates object contains Event objects that are also in the // events array. Setting DisableCircularReferenceDetect tells fastjson that @@ -73,7 +76,7 @@ public void write(CodeObjectTree classMap, Metadata metadata, Map updates) throws IOException { } /** - * Closes outstanding JSON objects and closes the writer. + * Closes outstanding JSON objects and closes the underlying writer. Safe to call multiple times. * @throws IOException If a writer error occurs */ - private void finish() throws IOException { - this.setCurrentSection("EOF", ""); - this.json.endObject(); - this.json.close(); + @Override + public void close() throws IOException { + if (!this.closed) { + try { + this.setCurrentSection("EOF", ""); + this.json.endObject(); + this.json.close(); + } finally { + // JSONWriter.close() does not close the underlying writer, so we must do it explicitly + // Always close the underlying writer, even if JSON finalization fails + this.underlyingWriter.close(); + this.closed = true; + } + } } } diff --git a/agent/src/main/java/com/appland/appmap/record/Recording.java b/agent/src/main/java/com/appland/appmap/record/Recording.java index 649b37d2..b66daacb 100644 --- a/agent/src/main/java/com/appland/appmap/record/Recording.java +++ b/agent/src/main/java/com/appland/appmap/record/Recording.java @@ -5,11 +5,12 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -100,7 +101,7 @@ public Path moveTo(Path targetPath) { } public void readFully(boolean delete, Writer writer) throws IOException { - try (final Reader reader = new FileReader(this.file)) { + try (final Reader reader = new InputStreamReader(new FileInputStream(this.file), StandardCharsets.UTF_8)) { char[] buffer = new char[2048]; int bytesRead; while ((bytesRead = reader.read(buffer)) != -1) { diff --git a/agent/src/main/java/com/appland/appmap/record/RecordingSession.java b/agent/src/main/java/com/appland/appmap/record/RecordingSession.java index 583177fd..7e99fcf3 100644 --- a/agent/src/main/java/com/appland/appmap/record/RecordingSession.java +++ b/agent/src/main/java/com/appland/appmap/record/RecordingSession.java @@ -1,12 +1,14 @@ package com.appland.appmap.record; import java.io.File; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.Writer; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -92,22 +94,19 @@ public synchronized Recording checkpoint() { // By using RandomAccessFile we can erase that character. // If we don't let the JSON writer write the "begin object" token, it refuses // to do anything else properly either. - RandomAccessFile raf = new RandomAccessFile(targetPath.toFile(), "rw"); - Writer fw = new OutputStreamWriter(new OutputStream() { - @Override - public void write(int b) throws IOException { - raf.write(b); + try (RandomAccessFile raf = new RandomAccessFile(targetPath.toFile(), "rw")) { + Writer fw = new OutputStreamWriter(Channels.newOutputStream(raf.getChannel()), StandardCharsets.UTF_8); + raf.seek(targetPath.toFile().length()); + + if (eventReceived) { + fw.write("],"); } - }); - raf.seek(targetPath.toFile().length()); + fw.flush(); - if ( eventReceived ) { - fw.write("],"); + try (AppMapSerializer checkpointSerializer = AppMapSerializer.reopen(fw, raf)) { + checkpointSerializer.write(this.getClassMap(), this.metadata, this.eventUpdates); + } } - fw.flush(); - - AppMapSerializer serializer = AppMapSerializer.reopen(fw, raf); - serializer.write(this.getClassMap(), this.metadata, this.eventUpdates); } catch (IOException e) { throw new RuntimeException(e); } @@ -123,16 +122,22 @@ public synchronized Recording stop() { throw new IllegalStateException("AppMap: Unable to stop the recording because no recording is in progress."); } + File file = this.tmpPath.toFile(); try { this.serializer.write(this.getClassMap(), this.metadata, this.eventUpdates); } catch (IOException e) { throw new RuntimeException(e); + } finally { + // Ensure serializer is closed even if write() throws an exception + try { + this.serializer.close(); + } catch (IOException e) { + logger.error("Failed to close serializer", e); + } + this.serializer = null; + this.tmpPath = null; } - File file = this.tmpPath.toFile(); - this.serializer = null; - this.tmpPath = null; - logger.debug("Recording finished"); logger.debug("Wrote recording to file {}", file.getPath()); @@ -162,7 +167,10 @@ void start() { try { this.tmpPath = Files.createTempFile(null, ".appmap.json"); this.tmpPath.toFile().deleteOnExit(); - this.serializer = AppMapSerializer.open(new FileWriter(this.tmpPath.toFile())); + // Note this output stream will be closed by writer is closed by the serializer + FileOutputStream fileOutputStream = new FileOutputStream(this.tmpPath.toFile()); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); + this.serializer = AppMapSerializer.open(writer); } catch (IOException e) { this.tmpPath = null; this.serializer = null; diff --git a/agent/src/main/java/com/appland/appmap/transform/ClassFileTransformer.java b/agent/src/main/java/com/appland/appmap/transform/ClassFileTransformer.java index 34764ffc..774a610d 100644 --- a/agent/src/main/java/com/appland/appmap/transform/ClassFileTransformer.java +++ b/agent/src/main/java/com/appland/appmap/transform/ClassFileTransformer.java @@ -153,7 +153,21 @@ private Hook[] getHooks(String methodId) { return methodHooks != null ? methodHooks : sortedUnkeyedHooks; } + private boolean isExcludedHook(String className) { + for (String excluded : Properties.ExcludedHooks) { + if (className.equals(excluded)) { + return true; + } + } + return false; + } + private void processClass(CtClass ctClass) { + if (isExcludedHook(ctClass.getName())) { + logger.debug("excluding hook class {}", ctClass.getName()); + return; + } + boolean traceClass = tracePrefix == null || ctClass.getName().startsWith(tracePrefix); if (traceClass) { @@ -189,9 +203,7 @@ private void processClass(CtClass ctClass) { } } - private boolean applyHooks(CtBehavior behavior) { - boolean traceClass = tracePrefix == null || behavior.getDeclaringClass().getName().startsWith(tracePrefix); - + private Set applyHooks(CtBehavior behavior, boolean traceClass) { try { List hookSites = getHookSites(behavior); @@ -199,37 +211,16 @@ private boolean applyHooks(CtBehavior behavior) { if (traceClass) { logger.trace("no hook sites"); } - return false; + return java.util.Collections.emptySet(); } - Hook.apply(behavior, hookSites); - - if (logger.isDebugEnabled()) { - for (HookSite hookSite : hookSites) { - final Hook hook = hookSite.getHook(); - String className = behavior.getDeclaringClass().getName(); - if (tracePrefix != null && !className.startsWith(tracePrefix)) { - continue; - } - - if (traceClass) { - logger.trace("hooked {}.{}{} on ({},{}) with {}", - className, - behavior.getName(), - behavior.getMethodInfo().getDescriptor(), - hook.getMethodEvent().getEventString(), - hook.getPosition(), - hook); - } - } - } - return true; + return Hook.apply(behavior, hookSites, traceClass); } catch (NoSourceAvailableException e) { Logger.println(e); } - return false; + return java.util.Collections.emptySet(); } public List getHookSites(CtBehavior behavior) { @@ -292,7 +283,7 @@ public byte[] transform(ClassLoader loader, try { ClassPool classPool = AppMapClassPool.get(); if (traceClass) { - logger.debug("className: {}", className); + logger.trace("className: {}", className); } ctClass = classPool.makeClass(new ByteArrayInputStream(bytes)); @@ -317,7 +308,8 @@ public byte[] transform(ClassLoader loader, return null; } - boolean hookApplied = false; + boolean bytecodeModified = false; + boolean needsByteBuddy = false; for (CtBehavior behavior : ctClass.getDeclaredBehaviors()) { if (traceClass) { logger.trace("behavior: {}", behavior.getLongName()); @@ -331,24 +323,27 @@ public byte[] transform(ClassLoader loader, } methodsExamined++; - if (this.applyHooks(behavior)) { - hookApplied = true; + Set actions = this.applyHooks(behavior, traceClass); + if (!actions.isEmpty()) { + bytecodeModified = true; methodsHooked++; + if (actions.contains(Hook.ApplicationAction.MARKED)) { + needsByteBuddy = true; + } } } - if (hookApplied) { - // One or more of the methods in the the class were marked as needing to + if (bytecodeModified) { + // One or more of the methods in the class were marked as needing to // be instrumented. Mark the class so the bytebuddy transformer will // know it needs to be instrumented. - ClassFile classFile = ctClass.getClassFile(); - ConstPool constPool = classFile.getConstPool(); - Annotation annot = new Annotation(AppMapInstrumented.class.getName(), constPool); - AnnotationUtil.setAnnotation(new AnnotatedClass(ctClass), annot); - - if (traceClass) { - logger.trace("hooks applied to {}", className); + if (needsByteBuddy) { + ClassFile classFile = ctClass.getClassFile(); + ConstPool constPool = classFile.getConstPool(); + Annotation annot = new Annotation(AppMapInstrumented.class.getName(), constPool); + AnnotationUtil.setAnnotation(new AnnotatedClass(ctClass), annot); } + if (logger.isDebugEnabled()) { packagesHooked.compute(ctClass.getPackageName(), (k, v) -> v == null ? 1 : v + 1); } diff --git a/agent/src/main/java/com/appland/appmap/transform/annotations/Hook.java b/agent/src/main/java/com/appland/appmap/transform/annotations/Hook.java index 3ff0e28e..5a695ef4 100644 --- a/agent/src/main/java/com/appland/appmap/transform/annotations/Hook.java +++ b/agent/src/main/java/com/appland/appmap/transform/annotations/Hook.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -73,7 +74,7 @@ public void buildParameters() { } public Parameters getRuntimeParameters(HookBinding binding) { - Parameters runtimeParameters = this.staticParameters.clone(); + Parameters runtimeParameters = this.staticParameters.freshCopy(); Stream.concat(Stream.of(this.sourceSystem), this.optionalSystems.stream()) .sorted(Comparator.comparingInt(ISystem::getParameterPriority)) .forEach(system -> { @@ -107,25 +108,72 @@ public HookSite prepare(CtBehavior targetBehavior, Map hookConte return new HookSite(this, behaviorOrdinal, binding); } - public static void apply(CtBehavior targetBehavior, List hookSites) { + public enum ApplicationAction { + MARKED, + INSTRUMENTED + } + + // We only log the first exception to avoid flooding the logs at the debug level. + // Note this variable is not thread safe, but this is okay; the worst that can happen is + // that we log more than one exception in a multi-threaded scenario. + private static boolean exceptionLogged = false; + + public static Set apply(CtBehavior targetBehavior, List hookSites, boolean traceClass) { + Set actions = EnumSet.noneOf(ApplicationAction.class); MethodInfo methodInfo = targetBehavior.getMethodInfo(); AnnotationsAttribute attr = (AnnotationsAttribute)methodInfo.getAttribute(AnnotationsAttribute.visibleTag); - // If the behavior is marked as an app method, update the annotation with - // the behavior ordinals so the bytebuddy transformer can instrument it. - if (attr.getAnnotation(AppMapAppMethod.class.getName()) != null) { - setBehaviorOrdinals(targetBehavior, hookSites); - } + if (attr != null) { + // If the behavior is marked as an app method, update the annotation with + // the behavior ordinals so the bytebuddy transformer can instrument it. + if (attr.getAnnotation(AppMapAppMethod.class.getName()) != null) { + setBehaviorOrdinals(targetBehavior, hookSites); + actions.add(ApplicationAction.MARKED); + if (traceClass) { + logger.debug("tracing {}.{}{}", + targetBehavior.getDeclaringClass().getName(), + targetBehavior.getName(), + targetBehavior.getMethodInfo().getDescriptor()); + } + } - // If it's (also) marked as an agent method, it needs to be instrumented - // by javassist. - if (attr.getAnnotation(AppMapAgentMethod.class.getName()) != null) { - instrument(targetBehavior, hookSites); + // If it's (also) marked as an agent method, it needs to be instrumented + // by javassist. + if (attr.getAnnotation(AppMapAgentMethod.class.getName()) != null) { + try { + instrument(targetBehavior, hookSites); + actions.add(ApplicationAction.INSTRUMENTED); + if (traceClass) { + String hooks = hookSites.stream() + .map(h -> h.getHook().toString()) + .collect(Collectors.joining(", ")); + logger.debug("{}.{}{} instrumented with hooks: {}", + targetBehavior.getDeclaringClass().getName(), + targetBehavior.getName(), + targetBehavior.getMethodInfo().getDescriptor(), + hooks); + } + } catch (CannotCompileException | NotFoundException e) { + String msg = String.format("failed to instrument %s.%s: %s", + targetBehavior.getDeclaringClass().getName(), targetBehavior.getName(), e.getMessage()); + if (!exceptionLogged) { + logger.debug(e, msg); + exceptionLogged = true; + } else { + // Log at trace level after the first one to avoid flooding the debug logs + logger.trace(e, msg); + logger.debug(msg); + } + } + } } + + return actions; } - public static void instrument(CtBehavior targetBehavior, List hookSites) { + public static void instrument(CtBehavior targetBehavior, List hookSites) + throws CannotCompileException, NotFoundException { final CtClass returnType = getReturnType(targetBehavior); final Boolean returnsVoid = (returnType == CtClass.voidType); @@ -150,44 +198,36 @@ public static void instrument(CtBehavior targetBehavior, List hookSite } - try { - String beforeSrcBlock = beforeSrcBlock(uniqueLocks.toString(), - invocations[MethodEvent.METHOD_INVOCATION.getIndex()]); - logger.trace("{}: beforeSrcBlock:\n{}", targetBehavior::getName, beforeSrcBlock::toString); - targetBehavior.insertBefore( - beforeSrcBlock); - - String afterSrcBlock = afterSrcBlock(invocations[MethodEvent.METHOD_RETURN.getIndex()]); - logger.trace("{}: afterSrcBlock:\n{}", targetBehavior::getName, afterSrcBlock::toString); - - targetBehavior.insertAfter( - afterSrcBlock); - - ClassPool cp = AppMapClassPool.get(); - String exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return;}"; - if (returnsVoid) { - targetBehavior.addCatch(exitEarlyCatchSrc, - cp.get("com.appland.appmap.process.ExitEarly")); - } else if (!returnType.isPrimitive()) { - exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return(" - + returnType.getName() + ")$e.getReturnValue();}"; - targetBehavior - .addCatch(exitEarlyCatchSrc, cp.get("com.appland.appmap.process.ExitEarly")); - } - logger.trace("{}: catch1Src:\n{}", targetBehavior::getName, exitEarlyCatchSrc::toString); - - String catchSrcBlock = catchSrcBlock(invocations[MethodEvent.METHOD_EXCEPTION.getIndex()]); - targetBehavior.addCatch( - catchSrcBlock, - cp.get("java.lang.Throwable")); - logger.trace("{}: catchSrcBlock:\n{}", targetBehavior::getName, catchSrcBlock::toString); - - } catch (CannotCompileException e) { - logger.debug(e, "failed to compile {}.{}", targetBehavior.getDeclaringClass().getName(), - targetBehavior.getName()); - } catch (NotFoundException e) { - logger.debug(e); + String beforeSrcBlock = beforeSrcBlock(uniqueLocks.toString(), + invocations[MethodEvent.METHOD_INVOCATION.getIndex()]); + logger.trace("{}: beforeSrcBlock:\n{}", targetBehavior::getName, beforeSrcBlock::toString); + targetBehavior.insertBefore( + beforeSrcBlock); + + String afterSrcBlock = afterSrcBlock(invocations[MethodEvent.METHOD_RETURN.getIndex()]); + logger.trace("{}: afterSrcBlock:\n{}", targetBehavior::getName, afterSrcBlock::toString); + + targetBehavior.insertAfter( + afterSrcBlock); + + ClassPool cp = AppMapClassPool.get(); + String exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return;}"; + if (returnsVoid) { + targetBehavior.addCatch(exitEarlyCatchSrc, + cp.get("com.appland.appmap.process.ExitEarly")); + } else if (!returnType.isPrimitive()) { + exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return(" + + returnType.getName() + ")$e.getReturnValue();}"; + targetBehavior + .addCatch(exitEarlyCatchSrc, cp.get("com.appland.appmap.process.ExitEarly")); } + logger.trace("{}: catch1Src:\n{}", targetBehavior::getName, exitEarlyCatchSrc::toString); + + String catchSrcBlock = catchSrcBlock(invocations[MethodEvent.METHOD_EXCEPTION.getIndex()]); + targetBehavior.addCatch( + catchSrcBlock, + cp.get("java.lang.Throwable")); + logger.trace("{}: catchSrcBlock:\n{}", targetBehavior::getName, catchSrcBlock::toString); } private static void setBehaviorOrdinals(CtBehavior behavior, diff --git a/agent/src/main/java/com/appland/appmap/util/PrefixTrie.java b/agent/src/main/java/com/appland/appmap/util/PrefixTrie.java new file mode 100644 index 00000000..bf19d45e --- /dev/null +++ b/agent/src/main/java/com/appland/appmap/util/PrefixTrie.java @@ -0,0 +1,64 @@ +package com.appland.appmap.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * A simple Trie (Prefix Tree) for efficient prefix-based string matching. + * This is used to check if a class name matches any of the exclusion patterns. + */ +public class PrefixTrie { + private static class TrieNode { + Map children = new HashMap<>(); + boolean isEndOfWord = false; + } + + private final TrieNode root; + + public PrefixTrie() { + root = new TrieNode(); + } + + /** + * Inserts a word into the Trie. + * @param word The word to insert. + */ + public void insert(String word) { + if (word == null) { + return; + } + TrieNode current = root; + for (char ch : word.toCharArray()) { + current = current.children.computeIfAbsent(ch, c -> new TrieNode()); + } + current.isEndOfWord = true; + } + + /** + * Checks if any prefix of the given word exists in the Trie. + * For example, if "java." is in the Trie, this will return true for "java.lang.String". + * @param word The word to check. + * @return {@code true} if a prefix of the word is found in the Trie, {@code false} otherwise. + */ + public boolean startsWith(String word) { + if (word == null) { + return false; + } + TrieNode current = root; + for (int i = 0; i < word.length(); i++) { + if (current.isEndOfWord) { + // We've found a stored pattern that is a prefix of the word. + // e.g., Trie has "java." and word is "java.lang.String" + return true; + } + char ch = word.charAt(i); + current = current.children.get(ch); + if (current == null) { + return false; // No prefix match + } + } + // The word itself is a prefix or an exact match for a pattern in the Trie + // e.g., Trie has "java.lang" and word is "java.lang" + return current.isEndOfWord; + } +} diff --git a/agent/src/main/java/com/appland/appmap/util/tinylog/AppMapConfigurationLoader.java b/agent/src/main/java/com/appland/appmap/util/tinylog/AppMapConfigurationLoader.java index defb275e..d99cb64a 100644 --- a/agent/src/main/java/com/appland/appmap/util/tinylog/AppMapConfigurationLoader.java +++ b/agent/src/main/java/com/appland/appmap/util/tinylog/AppMapConfigurationLoader.java @@ -16,7 +16,7 @@ public class AppMapConfigurationLoader implements ConfigurationLoader { @Override - public Properties load() throws IOException { + public Properties load() { Properties properties = new Properties(); final File localConfigFile = new File("appmap-log.local.properties"); final String[] configFiles = {"appmap-log.properties", localConfigFile.getName()}; @@ -28,6 +28,8 @@ public Properties load() throws IOException { if (stream != null) { properties.load(stream); } + } catch (IOException e) { + InternalLogger.log(Level.ERROR, e, "Failed to load " + configFile + " from classloader " + cl); } } } diff --git a/agent/src/test/java/com/appland/appmap/config/AppMapConfigTest.java b/agent/src/test/java/com/appland/appmap/config/AppMapConfigTest.java index f655ac4d..fef83891 100644 --- a/agent/src/test/java/com/appland/appmap/config/AppMapConfigTest.java +++ b/agent/src/test/java/com/appland/appmap/config/AppMapConfigTest.java @@ -118,6 +118,19 @@ public void loadPackagesKeyWithScalarValue() throws Exception { String actualErr = tapSystemErr(() -> AppMapConfig.load(configFile, false)); assertTrue(actualErr.contains("AppMap: encountered syntax error in appmap.yml")); } -} + @Test + public void loadEmptyExcludeField() throws Exception { + Path configFile = tmpdir.resolve("appmap.yml"); + final String contents = "name: test\npackages:\n- path: com.example\n exclude:\n"; + Files.write(configFile, contents.getBytes()); + + AppMapConfig config = AppMapConfig.load(configFile, false); + assertNotNull(config); + assertEquals(1, config.packages.length); + assertEquals("com.example", config.packages[0].path); + assertNotNull(config.packages[0].exclude); + assertEquals(0, config.packages[0].exclude.length); + } +} diff --git a/agent/src/test/java/com/appland/appmap/config/AppMapPackageTest.java b/agent/src/test/java/com/appland/appmap/config/AppMapPackageTest.java index eea6fdaf..67e1906c 100644 --- a/agent/src/test/java/com/appland/appmap/config/AppMapPackageTest.java +++ b/agent/src/test/java/com/appland/appmap/config/AppMapPackageTest.java @@ -1,14 +1,18 @@ package com.appland.appmap.config; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.appland.appmap.util.FullyQualifiedName; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -103,4 +107,583 @@ public void testLoadConfig() throws Exception { assertNotNull(appMapPackage.methods); } } + + @Nested + class ExcludeModeTests { + @Nested + class BasicMatching { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testMatchesMethodInPackage() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Foo", false, "bar"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match methods in the configured package"); + // In exclude mode, a new LabelConfig() is returned which has an empty array for + // labels + assertNotNull(result.getLabels(), "Labels should be non-null"); + assertEquals(0, result.getLabels().length, "Should have no labels in exclude mode"); + } + + @Test + public void testMatchesMethodInSubpackage() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example.sub", "Foo", false, "bar"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match methods in subpackages"); + } + + @Test + public void testDoesNotMatchMethodOutsidePackage() { + FullyQualifiedName fqn = new FullyQualifiedName("org.other", "Foo", false, "bar"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should not match methods outside the package"); + } + + @Test + public void testDoesNotMatchPartialPackageName() { + // Package is "com.example", should not match "com.examples" + FullyQualifiedName fqn = new FullyQualifiedName("com.examples", "Foo", false, "bar"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should not match partial package names"); + } + } + + @Nested + class WithExclusions { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude: [Internal, com.example.Private, Secret.sensitiveMethod]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testExcludesRelativeClassName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Internal", false, "foo"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should exclude relative class name"); + } + + @Test + public void testExcludesAbsoluteClassName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Private", false, "foo"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should exclude absolute class name"); + } + + @Test + public void testExcludesSpecificMethod() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Secret", false, "sensitiveMethod"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should exclude specific method"); + } + + @Test + public void testDoesNotExcludeOtherMethodsInExcludedClass() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Secret", false, "publicMethod"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should not exclude other methods in partially excluded class"); + } + + @Test + public void testIncludesNonExcludedClass() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Public", false, "foo"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should include non-excluded classes"); + } + + @Test + public void testExcludesSubclassesOfExcludedClass() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Internal$Inner", false, "foo"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should exclude nested classes of excluded class"); + } + } + + @Nested + class WithHashSeparator { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude: [Foo#bar, Internal#secretMethod]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testConvertsHashToDot() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Foo", false, "bar"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should convert # to . for backward compatibility"); + } + + @Test + public void testDoesNotExcludeOtherMethods() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Foo", false, "baz"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should not exclude methods not specified"); + } + } + } + + @Nested + class MethodsModeTests { + @Nested + class BasicPatternMatching { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "methods:", + "- class: Controller", + " name: handle.*", + " labels: [controller]", + "- class: Service", + " name: process", + " labels: [service, business-logic]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testMatchesSimpleClassName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "handleRequest"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match simple class name"); + assertArrayEquals(new String[] { "controller" }, result.getLabels()); + } + + @Test + public void testMatchesMethodPattern() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "handleResponse"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match method name pattern"); + assertArrayEquals(new String[] { "controller" }, result.getLabels()); + } + + @Test + public void testDoesNotMatchNonMatchingMethod() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "initialize"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should not match non-matching method name"); + } + + @Test + public void testMatchesExactMethodName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Service", false, "process"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match exact method name"); + assertArrayEquals(new String[] { "service", "business-logic" }, result.getLabels()); + } + + @Test + public void testDoesNotMatchPartialMethodName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Service", false, "processData"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should not match partial method name (no implicit wildcards)"); + } + + @Test + public void testDoesNotMatchDifferentPackage() { + FullyQualifiedName fqn = new FullyQualifiedName("org.other", "Controller", false, "handleRequest"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Should not match methods in different package"); + } + + @Test + public void testDoesNotMatchSubpackage() { + // In methods mode, package must match exactly (not subpackages) + FullyQualifiedName fqn = new FullyQualifiedName("com.example.sub", "Controller", false, "handleRequest"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNull(result, "Methods mode should not match subpackages"); + } + } + + @Nested + class FullyQualifiedClassNames { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "methods:", + "- class: com.example.web.Controller", + " name: handle.*", + " labels: [web-controller]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testMatchesFullyQualifiedClassName() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "web.Controller", false, "handleGet"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should match fully qualified class name pattern"); + assertArrayEquals(new String[] { "web-controller" }, result.getLabels()); + } + } + + @Nested + class RegexPatterns { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "methods:", + "- class: (Controller|Handler)", + " name: (get|set).*", + " labels: [accessor]", + "- class: .*Service", + " name: execute", + " labels: [service-executor]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testMatchesClassAlternation() { + FullyQualifiedName fqn1 = new FullyQualifiedName("com.example", "Controller", false, "getData"); + FullyQualifiedName fqn2 = new FullyQualifiedName("com.example", "Handler", false, "setData"); + + AppMapPackage.LabelConfig result1 = pkg.find(fqn1); + AppMapPackage.LabelConfig result2 = pkg.find(fqn2); + + assertNotNull(result1, "Should match first class alternative"); + assertNotNull(result2, "Should match second class alternative"); + assertArrayEquals(new String[] { "accessor" }, result1.getLabels()); + assertArrayEquals(new String[] { "accessor" }, result2.getLabels()); + } + + @Test + public void testMatchesClassWildcard() { + FullyQualifiedName fqn1 = new FullyQualifiedName("com.example", "UserService", false, "execute"); + FullyQualifiedName fqn2 = new FullyQualifiedName("com.example", "OrderService", false, "execute"); + + AppMapPackage.LabelConfig result1 = pkg.find(fqn1); + AppMapPackage.LabelConfig result2 = pkg.find(fqn2); + + assertNotNull(result1, "Should match first service"); + assertNotNull(result2, "Should match second service"); + assertArrayEquals(new String[] { "service-executor" }, result1.getLabels()); + assertArrayEquals(new String[] { "service-executor" }, result2.getLabels()); + } + } + + @Nested + class IgnoresExcludeField { + AppMapPackage pkg; + + @BeforeEach + public void setup() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude: [Controller]", // This should be ignored + "methods:", + "- class: Controller", + " name: handleRequest", + " labels: [controller]" + }; + pkg = loadYaml(yaml, AppMapPackage.class); + } + + @Test + public void testIgnoresExcludeWhenMethodsIsSet() { + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "handleRequest"); + AppMapPackage.LabelConfig result = pkg.find(fqn); + assertNotNull(result, "Should ignore exclude field when methods is set"); + assertArrayEquals(new String[] { "controller" }, result.getLabels()); + } + } + } + + @Nested + class EdgeCases { + @Test + public void testNullPath() throws Exception { + String[] yaml = { + "---", + "path: null" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Foo", false, "bar"); + assertNull(pkg.find(fqn), "Should handle null path gracefully"); + } + + @Test + public void testNullCanonicalName() throws Exception { + String[] yaml = { + "---", + "path: com.example" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertNull(pkg.find(null), "Should handle null canonical name gracefully"); + } + + @Test + public void testEmptyExclude() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude: []" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertEquals(0, pkg.exclude.length, "Should handle empty exclude array"); + } + + @Test + public void testNullExclude() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude:" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertNotNull(pkg.exclude, "Should initialize exclude to empty array"); + assertEquals(0, pkg.exclude.length, "Should handle null exclude array"); + } + + @Test + public void testNoExclude() throws Exception { + String[] yaml = { + "---", + "path: com.example" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertNotNull(pkg.exclude, "Should initialize exclude to empty array"); + assertEquals(0, pkg.exclude.length); + } + + @Test + public void testShallowDefault() throws Exception { + String[] yaml = { + "---", + "path: com.example" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertFalse(pkg.shallow, "shallow should default to false"); + } + + @Test + public void testShallowTrue() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "shallow: true" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + assertTrue(pkg.shallow, "shallow should be set to true"); + } + } + + @Nested + class EnhancedLabelConfigTests { + @Test + public void testEmptyLabelConfig() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig(); + // Empty constructor uses field initialization, which is an empty array + assertNotNull(lc.getLabels(), "Empty LabelConfig should have non-null labels"); + assertEquals(0, lc.getLabels().length, "Empty LabelConfig should have empty labels array"); + } + + @Test + public void testLabelConfigWithLabels() throws Exception { + String[] yaml = { + "---", + "class: Foo", + "name: bar", + "labels: [test, example]" + }; + AppMapPackage.LabelConfig lc = loadYaml(yaml, AppMapPackage.LabelConfig.class); + assertNotNull(lc.getLabels()); + assertEquals(2, lc.getLabels().length); + assertEquals("test", lc.getLabels()[0]); + assertEquals("example", lc.getLabels()[1]); + } + + @Test + public void testLabelConfigMatchesSimpleClass() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Controller", "handle.*", new String[] { "web" }); + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "handleGet"); + assertTrue(lc.matches(fqn), "Should match simple class name"); + } + + @Test + public void testLabelConfigMatchesFullyQualifiedClass() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("com.example.Controller", "handle.*", + new String[] { "web" }); + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "handleGet"); + assertTrue(lc.matches(fqn), "Should match fully qualified class name"); + } + + @Test + public void testLabelConfigDoesNotMatchWrongClass() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Controller", "handle.*", new String[] { "web" }); + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Service", false, "handleGet"); + assertFalse(lc.matches(fqn), "Should not match wrong class"); + } + + @Test + public void testLabelConfigDoesNotMatchWrongMethod() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Controller", "handle.*", new String[] { "web" }); + FullyQualifiedName fqn = new FullyQualifiedName("com.example", "Controller", false, "process"); + assertFalse(lc.matches(fqn), "Should not match wrong method"); + } + + @Test + public void testLabelConfigMatchesExactPattern() { + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Foo", "bar", new String[] { "test" }); + assertTrue(lc.matches("Foo", "bar"), "Should match exact patterns"); + } + + @Test + public void testLabelConfigDoesNotMatchPartialClass() { + // Pattern "Foo" should not match "Foo1" due to anchoring + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Foo", "bar", new String[] { "test" }); + assertFalse(lc.matches("Foo1", "bar"), "Should not match partial class name"); + } + + @Test + public void testLabelConfigDoesNotMatchPartialMethod() { + // Pattern "bar" should not match "bar!" due to anchoring + AppMapPackage.LabelConfig lc = new AppMapPackage.LabelConfig("Foo", "bar", new String[] { "test" }); + assertFalse(lc.matches("Foo", "bar!"), "Should not match partial method name"); + } + } + + @Nested + class ExcludesMethodTests { + @Test + public void testExcludesFullyQualifiedName() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude: [Internal, Private.secret]" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + + FullyQualifiedName fqn1 = new FullyQualifiedName("com.example", "Internal", false, "foo"); + FullyQualifiedName fqn2 = new FullyQualifiedName("com.example", "Private", false, "secret"); + FullyQualifiedName fqn3 = new FullyQualifiedName("com.example", "Public", false, "method"); + + assertTrue(pkg.excludes(fqn1), "Should exclude Internal class"); + assertTrue(pkg.excludes(fqn2), "Should exclude Private.secret method"); + assertFalse(pkg.excludes(fqn3), "Should not exclude Public class"); + } + } + + @Nested + class ComplexScenarios { + @Test + public void testMultipleMethodConfigs() throws Exception { + String[] yaml = { + "---", + "path: com.example.api", + "methods:", + "- class: .*Controller", + " name: handle.*", + " labels: [web, controller]", + "- class: .*Service", + " name: execute.*", + " labels: [service]", + "- class: Repository", + " name: (find|save|delete).*", + " labels: [data-access, repository]" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + + FullyQualifiedName controller = new FullyQualifiedName("com.example.api", "UserController", false, "handleGet"); + FullyQualifiedName service = new FullyQualifiedName("com.example.api", "UserService", false, "executeQuery"); + FullyQualifiedName repo = new FullyQualifiedName("com.example.api", "Repository", false, "findById"); + + AppMapPackage.LabelConfig result1 = pkg.find(controller); + AppMapPackage.LabelConfig result2 = pkg.find(service); + AppMapPackage.LabelConfig result3 = pkg.find(repo); + + assertNotNull(result1); + assertArrayEquals(new String[] { "web", "controller" }, result1.getLabels()); + + assertNotNull(result2); + assertArrayEquals(new String[] { "service" }, result2.getLabels()); + + assertNotNull(result3); + assertArrayEquals(new String[] { "data-access", "repository" }, result3.getLabels()); + } + + @Test + public void testComplexExclusionPatterns() throws Exception { + String[] yaml = { + "---", + "path: com.example", + "exclude:", + " - internal", + " - util.Helper", + " - com.example.test.Mock", + " - Secret.getPassword", + " - Cache.clear" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + + FullyQualifiedName internal = new FullyQualifiedName("com.example.internal", "Foo", false, "bar"); + FullyQualifiedName helper = new FullyQualifiedName("com.example.util", "Helper", false, "help"); + FullyQualifiedName mock = new FullyQualifiedName("com.example.test", "Mock", false, "setup"); + FullyQualifiedName secretGet = new FullyQualifiedName("com.example", "Secret", false, "getPassword"); + FullyQualifiedName secretSet = new FullyQualifiedName("com.example", "Secret", false, "setPassword"); + FullyQualifiedName cacheClear = new FullyQualifiedName("com.example", "Cache", false, "clear"); + FullyQualifiedName cacheGet = new FullyQualifiedName("com.example", "Cache", false, "get"); + + assertNull(pkg.find(internal), "Should exclude internal package"); + assertNull(pkg.find(helper), "Should exclude util.Helper"); + assertNull(pkg.find(mock), "Should exclude test.Mock"); + assertNull(pkg.find(secretGet), "Should exclude Secret.getPassword"); + assertNotNull(pkg.find(secretSet), "Should not exclude Secret.setPassword"); + assertNull(pkg.find(cacheClear), "Should exclude Cache.clear"); + assertNotNull(pkg.find(cacheGet), "Should not exclude Cache.get"); + } + + @Test + public void testUnnamedPackage() throws Exception { + String[] yaml = { + "---", + "path: HelloWorld" + }; + AppMapPackage pkg = loadYaml(yaml, AppMapPackage.class); + + // Test a method in the unnamed package (empty package name) + FullyQualifiedName method = new FullyQualifiedName("", "HelloWorld", false, "getGreetingWithPunctuation"); + + AppMapPackage.LabelConfig result = pkg.find(method); + assertNotNull(result, "Should find method in unnamed package when path specifies the class name"); + + // Test that other classes in the unnamed package are not matched + FullyQualifiedName otherClass = new FullyQualifiedName("", "OtherClass", false, "someMethod"); + assertNull(pkg.find(otherClass), "Should not match other classes in the unnamed package"); + } + } } \ No newline at end of file diff --git a/agent/src/test/java/com/appland/appmap/process/hooks/SqlQuerySQLExceptionAvailabilityTest.java b/agent/src/test/java/com/appland/appmap/process/hooks/SqlQuerySQLExceptionAvailabilityTest.java new file mode 100644 index 00000000..eca24a71 --- /dev/null +++ b/agent/src/test/java/com/appland/appmap/process/hooks/SqlQuerySQLExceptionAvailabilityTest.java @@ -0,0 +1,121 @@ +package com.appland.appmap.process.hooks; + +import org.junit.jupiter.api.Test; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; + +/** + * Regression test for a {@link NoClassDefFoundError} involving {@link java.sql.SQLException}. + *

+ * In certain environments (e.g., specific configurations of Oracle UCP or custom container classloaders), + * {@code java.sql.SQLException} might not be visible to the classloader responsible for loading + * {@code com.appland.appmap.process.hooks.SqlQuery}. This can lead to a crash when the agent attempts + * to handle SQL events. + *

+ * The crash manifests as: + *

+ * Caused by: com.example.operation.flow.FlowException: java/sql/SQLException
+ * ...
+ * Caused by: java.lang.NoClassDefFoundError: java/sql/SQLException
+ *     at com.appland.appmap.process.hooks.SqlQuery.getDbName(SqlQuery.java:76)
+ *     at com.appland.appmap.process.hooks.SqlQuery.recordSql(SqlQuery.java:89)
+ *     at com.appland.appmap.process.hooks.SqlQuery.executeQuery(SqlQuery.java:172)
+ * 
+ *

+ * This test reproduces the environment by using a custom {@link ClassLoader} that explicitly + * throws {@link ClassNotFoundException} when {@code java.sql.SQLException} is requested. + * It verifies that {@code SqlQuery} can be loaded and executed without triggering the error. + */ +public class SqlQuerySQLExceptionAvailabilityTest { + + @Test + public void testSqlQueryResilienceToMissingSQLException() throws Exception { + // 1. Create a RestrictedClassLoader that hides java.sql.SQLException + ClassLoader restrictedLoader = new RestrictedClassLoader(this.getClass().getClassLoader()); + + // 2. Load the SqlQuery class using the restricted loader. + // This forces the verifier to check dependencies of SqlQuery using our restricted loader. + // If SqlQuery explicitly catches or references SQLException in a way that requires resolution, + // this (or the method invocation below) should fail. + String sqlQueryClassName = "com.appland.appmap.process.hooks.SqlQuery"; + Class sqlQueryClass = restrictedLoader.loadClass(sqlQueryClassName); + + // 3. Invoke a method that triggers the problematic code path (getDbName). + // We choose recordSql(Event, Connection, String) which calls getDbName(Connection). + Method recordSqlMethod = sqlQueryClass.getMethod("recordSql", + com.appland.appmap.output.v1.Event.class, + java.sql.Statement.class, + String.class + ); + + // Prepare arguments + com.appland.appmap.output.v1.Event mockEvent = mock(com.appland.appmap.output.v1.Event.class); + java.sql.Statement mockStatement = mock(java.sql.Statement.class); + + assertDoesNotThrow(() -> { + recordSqlMethod.invoke(null, mockEvent, mockStatement, "SELECT 1"); + }, "SqlQuery should not fail even if java.sql.SQLException is missing"); + } + + /** + * A ClassLoader that throws ClassNotFoundException for java.sql.SQLException + * and forces re-definition of SqlQuery to ensure it's loaded by this loader. + */ + private static class RestrictedClassLoader extends ClassLoader { + + public RestrictedClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + String forbiddenClassName = "java.sql.SQLException"; + if (forbiddenClassName.equals(name)) { + throw new ClassNotFoundException("Simulated missing class: " + name); + } + + // If it's the target class, we want to define it ourselves to ensure + // this classloader (and its restrictions) is used for verification. + String targetClassName = "com.appland.appmap.process.hooks.SqlQuery"; + if (targetClassName.equals(name)) { + // Check if already loaded + Class loaded = findLoadedClass(name); + if (loaded != null) { + return loaded; + } + + try { + byte[] bytes = loadClassBytes(name); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + throw new ClassNotFoundException("Failed to read bytes for " + name, e); + } + } + + // For everything else, delegate to parent + return super.loadClass(name); + } + + private byte[] loadClassBytes(String className) throws IOException { + String resourceName = "/" + className.replace('.', '/') + ".class"; + try (InputStream is = getClass().getResourceAsStream(resourceName)) { + if (is == null) { + throw new IOException("Resource not found: " + resourceName); + } + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + stream.write(buffer, 0, bytesRead); + } + return stream.toByteArray(); + } + } + } +} diff --git a/agent/src/test/java/com/appland/appmap/util/PrefixTrieTest.java b/agent/src/test/java/com/appland/appmap/util/PrefixTrieTest.java new file mode 100644 index 00000000..a28cbe43 --- /dev/null +++ b/agent/src/test/java/com/appland/appmap/util/PrefixTrieTest.java @@ -0,0 +1,388 @@ +package com.appland.appmap.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class PrefixTrieTest { + + @Nested + class BasicOperations { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testEmptyTrie() { + assertFalse(trie.startsWith("anything"), "Empty trie should not match any string"); + assertFalse(trie.startsWith(""), "Empty trie should not match empty string"); + } + + @Test + void testSingleInsertExactMatch() { + trie.insert("foo"); + assertTrue(trie.startsWith("foo"), "Should match exact string"); + } + + @Test + void testSingleInsertPrefixMatch() { + trie.insert("foo"); + assertTrue(trie.startsWith("foobar"), "Should match when pattern is a prefix"); + assertTrue(trie.startsWith("foo.bar"), "Should match when pattern is a prefix"); + } + + @Test + void testSingleInsertNoMatch() { + trie.insert("foo"); + assertFalse(trie.startsWith("bar"), "Should not match unrelated string"); + assertFalse(trie.startsWith("fo"), "Should not match partial prefix"); + assertFalse(trie.startsWith("f"), "Should not match single character"); + } + + @Test + void testEmptyStringInsert() { + trie.insert(""); + assertTrue(trie.startsWith(""), "Should match empty string when empty string is inserted"); + assertTrue(trie.startsWith("anything"), "Empty pattern at root matches non-empty strings"); + } + + @Test + void testNullHandling() { + trie.insert(null); + assertFalse(trie.startsWith(null), "Null should not match anything"); + + trie.insert("foo"); + assertFalse(trie.startsWith(null), "Null should not match even when trie has entries"); + } + } + + @Nested + class MultiplePatterns { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testMultipleDistinctPatterns() { + trie.insert("foo"); + trie.insert("bar"); + trie.insert("baz"); + + assertTrue(trie.startsWith("foobar"), "Should match first pattern"); + assertTrue(trie.startsWith("barbell"), "Should match second pattern"); + assertTrue(trie.startsWith("bazinga"), "Should match third pattern"); + assertFalse(trie.startsWith("qux"), "Should not match uninserted pattern"); + } + + @Test + void testOverlappingPatterns() { + trie.insert("foo"); + trie.insert("foobar"); + + assertTrue(trie.startsWith("foo"), "Should match shorter pattern"); + assertTrue(trie.startsWith("foobar"), "Should match longer pattern"); + assertTrue(trie.startsWith("foobarbaz"), "Should match shortest prefix (foo)"); + } + + @Test + void testPrefixOfPrefix() { + trie.insert("a"); + trie.insert("ab"); + trie.insert("abc"); + + assertTrue(trie.startsWith("a"), "Should match 'a'"); + assertTrue(trie.startsWith("ab"), "Should match 'ab'"); + assertTrue(trie.startsWith("abc"), "Should match 'abc'"); + assertTrue(trie.startsWith("abcd"), "Should match via 'a' prefix"); + assertFalse(trie.startsWith("b"), "Should not match 'b'"); + } + } + + @Nested + class PackageScenarios { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testPackageExclusion() { + trie.insert("internal."); + + assertTrue(trie.startsWith("internal.Foo"), "Should match class in excluded package"); + assertTrue(trie.startsWith("internal.sub.Bar"), "Should match class in excluded subpackage"); + assertFalse(trie.startsWith("internal"), "Should not match package name without separator"); + assertFalse(trie.startsWith("internals.Foo"), "Should not match similar package with separator"); + } + + @Test + void testPackageBoundary() { + trie.insert("test."); + + assertTrue(trie.startsWith("test.Foo"), "Should match class in test package"); + assertTrue(trie.startsWith("test.sub.Bar"), "Should match class in test subpackage"); + assertFalse(trie.startsWith("test"), "Should not match package name without separator"); + assertFalse(trie.startsWith("testing"), "Should not match similar package"); + } + + @Test + void testClassExclusion() { + trie.insert("util.Helper."); + + assertTrue(trie.startsWith("util.Helper.method"), "Should match method in excluded class"); + assertFalse(trie.startsWith("util.Helper"), "Should not match class name without separator"); + assertFalse(trie.startsWith("util.HelperUtils"), "Should not match similar class name"); + assertFalse(trie.startsWith("util"), "Should not match package alone"); + } + + @Test + void testMethodExclusion() { + trie.insert("Cache.clear"); + + assertTrue(trie.startsWith("Cache.clear"), "Should match method exactly"); + assertTrue(trie.startsWith("Cache.clearAll"), "Will match since 'Cache.clear' is a prefix of 'Cache.clearAll'"); + assertFalse(trie.startsWith("Cache"), "Should not match class alone"); + } + + @Test + void testMixedExclusions() { + trie.insert("internal"); // whole package + trie.insert("util.Helper"); // specific class + trie.insert("Cache.clear"); // specific method + trie.insert("test."); // package with separator + + assertTrue(trie.startsWith("internal.Foo.bar"), "Should match package exclusion"); + assertTrue(trie.startsWith("util.Helper.method"), "Should match class exclusion"); + assertTrue(trie.startsWith("Cache.clear"), "Should match method exclusion"); + assertTrue(trie.startsWith("test.Foo"), "Should match package with separator"); + + assertFalse(trie.startsWith("util.Other"), "Should not match other class in util"); + assertFalse(trie.startsWith("Cache.get"), "Should not match other method in Cache"); + } + } + + @Nested + class HierarchicalPatterns { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testDeeplyNestedPackages() { + trie.insert("com.example.internal"); + + assertTrue(trie.startsWith("com.example.internal"), "Should match exact package"); + assertTrue(trie.startsWith("com.example.internal.Foo"), "Should match class in package"); + assertTrue(trie.startsWith("com.example.internal.sub.Bar"), "Should match class in subpackage"); + assertFalse(trie.startsWith("com.example"), "Should not match parent package"); + assertFalse(trie.startsWith("com.example.public"), "Should not match sibling package"); + } + + @Test + void testMultipleLevelsOfExclusion() { + trie.insert("com"); + trie.insert("com.example"); + trie.insert("com.example.foo"); + + assertTrue(trie.startsWith("com.anything"), "Should match via 'com' prefix"); + assertTrue(trie.startsWith("com.example.anything"), "Should match via 'com' prefix"); + assertTrue(trie.startsWith("com.example.foo.Bar"), "Should match via 'com' prefix"); + } + + @Test + void testFullyQualifiedNames() { + trie.insert("com.example.MyClass.myMethod"); + + assertTrue(trie.startsWith("com.example.MyClass.myMethod"), "Should match exact FQN"); + assertFalse(trie.startsWith("com.example.MyClass.otherMethod"), "Should not match different method"); + assertFalse(trie.startsWith("com.example.MyClass"), "Should not match just the class"); + } + } + + @Nested + class EdgeCases { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testSingleCharacterPatterns() { + trie.insert("a"); + + assertTrue(trie.startsWith("a"), "Should match single character"); + assertTrue(trie.startsWith("abc"), "Should match when single char is prefix"); + assertFalse(trie.startsWith("b"), "Should not match different character"); + } + + @Test + void testSpecialCharacters() { + trie.insert("foo$bar"); + trie.insert("baz#qux"); + + assertTrue(trie.startsWith("foo$bar"), "Should match pattern with $"); + assertTrue(trie.startsWith("foo$barbaz"), "Should match when $ pattern is prefix"); + assertTrue(trie.startsWith("baz#qux"), "Should match pattern with #"); + assertFalse(trie.startsWith("foo"), "Should not match partial before special char"); + } + + @Test + void testDuplicateInsertions() { + trie.insert("foo"); + trie.insert("foo"); + trie.insert("foo"); + + assertTrue(trie.startsWith("foobar"), "Should still work after duplicate insertions"); + } + + @Test + void testLongStrings() { + String longPattern = "com.example.very.long.package.name.with.many.segments.MyClass.myMethod"; + trie.insert(longPattern); + + assertTrue(trie.startsWith(longPattern), "Should match long pattern exactly"); + assertTrue(trie.startsWith(longPattern + ".extra"), "Should match long pattern as prefix"); + assertFalse(trie.startsWith("com.example.very.long.package"), "Should not match partial"); + } + + @Test + void testUnicodeCharacters() { + trie.insert("café"); + trie.insert("日本語"); + + assertTrue(trie.startsWith("café.method"), "Should match unicode pattern"); + assertTrue(trie.startsWith("日本語.クラス"), "Should match Japanese characters"); + } + } + + @Nested + class PrefixMatchingBehavior { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testExactMatchIsPrefix() { + trie.insert("exact"); + + assertTrue(trie.startsWith("exact"), "Exact match should return true"); + } + + @Test + void testLongerThanPattern() { + trie.insert("short"); + + assertTrue(trie.startsWith("short.longer.path"), "Longer string should match"); + } + + @Test + void testShorterThanPattern() { + trie.insert("verylongpattern"); + + assertFalse(trie.startsWith("verylong"), "Shorter string should not match"); + assertFalse(trie.startsWith("very"), "Much shorter string should not match"); + } + + @Test + void testFirstMatchWins() { + trie.insert("foo"); + trie.insert("foobar"); + trie.insert("foobarbaz"); + + // When checking "foobarbazqux", it should match "foo" first + assertTrue(trie.startsWith("foobarbazqux"), "Should match shortest prefix"); + } + + @Test + void testNoPartialPrefixMatch() { + trie.insert("complete"); + + assertFalse(trie.startsWith("comp"), "Should not match partial prefix"); + assertFalse(trie.startsWith("compl"), "Should not match partial prefix"); + assertFalse(trie.startsWith("complet"), "Should not match partial prefix"); + assertTrue(trie.startsWith("complete"), "Should match complete pattern"); + assertTrue(trie.startsWith("complete.more"), "Should match with additional text"); + } + } + + @Nested + class RealWorldScenarios { + private PrefixTrie trie; + + @BeforeEach + void setUp() { + trie = new PrefixTrie(); + } + + @Test + void testCommonExclusionPatterns() { + // Typical AppMap exclusion patterns + trie.insert("internal"); + trie.insert("test"); + trie.insert("generated"); + trie.insert("impl.Helper"); + trie.insert("util.StringUtil.intern"); + + // Should match + assertTrue(trie.startsWith("internal.SecretClass.method")); + assertTrue(trie.startsWith("test.MockService.setup")); + assertTrue(trie.startsWith("generated.AutoValue_Foo")); + assertTrue(trie.startsWith("impl.Helper.doSomething")); + assertTrue(trie.startsWith("util.StringUtil.intern")); + + // Should not match + assertFalse(trie.startsWith("impl.OtherClass")); + assertFalse(trie.startsWith("util.StringUtil.format")); + assertFalse(trie.startsWith("public.ApiClass")); + } + + @Test + void testJavaStandardLibraryExclusions() { + trie.insert("java."); + trie.insert("javax."); + trie.insert("sun."); + trie.insert("com.sun."); + + assertTrue(trie.startsWith("java.lang.String")); + assertTrue(trie.startsWith("javax.servlet.HttpServlet")); + assertTrue(trie.startsWith("sun.misc.Unsafe")); + assertTrue(trie.startsWith("com.sun.management.GarbageCollectorMXBean")); + + assertFalse(trie.startsWith("javalin.Context")); + assertFalse(trie.startsWith("com.example.Service")); + } + + @Test + void testFrameworkInternalExclusions() { + trie.insert("org.springframework.cglib"); + trie.insert("org.hibernate.internal"); + trie.insert("net.bytebuddy"); + + assertTrue(trie.startsWith("org.springframework.cglib.Enhancer")); + assertTrue(trie.startsWith("org.hibernate.internal.SessionImpl")); + assertTrue(trie.startsWith("net.bytebuddy.ByteBuddy")); + + assertFalse(trie.startsWith("org.springframework.web.Controller")); + assertFalse(trie.startsWith("org.hibernate.Session")); + } + } +} diff --git a/agent/test/access/MyClass.java b/agent/test/access/MyClass.java new file mode 100644 index 00000000..724fa061 --- /dev/null +++ b/agent/test/access/MyClass.java @@ -0,0 +1,20 @@ +package access; + +public class MyClass { + public void myMethod() { + // do nothing + } + + public void callNonPublic() { + myPackageMethod(); + myPrivateMethod(); + } + + String myPackageMethod() { + return "package method"; + } + + private String myPrivateMethod() { + return "private method"; + } +} diff --git a/agent/test/access/RecordPackage.java b/agent/test/access/RecordPackage.java index 5a9ec27e..ee14035d 100644 --- a/agent/test/access/RecordPackage.java +++ b/agent/test/access/RecordPackage.java @@ -1,9 +1,10 @@ +package access; + import java.io.IOException; import java.io.OutputStreamWriter; import com.appland.appmap.record.Recorder; import com.appland.appmap.record.Recording; -import com.appland.appmap.test.fixture.MyClass; public class RecordPackage { public static void main(String[] argv) { diff --git a/agent/test/access/access.bats b/agent/test/access/access.bats old mode 100644 new mode 100755 index abe6e730..779c0208 --- a/agent/test/access/access.bats +++ b/agent/test/access/access.bats @@ -4,14 +4,17 @@ load '../helper' sep="$JAVA_PATH_SEPARATOR" AGENT_JAR="$(find_agent_jar)" -wd=$(getcwd) -test_cp="${wd}/test/access${sep}${wd}/build/classes/java/test" + +# Resolves to .../agent/test +TEST_DIR="$(dirname "$BATS_TEST_DIRNAME")" + +# test_cp must include 'test' (for access package) and 'test/access' (for default package) +test_cp="${TEST_DIR}${sep}${TEST_DIR}/access" java_cmd="java -javaagent:'${AGENT_JAR}' -cp '${test_cp}'" setup() { - javac -cp "${AGENT_JAR}${sep}${test_cp}" test/access/*.java - - cd test/access + cd "$BATS_TEST_DIRNAME" || exit + javac -cp "${AGENT_JAR}" -sourcepath "$TEST_DIR" ./*.java _configure_logging } @@ -22,7 +25,7 @@ setup() { # functionaly it enables is pretty useless, though, so it seems harmless to # hijack it. @test "testProtected" { - local cmd="${java_cmd} RecordPackage" + local cmd="${java_cmd} access.RecordPackage" [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 # Exactly 4 events means that MyClass.myPrivateMethod was not recorded @@ -31,7 +34,7 @@ setup() { } @test "testPrivate" { - local cmd="${java_cmd} -Dappmap.record.private=true RecordPackage" + local cmd="${java_cmd} -Dappmap.record.private=true access.RecordPackage" [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 # 6 events means that both myPackageMethod and myPrivateMethod were recorded @@ -40,7 +43,7 @@ setup() { } @test "outside git repo" { - cp -v "$(_top_level)/agent/appmap.yml" "$BATS_TEST_TMPDIR"/. + cp -v "appmap.yml" "$BATS_TEST_TMPDIR"/. cd "$BATS_TEST_TMPDIR" # sanity check @@ -48,7 +51,7 @@ setup() { assert_output -p 'not a git repository' assert_failure - local cmd="${java_cmd} RecordPackage" + local cmd="${java_cmd} access.RecordPackage" [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 run bash -c "eval \"$cmd\" 2>/dev/null" assert_success diff --git a/agent/test/access/appmap.yml b/agent/test/access/appmap.yml index e20bd71a..9c3ef4a1 100644 --- a/agent/test/access/appmap.yml +++ b/agent/test/access/appmap.yml @@ -1,4 +1,4 @@ name: test-access packages: -- path: com.appland.appmap.test.fixture +- path: access - path: HelloWorld \ No newline at end of file diff --git a/agent/test/classloading/app/build.gradle b/agent/test/classloading/app/build.gradle index 6fc3530f..b29e9257 100644 --- a/agent/test/classloading/app/build.gradle +++ b/agent/test/classloading/app/build.gradle @@ -47,12 +47,21 @@ application { // Define the main class for the application. mainClass = 'com.appland.appmap.test.fixture.Runner' def libJar = tasks.getByPath(':lib:jar').outputs.files.singleFile + + // Allow testing with bootstrap classpath instead of javaagent + def useBootstrapClasspath = findProperty("useBootstrapClasspath") == "true" + applicationDefaultJvmArgs += [ - "-javaagent:${appmapJar}", - /*"-Dsun.misc.URLClassPath.debug",*/ - "-DtestJars=${configurations.servlet.asPath};${libJar}", - "-Djava.util.logging.config.file=${System.env.JUL_CONFIG}" + "-javaagent:${appmapJar}", + /*"-Dsun.misc.URLClassPath.debug",*/ + "-DtestJars=${configurations.servlet.asPath};${libJar}", + "-Djava.util.logging.config.file=${System.env.JUL_CONFIG}" ] + if (useBootstrapClasspath) { + applicationDefaultJvmArgs += [ + "-Xbootclasspath/a:${appmapJar}", + ] + } } tasks.named('test') { diff --git a/agent/test/classloading/app/src/main/java/com/appland/appmap/test/fixture/TestBootstrapClasspath.java b/agent/test/classloading/app/src/main/java/com/appland/appmap/test/fixture/TestBootstrapClasspath.java new file mode 100644 index 00000000..7e6298d4 --- /dev/null +++ b/agent/test/classloading/app/src/main/java/com/appland/appmap/test/fixture/TestBootstrapClasspath.java @@ -0,0 +1,45 @@ +package com.appland.appmap.test.fixture; + +import com.appland.appmap.Agent; +import com.appland.appmap.config.Properties; + +/** + * Test that verifies the agent can run when loaded on the bootstrap classpath. + * This is a regression test for the fix that prevents NullPointerException when + * Agent.class.getClassLoader() returns null. + */ +public class TestBootstrapClasspath implements TestClass { + + @Override + public int beforeTest() throws Exception { + return 0; + } + + @Override + public int runTest() throws Exception { + // Verify that the agent is actually running on the bootstrap classpath + ClassLoader agentClassLoader = Agent.class.getClassLoader(); + if (agentClassLoader != null) { + System.err.println("ERROR: Agent is not running on bootstrap classpath"); + System.err.println("Agent class loader: " + agentClassLoader); + return 1; + } + + // Verify that Properties class is also on bootstrap classpath + ClassLoader propertiesClassLoader = Properties.class.getClassLoader(); + if (propertiesClassLoader != null) { + System.err.println("ERROR: Properties is not running on bootstrap classpath"); + System.err.println("Properties class loader: " + propertiesClassLoader); + return 1; + } + + // Verify that Git integration is automatically disabled when on bootstrap classpath + if (!Properties.DisableGit) { + System.err.println("ERROR: Git integration should be automatically disabled on bootstrap classpath"); + return 1; + } + + System.out.println("SUCCESS: Agent running on bootstrap classpath with Git disabled"); + return 0; + } +} diff --git a/agent/test/classloading/classloading.bats b/agent/test/classloading/classloading.bats index 85bc3c82..62b184e6 100644 --- a/agent/test/classloading/classloading.bats +++ b/agent/test/classloading/classloading.bats @@ -1,9 +1,11 @@ +#!/usr/bin/env bats + load '../helper' setup_file() { export AGENT_JAR="$(find_agent_jar)" - cd test/classloading + cd "$BATS_TEST_DIRNAME" _configure_logging } @@ -12,15 +14,29 @@ setup_file() { # it's easy to do (the test for BATS_VERSION will add -q to the command line # when run as a test, but not when run in a shell). run \ - ./gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" run --args "TestSafeClassForName" + gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" run --args "TestSafeClassForName" assert_success } @test "Proxy" { run --separate-stderr \ - ./gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" run --args "TestProxy" + gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" run --args "TestProxy" assert_success assert_json_eq ".events[0].defined_class" "com.appland.appmap.test.fixture.helloworld.HelloWorld" assert_json_eq ".events[0].method_id" "getGreeting" assert_json_eq ".events[0].path" "lib/src/main/java/com/appland/appmap/test/fixture/helloworld/HelloWorld.java" } + +@test "Bootstrap Classpath" { + # Regression test for fix that allows running agent on bootstrap classpath. + # This verifies that: + # 1. Agent.class.getResource() works when getClassLoader() returns null + # 2. Git integration is automatically disabled on bootstrap classpath + # 3. No NullPointerException occurs during agent initialization + run \ + gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" -PuseBootstrapClasspath=true run --args "TestBootstrapClasspath" + assert_success + + # Verify the test confirmed running on bootstrap classpath with Git disabled + assert_output --partial "SUCCESS: Agent running on bootstrap classpath with Git disabled" +} diff --git a/agent/test/classloading/gradle/wrapper/gradle-wrapper.jar b/agent/test/classloading/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index c1962a79..00000000 Binary files a/agent/test/classloading/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/classloading/gradle/wrapper/gradle-wrapper.properties b/agent/test/classloading/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37aef8d3..00000000 --- a/agent/test/classloading/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/classloading/gradlew b/agent/test/classloading/gradlew deleted file mode 100755 index aeb74cbb..00000000 --- a/agent/test/classloading/gradlew +++ /dev/null @@ -1,245 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/classloading/gradlew.bat b/agent/test/classloading/gradlew.bat deleted file mode 100644 index 6689b85b..00000000 --- a/agent/test/classloading/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/agent/test/encoding/ReadFullyTest.java b/agent/test/encoding/ReadFullyTest.java new file mode 100644 index 00000000..68b20c55 --- /dev/null +++ b/agent/test/encoding/ReadFullyTest.java @@ -0,0 +1,47 @@ +package test.pkg; + +import com.appland.appmap.config.AppMapConfig; +import com.appland.appmap.record.Recording; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; + +public class ReadFullyTest { + public static void main(String[] args) { + try { + runTest(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + public static void runTest() throws IOException { + // Initialize AppMapConfig + AppMapConfig.initialize(FileSystems.getDefault()); + + // 1. Create a dummy AppMap file with known UTF-8 content + String content = "Check: \u26A0\uFE0F \u041F\u0440\u0438\u0432\u0435\u0442"; + File tempFile = File.createTempFile("readfully", ".appmap.json"); + tempFile.deleteOnExit(); + + try (Writer fw = new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8)) { + fw.write(content); + } + + // 2. Create a Recording object pointing to it + Recording recording = new Recording("test", tempFile); + + // 3. Call readFully and write to stdout using UTF-8 + // This validates that readFully correctly reads the UTF-8 file bytes into characters + // regardless of the system's default encoding (which we will set to something else in BATS). + Writer stdoutWriter = new OutputStreamWriter(System.out, StandardCharsets.UTF_8); + recording.readFully(false, stdoutWriter); + stdoutWriter.flush(); + } +} diff --git a/agent/test/encoding/UnicodeTest.java b/agent/test/encoding/UnicodeTest.java new file mode 100644 index 00000000..2f30dc8f --- /dev/null +++ b/agent/test/encoding/UnicodeTest.java @@ -0,0 +1,41 @@ +package test.pkg; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.io.IOException; + +public class UnicodeTest { + public static String echo(String input) { + return input; + } + + public static byte[] echoBytes(byte[] input) { + return input.clone(); + } + + public static void main(String[] args) { + try { + runTest(); + } catch (IOException e) { + e.printStackTrace(); + // exit 1 + System.exit(1); + } + } + + public static void runTest() throws IOException { + byte[] allBytes = Files.readAllBytes(Paths.get("encoding_test.cp1252")); + + String allString = new String(allBytes, "Cp1252"); + String echoedString = echo(allString); + + // print out the echoed string + System.out.println(echoedString); + + byte[] echoedBytes = echoBytes(allBytes); + // print out the echoed bytes as hex + for (byte b : echoedBytes) { + System.out.printf("%02X ", b); + } + } +} diff --git a/agent/test/encoding/appmap.yml b/agent/test/encoding/appmap.yml new file mode 100644 index 00000000..1b191a20 --- /dev/null +++ b/agent/test/encoding/appmap.yml @@ -0,0 +1,3 @@ +name: encoding +packages: +- path: test.pkg diff --git a/agent/test/encoding/encoding.bats b/agent/test/encoding/encoding.bats new file mode 100755 index 00000000..44e913b8 --- /dev/null +++ b/agent/test/encoding/encoding.bats @@ -0,0 +1,63 @@ +#!/usr/bin/env bats +# shellcheck disable=SC2164 + +load '../helper' + +sep="$JAVA_PATH_SEPARATOR" +AGENT_JAR="$(find_agent_jar)" +java_cmd="java -cp ${BATS_TEST_DIRNAME}/build -javaagent:'${AGENT_JAR}'" + +setup() { + cd "${BATS_TEST_DIRNAME}" + + mkdir -p build + # Compile tests. Output to build so package structure 'test/pkg' works. + javac -d ./build UnicodeTest.java + + # Require the agent jar on the classpath to find the Recording class. + javac -cp "${AGENT_JAR}" -d ./build ReadFullyTest.java + + rm -rf "${BATS_TEST_DIRNAME}/tmp/appmap" + _configure_logging +} + +@test "AppMap file encoding with Windows-1252" { + # Run with windows-1252 encoding. + # We assert that the generated file is valid UTF-8 and contains the correct characters, + # even though the JVM default encoding is Windows-1252. + local cmd="${java_cmd} -Dfile.encoding=windows-1252 -Dappmap.recording.auto=true test.pkg.UnicodeTest" + [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 + + eval "$cmd" + + # Verify the output file exists — it should be the only AppMap file generated, with random name + # so glob for tmp/appmap/java/*.appmap.json + appmap_file=$(ls tmp/appmap/java/*.appmap.json) + [ -f "$appmap_file" ] + + # Verify it is valid JSON + jq . "$appmap_file" > /dev/null + + # Verify it is valid UTF-8 + iconv -f UTF-8 -t UTF-8 "$appmap_file" > /dev/null + + # Verify it contains the expected Unicode characters + grep -q "Euro: €, Accent: é, Quote: „" "$appmap_file" +} + +@test "Recording.readFully works with Windows-1252 default encoding" { + # Run ReadFullyTest with windows-1252 default encoding. + # We also need to add the agent jar to the classpath so it can find the Recording class. + local cmd="java -cp ${BATS_TEST_DIRNAME}/build${sep}${AGENT_JAR} -Dfile.encoding=windows-1252 test.pkg.ReadFullyTest" + [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 + + run eval "$cmd" + + [ "$status" -eq 0 ] + [[ "$output" == *"Check: ⚠️ Привет"* ]] +} + +teardown() { + rm -rf tmp + rm -rf build +} diff --git a/agent/test/encoding/encoding_test.cp1252 b/agent/test/encoding/encoding_test.cp1252 new file mode 100644 index 00000000..10b7f7d6 --- /dev/null +++ b/agent/test/encoding/encoding_test.cp1252 @@ -0,0 +1,5 @@ +Euro: , Accent: , Quote: +---SEPARATOR--- +:7ǞI +---BINARY--- +ޭ \ No newline at end of file diff --git a/agent/test/event/DisabledValue.java b/agent/test/event/DisabledValue.java index ef50abce..44df87b5 100644 --- a/agent/test/event/DisabledValue.java +++ b/agent/test/event/DisabledValue.java @@ -1,11 +1,11 @@ +package event; + import java.io.IOException; import java.io.OutputStreamWriter; import com.appland.appmap.record.Recorder; import com.appland.appmap.record.Recording; -import com.appland.appmap.test.fixture.Example; - public class DisabledValue { public static void main(String[] argv) { diff --git a/agent/test/event/Example.java b/agent/test/event/Example.java new file mode 100644 index 00000000..6cad632f --- /dev/null +++ b/agent/test/event/Example.java @@ -0,0 +1,12 @@ +package event; + +public class Example { + @Override + public String toString() { + return "an Example"; + } + + public Example doSomething(Example other) { + return other; + } +} diff --git a/agent/test/event/appmap.yml b/agent/test/event/appmap.yml index 63c6f786..bbef530c 100644 --- a/agent/test/event/appmap.yml +++ b/agent/test/event/appmap.yml @@ -1,3 +1,3 @@ name: test-event packages: -- path: com.appland.appmap.test.fixture +- path: event diff --git a/agent/test/event/event.bats b/agent/test/event/event.bats old mode 100644 new mode 100755 index c5c501e7..af9ea103 --- a/agent/test/event/event.bats +++ b/agent/test/event/event.bats @@ -2,21 +2,20 @@ load '../helper' -sep="$JAVA_PATH_SEPARATOR" AGENT_JAR="$(find_agent_jar)" -wd=$(getcwd) -test_cp="${wd}/test/event${sep}${wd}/build/classes/java/test" -java_cmd="java -javaagent:'${AGENT_JAR}' -cp '${test_cp}'" -setup() { - javac -cp "${AGENT_JAR}${sep}${test_cp}" test/event/*.java +# Resolves to .../agent/test +TEST_DIR="$(dirname "$BATS_TEST_DIRNAME")" +java_cmd="java -javaagent:'${AGENT_JAR}' -cp '${TEST_DIR}'" - cd test/event +setup() { + cd "$BATS_TEST_DIRNAME" || exit + javac -cp "${AGENT_JAR}" -sourcepath "$TEST_DIR" ./*.java _configure_logging } @test "disabled value" { - local cmd="${java_cmd} -Dappmap.event.disableValue=true DisabledValue" + local cmd="${java_cmd} -Dappmap.event.disableValue=true event.DisabledValue" [[ $BATS_VERBOSE_RUN == 1 ]] && echo "cmd: $cmd" >&3 local output diff --git a/agent/test/gradle/wrapper/gradle-wrapper.jar b/agent/test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f8e1ee31 Binary files /dev/null and b/agent/test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.properties b/agent/test/gradle/wrapper/gradle-wrapper.properties similarity index 94% rename from agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.properties rename to agent/test/gradle/wrapper/gradle-wrapper.properties index 3fa8f862..1af9e093 100644 --- a/agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.properties +++ b/agent/test/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/agent/test/test-frameworks/gradlew b/agent/test/gradlew similarity index 93% rename from agent/test/test-frameworks/gradlew rename to agent/test/gradlew index 1aa94a42..adff685a 100755 --- a/agent/test/test-frameworks/gradlew +++ b/agent/test/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/agent/test/http_client/httpclient/gradlew.bat b/agent/test/gradlew.bat similarity index 85% rename from agent/test/http_client/httpclient/gradlew.bat rename to agent/test/gradlew.bat index 6689b85b..e509b2dd 100644 --- a/agent/test/http_client/httpclient/gradlew.bat +++ b/agent/test/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/agent/test/gretty-tomcat/appmap.yml b/agent/test/gretty-tomcat/appmap.yml new file mode 100644 index 00000000..32e1ffc0 --- /dev/null +++ b/agent/test/gretty-tomcat/appmap.yml @@ -0,0 +1,3 @@ +name: gretty-tomcat +packages: +- path: org.example diff --git a/agent/test/gretty-tomcat/build.gradle b/agent/test/gretty-tomcat/build.gradle new file mode 100644 index 00000000..4d8da04f --- /dev/null +++ b/agent/test/gretty-tomcat/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'war' + id 'org.gretty' version '4.1.6' +} + +repositories { + mavenCentral() +} + +def appmapJar = System.env.AGENT_JAR + +gretty { + servletContainer = 'tomcat10' + contextPath = '/' + jvmArgs = [ + "-Dappmap.config.file=appmap.yml", + "-Dappmap.debug.file=../../build/logs/gretty-tomcat-appmap.log" + ] + if (appmapJar) { + jvmArgs << "-javaagent:${appmapJar}" + } +} + +dependencies { + providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0' +} + diff --git a/agent/test/gretty-tomcat/gretty-tomcat.bats b/agent/test/gretty-tomcat/gretty-tomcat.bats new file mode 100755 index 00000000..b9049c46 --- /dev/null +++ b/agent/test/gretty-tomcat/gretty-tomcat.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load '../helper' + +setup_file() { + is_java 11 || skip "Java 11 is required" + + cd "${BATS_TEST_DIRNAME}" || exit 1 + mkdir -p "$LOG_DIR" + + export LOG="$LOG_DIR/gretty-tomcat.log" + export SERVER_PORT=8080 + export WS_URL="http://localhost:${SERVER_PORT}/hello" + + _configure_logging + + echo -n "Starting gretty-tomcat test server..." >&3 + gradlew appStart -Pgretty.httpPort=${SERVER_PORT} &> $LOG & + export WS_PID=$! + + wait_for_ws +} + +teardown_file() { + gradlew appStop || true + # stop_ws might fail if /exit is not there, but it also waits for process to die. + # We can try to just kill the gradle process if it's still running. + kill "$WS_PID" || true +} + +@test "hello world" { + run _curl -sXGET "${WS_URL}" + assert_success + assert_output "Hello, World!" +} + + diff --git a/agent/test/gretty-tomcat/settings.gradle b/agent/test/gretty-tomcat/settings.gradle new file mode 100644 index 00000000..76ed3897 --- /dev/null +++ b/agent/test/gretty-tomcat/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'gretty-tomcat' diff --git a/agent/test/gretty-tomcat/src/main/java/org/example/HelloServlet.java b/agent/test/gretty-tomcat/src/main/java/org/example/HelloServlet.java new file mode 100644 index 00000000..4ceeac84 --- /dev/null +++ b/agent/test/gretty-tomcat/src/main/java/org/example/HelloServlet.java @@ -0,0 +1,16 @@ +package org.example; + +import java.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"}) +public class HelloServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.getWriter().print("Hello, World!"); + } +} diff --git a/agent/test/gretty-tomcat/src/main/webapp/.keep b/agent/test/gretty-tomcat/src/main/webapp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/agent/test/helper.bash b/agent/test/helper.bash index e11df0b5..edc2b985 100644 --- a/agent/test/helper.bash +++ b/agent/test/helper.bash @@ -13,6 +13,15 @@ export JAVA_VERSION # issues a warning. That warning confuses our tests, so set -Xshare:off to preempt it. export JAVA_OUTPUT_OPTIONS="-Xshare:off" +# Checks if Java version is at least the given version. +# Usage: if is_java 11; then ... +is_java() { + local required_version="$1" + if [[ $(echo -e "$required_version\n${JAVA_VERSION}" | sort -V | head -n1) == "$required_version" ]]; then + return 0 + fi + return 1 +} _curl() { curl -sfH 'Accept: application/json,*/*' "${@}" @@ -117,17 +126,16 @@ assert_json_not_contains() { refute jq -er "${query}" <<< "${output}" } -_top_level() { - git rev-parse --show-toplevel -} +TOP_LEVEL="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)" +LOG_DIR="$TOP_LEVEL/agent/build/log" find_agent_jar() { if [[ -n "$AGENT_JAR" ]]; then echo "$AGENT_JAR" return fi - find "$(_top_level)" -name 'appmap-[[:digit:]]*.jar' + find "$TOP_LEVEL" -name 'appmap-[[:digit:]]*.jar' } export AGENT_JAR="$(find_agent_jar)" @@ -137,11 +145,19 @@ find_annotation_jar() { echo "$ANNOTATION_JAR" return fi - find "$(_top_level)" -name 'annotation-[[:digit:]]*.jar' + find "$TOP_LEVEL" -name 'annotation-[[:digit:]]*.jar' } export ANNOTATION_JAR="$(find_annotation_jar)" +# absolute gradle wrapper path (in the same directory as this file) +GRADLE_WRAPPER="$TOP_LEVEL/agent/test/gradlew" + +# Shared gradle wrapper function +gradlew() { + "${GRADLE_WRAPPER}" "$@" +} + check_ws_running() { printf 'checking for running web server\n' @@ -154,32 +170,49 @@ check_ws_running() { } wait_for_ws() { - while ! curl -Isf "${WS_URL}" >/dev/null; do - if ! jcmd $JVM_MAIN_CLASS VM.uptime >&/dev/null; then - echo "$JVM_MAIN_CLASS failed" + local timeout=60 + for i in $(seq 1 $timeout); do + if curl -Isf "${WS_URL}" >/dev/null 2>&1; then + echo " ok" >&3 + return 0 + fi + + # Check if the process died + if [[ -n "$WS_PID" ]] && ! kill -0 "$WS_PID" 2>/dev/null; then + echo "Error: Web server process died" >&3 + if [[ -n "$LOG" ]]; then + cat "$LOG" >&3 + fi exit 1 fi + + echo -n '.' >&3 sleep 1 done - printf ' ok\n\n' + + echo "Error: Timeout waiting for web server to start" >&3 + if [[ -n "$LOG" ]]; then + cat "$LOG" >&3 + fi + exit 1 } wait_for_mvn() { local mvn_pid=$1 + export WS_PID=$mvn_pid - # The only thing special about the VM.uptime command is that it's fast, and - # the output is small. - local uptime="jcmd ${JVM_MAIN_CLASS} VM.uptime" - while ! ${uptime} >&/dev/null; do - if ! ps -p $mvn_pid >&/dev/null; then - echo "mvn failed" - cat $LOG + # Just wait for the maven wrapper process to keep running + # The actual JVM will be a child process, and wait_for_ws will verify it started + for i in {1..30}; do + if ! kill -0 $mvn_pid 2>/dev/null; then + echo "Error: Maven process exited prematurely" >&3 + if [[ -n "$LOG" ]]; then + cat "$LOG" >&3 + fi exit 1 fi sleep 1 done - # Final check, this will fail if the server didn't start - ${uptime} >&/dev/null } # Start a PetClinic server. Note that the output from the printf's in this @@ -193,7 +226,6 @@ start_petclinic() { WD=$(getcwd) - export LOG_DIR=$WD/build/log mkdir -p ${LOG_DIR} local fixture_dir="$WD/build/fixtures/spring-petclinic" @@ -225,7 +257,6 @@ start_petclinic() { start_petclinic_fw() { WD=$(getcwd) - export LOG_DIR="$WD/build/log" mkdir -p ${LOG_DIR} local fixture_dir="$WD/build/fixtures/spring-framework-petclinic" @@ -254,25 +285,31 @@ start_petclinic_fw() { } stop_ws() { - # curl doesn't like it when the server exits. Assume the request was - # successful, then wait for the main class to finish. - curl -sXDELETE "${WS_URL}"/exit || true + # If we don't have a PID, we can't stop anything + if [[ -z "$WS_PID" ]]; then + return 0 + fi + + # Try graceful shutdown first + curl -sXDELETE "${WS_URL}/exit" >&/dev/null || true + # Wait up to 30 seconds for process to exit for i in {1..30}; do - if ! jcmd $JVM_MAIN_CLASS VM.uptime >&/dev/null ; then - break; + if ! kill -0 "$WS_PID" 2>/dev/null; then + # Process is gone, success + return 0 fi sleep 1 done - if jcmd $JVM_MAIN_CLASS VM.update >&/dev/null; then - echo "$JVM_MAIN_CLASS didn't exit" - if [[ ! -z "$LOG" ]]; then + # If still running, force kill it + if kill -0 "$WS_PID" 2>/dev/null; then + echo "Warning: Process $WS_PID didn't exit gracefully, force killing" >&3 + kill -9 "$WS_PID" 2>/dev/null || true + if [[ -n "$LOG" ]]; then cat "$LOG" >&3 fi - exit 1; fi - } wait_for_glob() { @@ -287,7 +324,7 @@ wait_for_glob() { } _configure_logging() { - local logConfigDir="$(_top_level)/agent/test/shared" + local logConfigDir="$TOP_LEVEL/agent/test/shared" export APPMAP_DISABLELOGFILE=true export JUL_CONFIG="${logConfigDir}/java-logging.properties" } diff --git a/agent/test/http_client/gradle/wrapper/gradle-wrapper.jar b/agent/test/http_client/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba77..00000000 Binary files a/agent/test/http_client/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/http_client/gradle/wrapper/gradle-wrapper.properties b/agent/test/http_client/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index bdc9a83b..00000000 --- a/agent/test/http_client/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.jar b/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba77..00000000 Binary files a/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.properties b/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index bdc9a83b..00000000 --- a/agent/test/http_client/httpclient/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/http_client/httpclient/gradlew b/agent/test/http_client/httpclient/gradlew deleted file mode 100755 index 79a61d42..00000000 --- a/agent/test/http_client/httpclient/gradlew +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/http_client/httpclient/httpclient.bats b/agent/test/http_client/httpclient/httpclient.bats index e72d0372..362e95d5 100644 --- a/agent/test/http_client/httpclient/httpclient.bats +++ b/agent/test/http_client/httpclient/httpclient.bats @@ -9,7 +9,7 @@ setup_file() { } @test "request without query" { - run --separate-stderr ./gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/vets" + run --separate-stderr gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/vets" assert_json_eq '.events[1].http_client_request.request_method' "GET" assert_json_eq '.events[1].http_client_request.url' "${WS_URL}/vets" @@ -20,7 +20,7 @@ setup_file() { } @test "request with query" { - run --separate-stderr ./gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/owners?lastName=davis" + run --separate-stderr gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/owners?lastName=davis" assert_json_eq '.events[1].http_client_request.url' "${WS_URL}/owners" assert_json_eq '.events[1].message | length' 1 @@ -30,14 +30,14 @@ setup_file() { } @test "request without Content-Type" { - run --separate-stderr ./gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/no-content" + run --separate-stderr gradlew -q -PmainClass=httpclient.HttpClientTest run ${DEBUG} --args "${WS_URL}/no-content" assert_json_eq '.events[1].http_client_request.url' "${WS_URL}/no-content" assert_json_eq '.events[2].http_client_response.status' "200" } @test "request with HttpHost" { - run --separate-stderr ./gradlew -q -PmainClass=httpclient.HttpHostTest run ${DEBUG} --args "${WS_HOST} ${WS_PORT} /owners?lastName=davis" + run --separate-stderr gradlew -q -PmainClass=httpclient.HttpHostTest run ${DEBUG} --args "${WS_HOST} ${WS_PORT} /owners?lastName=davis" assert_json_eq '.events[1].http_client_request.url' "${WS_URL}/owners" assert_json_eq '.events[1].message | length' 1 diff --git a/agent/test/http_client/setup_suite.bash b/agent/test/http_client/setup_suite.bash index 08ee1b25..f5ec9092 100644 --- a/agent/test/http_client/setup_suite.bash +++ b/agent/test/http_client/setup_suite.bash @@ -9,17 +9,12 @@ setup_suite() { _shared_setup start_petclinic >&3 - case "${JAVA_VERSION}" in - 1.8*) - ;& - 11.*) - SPRING_BOOT_VERSION="2.7.18" - ;; - *) - SPRING_BOOT_VERSION="3.2.2" - ;; - esac - export SPRING_BOOT_VERSION + if is_java 17; then + SPRING_BOOT_VERSION="3.2.2" + else + SPRING_BOOT_VERSION="2.7.18" + fi + export SPRING_BOOT_VERSION } teardown_suite() { diff --git a/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.jar b/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba77..00000000 Binary files a/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.properties b/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index bdc9a83b..00000000 --- a/agent/test/http_client/springboot/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/http_client/springboot/gradlew b/agent/test/http_client/springboot/gradlew deleted file mode 100755 index 79a61d42..00000000 --- a/agent/test/http_client/springboot/gradlew +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/http_client/springboot/gradlew.bat b/agent/test/http_client/springboot/gradlew.bat deleted file mode 100644 index 6689b85b..00000000 --- a/agent/test/http_client/springboot/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/agent/test/http_client/springboot/springboot.bats b/agent/test/http_client/springboot/springboot.bats index ff2f93d4..5e0c111b 100644 --- a/agent/test/http_client/springboot/springboot.bats +++ b/agent/test/http_client/springboot/springboot.bats @@ -11,7 +11,7 @@ teardown_file() { } @test "runs in spring boot jar" { - run ./gradlew -q -PSPRING_BOOT_VERSION=$SPRING_BOOT_VERSION clean bootJar + run gradlew -q -PSPRING_BOOT_VERSION=$SPRING_BOOT_VERSION clean bootJar assert_success run java -javaagent:"$(find_agent_jar)" -jar build/libs/springboot-test.jar "$WS_URL" diff --git a/agent/test/httpcore/gradle/wrapper/gradle-wrapper.jar b/agent/test/httpcore/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a..00000000 Binary files a/agent/test/httpcore/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/httpcore/gradle/wrapper/gradle-wrapper.properties b/agent/test/httpcore/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index aa991fce..00000000 --- a/agent/test/httpcore/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/httpcore/gradlew b/agent/test/httpcore/gradlew deleted file mode 100755 index 1b6c7873..00000000 --- a/agent/test/httpcore/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/httpcore/gradlew.bat b/agent/test/httpcore/gradlew.bat deleted file mode 100644 index ac1b06f9..00000000 --- a/agent/test/httpcore/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/agent/test/httpcore/httpcore.bats b/agent/test/httpcore/httpcore.bats old mode 100644 new mode 100755 index 7a511d8c..f49497b3 --- a/agent/test/httpcore/httpcore.bats +++ b/agent/test/httpcore/httpcore.bats @@ -8,28 +8,21 @@ load '../helper' setup_file() { -mkdir -p build/log + agent_root="$BATS_TEST_DIRNAME/../.." + mkdir -p "$agent_root/build/log" -export LOG="$(getcwd)/build/log/httpcore.log" -export SERVER_PORT=9090 -export WS_URL=${WS_URL:-http://localhost:9090} - cd test/httpcore + export LOG="$agent_root/build/log/httpcore.log" + export SERVER_PORT=46406 + export WS_URL=${WS_URL:-http://localhost:${SERVER_PORT}} + + cd "$BATS_TEST_DIRNAME" || exit _configure_logging - printf 'getting set up' >&3 - ./gradlew run --args "${SERVER_PORT}" &> $LOG & - export JVM_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain - - while [ -z "$(curl -sI ${WS_URL} | grep 'HTTP/1.1 200')" ]; do - if ! jcmd $JVM_MAIN_CLASS VM.uptime >&/dev/null; then - echo "$JVM_MAIN_CLASS failed" >&3 - cat $LOG >&3 - exit 1 - fi - printf ' .' >&3 - sleep 1 - done - printf ' ok\n\n' >&3 + printf 'Starting httpcore test server' >&3 + gradlew run --args "${SERVER_PORT}" &> $LOG & + export WS_PID=$! + + wait_for_ws } teardown_file() { diff --git a/agent/test/httpcore/src/test/java/org/apache/http/examples/nio/HelloWorldServer.java b/agent/test/httpcore/src/test/java/org/apache/http/examples/nio/HelloWorldServer.java index 159fc353..58a80048 100644 --- a/agent/test/httpcore/src/test/java/org/apache/http/examples/nio/HelloWorldServer.java +++ b/agent/test/httpcore/src/test/java/org/apache/http/examples/nio/HelloWorldServer.java @@ -12,6 +12,7 @@ import org.apache.http.impl.nio.bootstrap.HttpServer; import org.apache.http.impl.nio.bootstrap.ServerBootstrap; +import org.apache.http.impl.nio.reactor.IOReactorConfig; import org.apache.http.nio.protocol.BasicAsyncRequestHandler; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; @@ -42,12 +43,17 @@ public void handle( @Labels({"server", "runner"}) public HttpServer run(int port) throws IOException, InterruptedException { + final IOReactorConfig ioReactorConfig = IOReactorConfig.custom() + .setSoTimeout(5000) + .setSoReuseAddress(true) + .build(); final HttpServer server = ServerBootstrap.bootstrap() - .setListenerPort(port) - .setServerInfo("HelloWorld/1.1") - .setExceptionLogger(ExceptionLogger.STD_ERR) - .registerHandler("*", new BasicAsyncRequestHandler(new HelloWorldHandler())) - .create(); + .setListenerPort(port) + .setIOReactorConfig(ioReactorConfig) + .setServerInfo("HelloWorld/1.1") + .setExceptionLogger(ExceptionLogger.STD_ERR) + .registerHandler("*", new BasicAsyncRequestHandler(new HelloWorldHandler())) + .create(); server.start(); server.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); diff --git a/agent/test/intellij/intellij.bats b/agent/test/intellij/intellij.bats index e2336bbd..2fa1841b 100644 --- a/agent/test/intellij/intellij.bats +++ b/agent/test/intellij/intellij.bats @@ -5,9 +5,7 @@ init_plugin() { } setup_file() { - if [[ $JAVA_VERSION != 17.* ]]; then - skip "needs Java 17" - fi + is_java 17 || skip "needs Java 17" export AGENT_JAR="$(find_agent_jar)" diff --git a/agent/test/jdbc/build.gradle b/agent/test/jdbc/build.gradle index ea106d16..162848ca 100644 --- a/agent/test/jdbc/build.gradle +++ b/agent/test/jdbc/build.gradle @@ -2,21 +2,25 @@ plugins { id 'org.springframework.boot' version '2.7.0' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' - // id 'com.appland.appmap' version '1.1.0' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' +// suppress warnings about source compatibility +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:-options' +} + repositories { - // mavenLocal() mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.oracle.database.jdbc:ojdbc8:21.9.0.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' } @@ -24,11 +28,16 @@ def appmapJar = "$System.env.AGENT_JAR" test { useJUnitPlatform() + if (System.env.ORACLE_URL) { + inputs.property("oracleUrl", System.env.ORACLE_URL) + } + if (System.env.AGENT_JAR) { + inputs.file(System.env.AGENT_JAR) + } jvmArgs += [ - "-javaagent:${appmapJar}", - "-Dappmap.config.file=appmap.yml", - "-Djava.util.logging.config.file=${System.env.JUL_CONFIG}" - // "-Dappmap.debug=true", - // "-Dappmap.debug.file=../../build/log/jdbc-appmap.log" + "-javaagent:${appmapJar}", + "-Dappmap.config.file=appmap.yml", + "-Djava.util.logging.config.file=${System.env.JUL_CONFIG}", + // "-Dappmap.debug=true", ] } diff --git a/agent/test/jdbc/docker-compose.yml b/agent/test/jdbc/docker-compose.yml new file mode 100644 index 00000000..a2ed3ffa --- /dev/null +++ b/agent/test/jdbc/docker-compose.yml @@ -0,0 +1,10 @@ +# This docker-compose file is used for local, manual execution of the Oracle JDBC integration tests. +# It starts a standalone Oracle database for testing purposes. +version: '3.8' +services: + oracle: + image: docker.io/gvenzl/oracle-free:slim-faststart + ports: + - "1521:1521" + environment: + ORACLE_PASSWORD: oracle diff --git a/agent/test/jdbc/gradle/wrapper/gradle-wrapper.jar b/agent/test/jdbc/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a..00000000 Binary files a/agent/test/jdbc/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/jdbc/gradle/wrapper/gradle-wrapper.properties b/agent/test/jdbc/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index aa991fce..00000000 --- a/agent/test/jdbc/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/jdbc/gradlew b/agent/test/jdbc/gradlew deleted file mode 100755 index 1b6c7873..00000000 --- a/agent/test/jdbc/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/jdbc/gradlew.bat b/agent/test/jdbc/gradlew.bat deleted file mode 100644 index ac1b06f9..00000000 --- a/agent/test/jdbc/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/agent/test/jdbc/helper.bash b/agent/test/jdbc/helper.bash new file mode 100644 index 00000000..9499bb6f --- /dev/null +++ b/agent/test/jdbc/helper.bash @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +# generate_sql_snapshots +# +# Generates .sql files in from .appmap.json files in +# that match . +generate_sql_snapshots() { + local appmap_dir="$1" + local target_dir="$2" + local file_glob="$3" + + mkdir -p "$target_dir" + + for f in "$appmap_dir"/$file_glob; do + if [ -f "$f" ]; then + local snapshot_name + snapshot_name=$(basename "$f" .appmap.json).sql + jq -r '.events[] | select(.sql_query) | .sql_query.sql' "$f" >"$target_dir/$snapshot_name" + fi + done +} + +# assert_all_calls_returned [ ...] +# +# Validates that all 'call' events in AppMap JSON file(s) have corresponding 'return' events. +# Returns failure if any call IDs are missing their return events (orphaned calls). +# Supports multiple files as arguments. +assert_all_calls_returned() { + local has_errors=0 + + for json_file in "$@"; do + if [ ! -f "$json_file" ]; then + echo "File not found: $json_file" + has_errors=1 + continue + fi + + # Extract IDs that exist as 'call' but not as a 'return' parent_id + local orphans + orphans=$(jq -e -r '.events | + (map(select(.event == "call").id) // []) as $calls | + (map(select(.event == "return").parent_id) // []) as $returns | + ($calls - $returns)[] + ' "$json_file" 2>/dev/null) + + # If orphans is not empty, print them and mark as error + if [[ -n "$orphans" ]]; then + echo "Validation Failed: $json_file" + echo "The following call IDs are missing a return event:" + echo "$orphans" + has_errors=1 + fi + done + + return $has_errors +} + +# requires_oracle +# +# Skips the current test if ORACLE_URL environment variable is not set. +# Used to conditionally run Oracle-specific tests. +requires_oracle() { + if [ -z "$ORACLE_URL" ]; then + skip "ORACLE_URL is not set" + fi +} diff --git a/agent/test/jdbc/jdbc.bats b/agent/test/jdbc/jdbc.bats old mode 100644 new mode 100755 index 71bb7c26..57e870ed --- a/agent/test/jdbc/jdbc.bats +++ b/agent/test/jdbc/jdbc.bats @@ -1,31 +1,32 @@ #!/usr/bin/env bats load '../helper' +load 'helper' setup_file() { - cd test/jdbc + cd "$BATS_TEST_DIRNAME" || exit 1 _configure_logging - ./gradlew -q clean + gradlew -q clean } setup() { rm -rf tmp/appmap } -@test "successful test" { - run ./gradlew -q test --tests 'CustomerRepositoryTests.testFindFromBogusTable' +@test "h2 successful test" { + run gradlew -q test --tests 'CustomerRepositoryTests.testFindFromBogusTable' --rerun-tasks assert_success output="$(<./tmp/appmap/junit/com_example_accessingdatajpa_CustomerRepositoryTests_testFindFromBogusTable.appmap.json)" assert_json_eq '.metadata.test_status' succeeded - assert_json_eq '.events | length' 6 - assert_json_eq '.events[3].exceptions | length' 1 - assert_json_eq '.events[3].exceptions[0].class' org.h2.jdbc.JdbcSQLSyntaxErrorException + assert_json_eq '.events | length' 4 + assert_json_eq '.events[2].exceptions | length' 3 + assert_json_eq '.events[2].exceptions[2].class' org.h2.jdbc.JdbcSQLSyntaxErrorException } -@test "failing test" { - run ./gradlew -q test --tests 'CustomerRepositoryTests.testFails' +@test "h2 failing test" { + run gradlew -q test --tests 'CustomerRepositoryTests.testFails' --rerun-tasks assert_failure output="$(<./tmp/appmap/junit/com_example_accessingdatajpa_CustomerRepositoryTests_testFails.appmap.json)" @@ -33,5 +34,64 @@ setup() { assert_json_eq '.metadata.test_failure.message' 'expected: but was: ' } +# Requires a running Oracle instance. +# Locally: docker-compose up -d (in agent/test/jdbc) +# CI: Service is configured in .github/workflows/build-and-test.yml +@test "oracle jpa test" { + requires_oracle + run gradlew -q test --tests 'OracleRepositoryTests' --rerun-tasks + assert_success + + map_file="tmp/appmap/junit/com_example_accessingdatajpa_OracleRepositoryTests_testFindByLastName.appmap.json" + [ -f "$map_file" ] + output="$(<"$map_file")" + assert_json_eq '.metadata.test_status' succeeded + event_count=$(echo "$output" | jq '.events | length') + if [ "$event_count" -le 0 ]; then + echo "Expected event count to be greater than 0, but it was $event_count" + return 1 + fi +} + +# To regenerate the SQL snapshots, run ./regenerate_jdbc_snapshots.sh from this directory. +@test "h2 pure jdbc test suite (snapshot)" { + export -n ORACLE_URL + run gradlew -q test --tests 'PureJDBCTests' --rerun-tasks + assert_success + + local appmap_dir="tmp/appmap/junit" + local snapshot_dir="snapshots/h2" + local test_output_dir + test_output_dir="$(mktemp -d)" + + generate_sql_snapshots "$appmap_dir" "$test_output_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json" + + run assert_all_calls_returned "$appmap_dir"/*.appmap.json + assert_success + run diff -u <(cd "$snapshot_dir" && grep -ri . | sort -s -t: -k1,1) <(cd "$test_output_dir" && grep -ri . | sort -s -t: -k1,1) + assert_success "Snapshot mismatch" + rm -rf "$test_output_dir" +} + +@test "oracle pure jdbc test suite (snapshot)" { + requires_oracle + export ORACLE_URL + run gradlew -q test --tests 'PureJDBCTests' --rerun-tasks + assert_success + + local appmap_dir="tmp/appmap/junit" + local snapshot_dir="snapshots/oracle" + local test_output_dir + test_output_dir="$(mktemp -d)" + + generate_sql_snapshots "$appmap_dir" "$test_output_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json" + + run assert_all_calls_returned "$appmap_dir"/*.appmap.json + assert_success + run diff -u <(cd "$snapshot_dir" && grep -ri . | sort -s -t: -k1,1) <(cd "$test_output_dir" && grep -ri . | sort -s -t: -k1,1) + assert_success "Snapshot mismatch" + + rm -rf "$test_output_dir" +} diff --git a/agent/test/jdbc/regenerate_jdbc_snapshots.sh b/agent/test/jdbc/regenerate_jdbc_snapshots.sh new file mode 100755 index 00000000..348c4269 --- /dev/null +++ b/agent/test/jdbc/regenerate_jdbc_snapshots.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# This script regenerates the SQL snapshots for the PureJDBCTests. +# It should be run from the agent/test/jdbc directory. +# +# Usage: +# ./regenerate_jdbc_snapshots.sh # Regenerate H2 snapshots +# ORACLE_URL=... ./regenerate_jdbc_snapshots.sh # Regenerate Oracle snapshots + +# Source helper.bash to get _find_agent_jar function +# Set BATS_TEST_DIR so helper.bash can locate files correctly +export BATS_TEST_DIR="$(pwd)" +source ../helper.bash +source ./helper.bash + +find_agent_jar +if [[ -z "$AGENT_JAR" ]]; then + echo "ERROR: Agent JAR not found by helper.bash. Please ensure the agent is built." >&2 + exit 1 +fi + +export AGENT_JAR + +regenerate_snapshots() { + local db_type="$1" + local snapshot_dir="$PWD/snapshots/$db_type" + local appmap_dir="$PWD/tmp/appmap/junit" + + echo "INFO: Regenerating $db_type snapshots..." + + # Clear old snapshots and appmap dirs + rm -f "$snapshot_dir"/* + rm -f "$appmap_dir"/com_example_accessingdatajpa_PureJDBCTests_*.appmap.json + + # Run the tests to generate fresh AppMaps + ../gradlew -q test --tests 'PureJDBCTests' --rerun-tasks + + echo "INFO: Generating raw SQL snapshots for $db_type..." + + # Generate new raw SQL snapshots + generate_sql_snapshots "$appmap_dir" "$snapshot_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json" + + echo "INFO: $db_type snapshots regenerated successfully in $snapshot_dir" +} + +if [[ -z "${ORACLE_URL:-}" ]]; then + echo "WARNING: ORACLE_URL is not set. Skipping Oracle snapshot regeneration." >&2 + echo "To regenerate Oracle snapshots, set ORACLE_URL and run this script again." >&2 +else + export ORACLE_URL + regenerate_snapshots "oracle" +fi + +unset ORACLE_URL +regenerate_snapshots "h2" diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql new file mode 100644 index 00000000..9f689b39 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql @@ -0,0 +1,6 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (5, 'I', 'J'); +INSERT INTO customer (id, first_name, last_name) VALUES (6, 'K', 'L') +INSERT INTO customer (id, first_name, last_name) VALUES (7, 'M', 'N'); +INSERT INTO customer (id, first_name, last_name) VALUES (8, 'O', 'P') + +SELECT * FROM customer WHERE batch error = ? diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql new file mode 100644 index 00000000..afdd2476 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql @@ -0,0 +1,6 @@ + call test_proc(?, ?) -- call 1 + call test_proc(?, ?) -- call 1 + call test_proc(?, ?) -- call 2 + call test_proc(?, ?) -- call 2 + call test_proc(?, ?) -- call 3 + call test_proc(?, ?) -- call 3 diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql new file mode 100644 index 00000000..739a5c57 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql @@ -0,0 +1 @@ +SELECT * FROM non_existent_table diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql new file mode 100644 index 00000000..ce73a32d --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql @@ -0,0 +1 @@ +SELECT * FROM customer diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql new file mode 100644 index 00000000..bba438c3 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql @@ -0,0 +1,8 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (20, 'A', 'B') +UPDATE customer SET first_name = 'C' WHERE id = 20 +DELETE FROM customer WHERE id = 20 +INSERT INTO customer (id, first_name, last_name) VALUES (21, 'D', 'E') +UPDATE customer SET first_name = 'F' WHERE id = 21 +UPDATE customer SET first_name = 'G' WHERE id = 21 +UPDATE customer SET first_name = 'H' WHERE id = 21 +UPDATE customer SET first_name = 'I' WHERE id = 21 diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testNativeSQL.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testNativeSQL.sql new file mode 100644 index 00000000..e69de29b diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql new file mode 100644 index 00000000..bac37fe8 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql @@ -0,0 +1,12 @@ +SELECT * FROM customer WHERE id = ? -- op 1 +SELECT * FROM customer WHERE id = ? -- op 1 +SELECT first_name FROM customer WHERE id = ? -- op 2 +SELECT first_name FROM customer WHERE id = ? -- op 2 +UPDATE customer SET last_name = ? WHERE id = ? -- op 3 +UPDATE customer SET last_name = ? WHERE id = ? -- op 3 +UPDATE customer SET first_name = ? WHERE id = ? -- op 4 +UPDATE customer SET first_name = ? WHERE id = ? -- op 4 +UPDATE customer SET last_name = ? WHERE id = ? -- op 5 +UPDATE customer SET last_name = ? WHERE id = ? -- op 5 +SELECT count(*) FROM customer WHERE id = ? -- op 6 +SELECT count(*) FROM customer WHERE id = ? -- op 6 diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql new file mode 100644 index 00000000..6cd539f0 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql @@ -0,0 +1,4 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?); +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?) +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?); +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?) diff --git a/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql new file mode 100644 index 00000000..a31275b2 --- /dev/null +++ b/agent/test/jdbc/snapshots/h2/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql @@ -0,0 +1,4 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (1, 'A', 'B') +UPDATE customer SET first_name = 'C' WHERE id = 1 +DELETE FROM customer WHERE id = 1 +INSERT INTO customer (id, first_name, last_name) VALUES (2, 'X', 'Y') diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql new file mode 100644 index 00000000..9f689b39 --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testBatch.sql @@ -0,0 +1,6 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (5, 'I', 'J'); +INSERT INTO customer (id, first_name, last_name) VALUES (6, 'K', 'L') +INSERT INTO customer (id, first_name, last_name) VALUES (7, 'M', 'N'); +INSERT INTO customer (id, first_name, last_name) VALUES (8, 'O', 'P') + +SELECT * FROM customer WHERE batch error = ? diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql new file mode 100644 index 00000000..0823fdfe --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testCallableStatement.sql @@ -0,0 +1,6 @@ +{call test_proc(?, ?)} -- call 1 +{call test_proc(?, ?)} -- call 1 +{call test_proc(?, ?)} -- call 2 +{call test_proc(?, ?)} -- call 2 +{call test_proc(?, ?)} -- call 3 +{call test_proc(?, ?)} -- call 3 diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql new file mode 100644 index 00000000..6ecb2a6d --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExceptions.sql @@ -0,0 +1,2 @@ +SELECT * FROM non_existent_table +INVALID SQL diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql new file mode 100644 index 00000000..ce73a32d --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteQuery.sql @@ -0,0 +1 @@ +SELECT * FROM customer diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql new file mode 100644 index 00000000..bba438c3 --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testExecuteUpdate.sql @@ -0,0 +1,8 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (20, 'A', 'B') +UPDATE customer SET first_name = 'C' WHERE id = 20 +DELETE FROM customer WHERE id = 20 +INSERT INTO customer (id, first_name, last_name) VALUES (21, 'D', 'E') +UPDATE customer SET first_name = 'F' WHERE id = 21 +UPDATE customer SET first_name = 'G' WHERE id = 21 +UPDATE customer SET first_name = 'H' WHERE id = 21 +UPDATE customer SET first_name = 'I' WHERE id = 21 diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testNativeSQL.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testNativeSQL.sql new file mode 100644 index 00000000..e69de29b diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql new file mode 100644 index 00000000..bac37fe8 --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPrepareStatement.sql @@ -0,0 +1,12 @@ +SELECT * FROM customer WHERE id = ? -- op 1 +SELECT * FROM customer WHERE id = ? -- op 1 +SELECT first_name FROM customer WHERE id = ? -- op 2 +SELECT first_name FROM customer WHERE id = ? -- op 2 +UPDATE customer SET last_name = ? WHERE id = ? -- op 3 +UPDATE customer SET last_name = ? WHERE id = ? -- op 3 +UPDATE customer SET first_name = ? WHERE id = ? -- op 4 +UPDATE customer SET first_name = ? WHERE id = ? -- op 4 +UPDATE customer SET last_name = ? WHERE id = ? -- op 5 +UPDATE customer SET last_name = ? WHERE id = ? -- op 5 +SELECT count(*) FROM customer WHERE id = ? -- op 6 +SELECT count(*) FROM customer WHERE id = ? -- op 6 diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql new file mode 100644 index 00000000..6cd539f0 --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testPreparedStatementBatch.sql @@ -0,0 +1,4 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?); +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?) +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?); +INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?) diff --git a/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql new file mode 100644 index 00000000..a31275b2 --- /dev/null +++ b/agent/test/jdbc/snapshots/oracle/com_example_accessingdatajpa_PureJDBCTests_testStatementExecute.sql @@ -0,0 +1,4 @@ +INSERT INTO customer (id, first_name, last_name) VALUES (1, 'A', 'B') +UPDATE customer SET first_name = 'C' WHERE id = 1 +DELETE FROM customer WHERE id = 1 +INSERT INTO customer (id, first_name, last_name) VALUES (2, 'X', 'Y') diff --git a/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/OracleRepositoryTests.java b/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/OracleRepositoryTests.java new file mode 100644 index 00000000..1c0456b0 --- /dev/null +++ b/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/OracleRepositoryTests.java @@ -0,0 +1,41 @@ +package com.example.accessingdatajpa; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import java.util.List; +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("oracle") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@EnabledIfEnvironmentVariable(named = "ORACLE_URL", matches = ".*") +public class OracleRepositoryTests { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private CustomerRepository customers; + + @Autowired + private DataSource dataSource; + + @Test + public void testFindByLastName() { + Customer customer = new Customer("Oracle", "User"); + entityManager.persist(customer); + + List findByLastName = customers.findByLastName(customer.getLastName()); + + assertThat(findByLastName).extracting(Customer::getLastName) + .containsOnly(customer.getLastName()); + } +} diff --git a/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/PureJDBCTests.java b/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/PureJDBCTests.java new file mode 100644 index 00000000..0d4b6f82 --- /dev/null +++ b/agent/test/jdbc/src/test/java/com/example/accessingdatajpa/PureJDBCTests.java @@ -0,0 +1,303 @@ +package com.example.accessingdatajpa; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.SAME_THREAD) +public class PureJDBCTests { + + private Connection connection; + private boolean isOracle; + + @BeforeEach + public void setUp() throws SQLException { + String oracleUrl = System.getenv("ORACLE_URL"); + + // Determine which database to use + if (oracleUrl != null && !oracleUrl.isEmpty()) { + // Use Oracle + isOracle = true; + String oracleUsername = System.getenv("ORACLE_USERNAME"); + if (oracleUsername == null) { + oracleUsername = "system"; + } + String oraclePassword = System.getenv("ORACLE_PASSWORD"); + if (oraclePassword == null) { + oraclePassword = "oracle"; + } + connection = DriverManager.getConnection(oracleUrl, oracleUsername, oraclePassword); + } else { + // Use H2 + isOracle = false; + connection = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", ""); + } + + // Create table with database-specific DDL + try (Statement statement = connection.createStatement()) { + String createTableSql; + if (isOracle) { + createTableSql = "CREATE TABLE customer (id NUMBER(19,0) NOT NULL, first_name VARCHAR2(255 CHAR), last_name VARCHAR2(255 CHAR), PRIMARY KEY (id))"; + } else { + createTableSql = "CREATE TABLE customer (id BIGINT NOT NULL, first_name VARCHAR(255), last_name VARCHAR(255), PRIMARY KEY (id))"; + } + statement.execute("DROP TABLE IF EXISTS customer"); + statement.execute(createTableSql); + + // Create a test procedure for CallableStatement tests + if (isOracle) { + statement.execute("CREATE OR REPLACE PROCEDURE test_proc(p1 IN VARCHAR2, p2 IN VARCHAR2) AS BEGIN NULL; END;"); + } else { + statement.execute("CREATE ALIAS IF NOT EXISTS test_proc FOR \"java.lang.System.setProperty\""); + } + } + } + + @AfterEach + public void tearDown() throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE customer"); + } + connection.close(); + } + + @Test + void testStatementExecute() throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.execute("INSERT INTO customer (id, first_name, last_name) VALUES (1, 'A', 'B')"); + stmt.execute("UPDATE customer SET first_name = 'C' WHERE id = 1", Statement.NO_GENERATED_KEYS); + stmt.execute("DELETE FROM customer WHERE id = 1", new int[] { 1 }); + stmt.execute("INSERT INTO customer (id, first_name, last_name) VALUES (2, 'X', 'Y')", new String[] { "id" }); + } + } + + // Note this test should generate no SQL in the AppMap - this was a bug in the + // agent + @Test + void testNativeSQL() throws Exception { + // Test nativeSQL method which converts SQL to the database's native grammar + String sql = "SELECT * FROM customer WHERE id = ?"; + String nativeSql = connection.nativeSQL(sql); + + assertTrue(nativeSql != null); + assertTrue(nativeSql.contains("customer")); + } + + @Test + void testBatch() throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (3, 'E', 'F')"); + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (4, 'G', 'H')"); + stmt.clearBatch(); + + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (5, 'I', 'J')"); + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (6, 'K', 'L')"); + stmt.executeBatch(); + + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (7, 'M', 'N')"); + stmt.addBatch("INSERT INTO customer (id, first_name, last_name) VALUES (8, 'O', 'P')"); + stmt.executeLargeBatch(); + + // This should generate empty SQL in the AppMap + stmt.executeLargeBatch(); + + // Let's try invalid sequel to cause an exception + try { + stmt.addBatch("SELECT * FROM customer WHERE batch error = ?"); + stmt.executeBatch(); + } catch (SQLException e) { + // expected + } + } + } + + @Test + void testPreparedStatementBatch() throws Exception { + String sql = "INSERT INTO customer (id, first_name, last_name) VALUES (?, ?, ?)"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setLong(1, 9); + pstmt.setString(2, "Q"); + pstmt.setString(3, "R"); + pstmt.addBatch(); + pstmt.clearBatch(); + + pstmt.setLong(1, 10); + pstmt.setString(2, "S"); + pstmt.setString(3, "T"); + pstmt.addBatch(); + pstmt.setLong(1, 11); + pstmt.setString(2, "U"); + pstmt.setString(3, "V"); + pstmt.addBatch(); + pstmt.executeBatch(); + + pstmt.setLong(1, 12); + pstmt.setString(2, "W"); + pstmt.setString(3, "X"); + pstmt.addBatch(); + pstmt.setLong(1, 13); + pstmt.setString(2, "Y"); + pstmt.setString(3, "Z"); + pstmt.addBatch(); + pstmt.executeLargeBatch(); + } + } + + @Test + void testCallableStatement() throws Exception { + // Each call uses slightly different SQL to ensure unique identification in + // AppMap + String sql1 = "{call test_proc(?, ?)} -- call 1"; + String sql2 = "{call test_proc(?, ?)} -- call 2"; + String sql3 = "{call test_proc(?, ?)} -- call 3"; + + // Test various prepareCall overloads and execute multiple times + try (CallableStatement cstmt = connection.prepareCall(sql1)) { + cstmt.setString(1, "key1.1"); + cstmt.setString(2, "val1.1"); + cstmt.execute(); + cstmt.setString(1, "key1.2"); + cstmt.setString(2, "val1.2"); + cstmt.execute(); + } + + try (CallableStatement cstmt = connection.prepareCall(sql2, ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY)) { + cstmt.setString(1, "key2.1"); + cstmt.setString(2, "val2.1"); + cstmt.execute(); + cstmt.setString(1, "key2.2"); + cstmt.setString(2, "val2.2"); + cstmt.execute(); + } + + try (CallableStatement cstmt = connection.prepareCall(sql3, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT)) { + cstmt.setString(1, "key3.1"); + cstmt.setString(2, "val3.1"); + cstmt.execute(); + cstmt.setString(1, "key3.2"); + cstmt.setString(2, "val3.2"); + cstmt.execute(); + } + } + + @Test + void testExecuteUpdate() throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("INSERT INTO customer (id, first_name, last_name) VALUES (20, 'A', 'B')"); + stmt.executeUpdate("UPDATE customer SET first_name = 'C' WHERE id = 20", Statement.NO_GENERATED_KEYS); + stmt.executeUpdate("DELETE FROM customer WHERE id = 20", new int[] { 1 }); + stmt.executeUpdate("INSERT INTO customer (id, first_name, last_name) VALUES (21, 'D', 'E')", + new String[] { "id" }); + + // Test executeLargeUpdate overloads + stmt.executeLargeUpdate("UPDATE customer SET first_name = 'F' WHERE id = 21"); + stmt.executeLargeUpdate("UPDATE customer SET first_name = 'G' WHERE id = 21", Statement.NO_GENERATED_KEYS); + stmt.executeLargeUpdate("UPDATE customer SET first_name = 'H' WHERE id = 21", new int[] { 1 }); + stmt.executeLargeUpdate("UPDATE customer SET first_name = 'I' WHERE id = 21", new String[] { "id" }); + } + } + + @Test + void testExecuteQuery() throws Exception { + try (Statement stmt = connection.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM customer")) { + while (rs.next()) { + } + } + } + } + + @Test + void testPrepareStatement() throws Exception { + // Unique SQL for each overload + String sql1 = "SELECT * FROM customer WHERE id = ? -- op 1"; + String sql2 = "SELECT first_name FROM customer WHERE id = ? -- op 2"; + String sql3 = "UPDATE customer SET last_name = ? WHERE id = ? -- op 3"; + String sql4 = "UPDATE customer SET first_name = ? WHERE id = ? -- op 4"; + String sql5 = "UPDATE customer SET last_name = ? WHERE id = ? -- op 5"; + String sql6 = "SELECT count(*) FROM customer WHERE id = ? -- op 6"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql1)) { + pstmt.setLong(1, 1); + pstmt.execute(); + pstmt.setLong(1, 2); + pstmt.execute(); + } + try (PreparedStatement pstmt = connection.prepareStatement(sql2, Statement.RETURN_GENERATED_KEYS)) { + pstmt.setLong(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + } + pstmt.setLong(1, 2); + try (ResultSet rs = pstmt.executeQuery()) { + } + } + try (PreparedStatement pstmt = connection.prepareStatement(sql3, new int[] { 1 })) { + pstmt.setString(1, "LastName3.1"); + pstmt.setLong(2, 1); + pstmt.executeUpdate(); + pstmt.setString(1, "LastName3.2"); + pstmt.setLong(2, 1); + pstmt.executeUpdate(); + } + try (PreparedStatement pstmt = connection.prepareStatement(sql4, ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY)) { + pstmt.setString(1, "Name1"); + pstmt.setLong(2, 21); + pstmt.executeLargeUpdate(); + pstmt.setString(1, "Name2"); + pstmt.setLong(2, 21); + pstmt.executeLargeUpdate(); + } + try (PreparedStatement pstmt = connection.prepareStatement(sql5, ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT)) { + pstmt.setString(1, "LName1"); + pstmt.setLong(2, 21); + pstmt.executeUpdate(); + pstmt.setString(1, "LName2"); + pstmt.setLong(2, 21); + pstmt.executeUpdate(); + } + try (PreparedStatement pstmt = connection.prepareStatement(sql6, new String[] { "id" })) { + pstmt.setLong(1, 1); + pstmt.execute(); + pstmt.setLong(1, 2); + pstmt.execute(); + } + } + + @Test + void testExceptions() throws Exception { + try (Statement stmt = connection.createStatement()) { + try { + stmt.execute("SELECT * FROM non_existent_table"); + } catch (SQLException e) { + // Expected + } + } + + try { + // note this will fail to prepare on h2 but only fail on execution on oracle + try (PreparedStatement stmt = connection.prepareStatement("INVALID SQL")) { + stmt.execute(); + } + } catch (SQLException e) { + // Expected + } + } +} diff --git a/agent/test/jdbc/src/test/resources/application-oracle.properties b/agent/test/jdbc/src/test/resources/application-oracle.properties new file mode 100644 index 00000000..d38c8ab0 --- /dev/null +++ b/agent/test/jdbc/src/test/resources/application-oracle.properties @@ -0,0 +1,6 @@ +spring.datasource.url=${ORACLE_URL:jdbc:oracle:thin:@localhost:1521} +spring.datasource.username=${ORACLE_USERNAME:system} +spring.datasource.password=${ORACLE_PASSWORD:oracle} +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver +spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect +spring.jpa.hibernate.ddl-auto=create-drop diff --git a/agent/test/petclinic-fw/petclinic-fw.bats b/agent/test/petclinic-fw/petclinic-fw.bats index 5910b986..b7167d26 100644 --- a/agent/test/petclinic-fw/petclinic-fw.bats +++ b/agent/test/petclinic-fw/petclinic-fw.bats @@ -7,9 +7,7 @@ load '../petclinic-shared/static-resources.bash' load '../petclinic-shared/message-params.bash' setup_file() { - if [[ $JAVA_VERSION != 17.* ]]; then - skip "needs Java 17" - fi + is_java 17 || skip "needs Java 17" export FIXTURE_DIR=build/fixtures/spring-framework-petclinic _shared_setup diff --git a/agent/test/petclinic-shared/shared-setup.bash b/agent/test/petclinic-shared/shared-setup.bash index 397b5348..0789e21e 100644 --- a/agent/test/petclinic-shared/shared-setup.bash +++ b/agent/test/petclinic-shared/shared-setup.bash @@ -1,5 +1,5 @@ _shared_setup() { - local fixtureSrc="$(_top_level)/agent/src/test/fixture" + local fixtureSrc="$TOP_LEVEL/agent/src/test/fixture" tar -C "${fixtureSrc}/shared" -c -f - . | tar -C "${FIXTURE_DIR}" -x -f - local testdir="$(basename ${BATS_TEST_DIRNAME})" if [[ -d "${fixtureSrc}/${testdir}" ]]; then diff --git a/agent/test/settings.gradle b/agent/test/settings.gradle new file mode 100644 index 00000000..85920239 --- /dev/null +++ b/agent/test/settings.gradle @@ -0,0 +1 @@ +/* Just a placeholder so gradle wrapper recognizes this as a gradle project */ \ No newline at end of file diff --git a/agent/test/spark/gradle/wrapper/gradle-wrapper.jar b/agent/test/spark/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index c1962a79..00000000 Binary files a/agent/test/spark/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/spark/gradle/wrapper/gradle-wrapper.properties b/agent/test/spark/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37aef8d3..00000000 --- a/agent/test/spark/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/agent/test/spark/gradlew b/agent/test/spark/gradlew deleted file mode 100755 index aeb74cbb..00000000 --- a/agent/test/spark/gradlew +++ /dev/null @@ -1,245 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/agent/test/spark/gradlew.bat b/agent/test/spark/gradlew.bat deleted file mode 100644 index 93e3f59f..00000000 --- a/agent/test/spark/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/agent/test/spark/spark.bats b/agent/test/spark/spark.bats index 4db1defb..58c52180 100644 --- a/agent/test/spark/spark.bats +++ b/agent/test/spark/spark.bats @@ -10,8 +10,11 @@ setup_file() { _configure_logging cd test/spark - ./gradlew -PappmapJar="${AGENT_JAR}" run & - export JVM_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain + export LOG="$BATS_TEST_DIRNAME/../../build/log/spark.log" + mkdir -p "$(dirname "$LOG")" + + gradlew -PappmapJar="${AGENT_JAR}" run &> "$LOG" & + export WS_PID=$! wait_for_ws } diff --git a/agent/test/test-frameworks/frameworks.bats b/agent/test/test-frameworks/frameworks.bats index 20d6d674..9887ae0e 100644 --- a/agent/test/test-frameworks/frameworks.bats +++ b/agent/test/test-frameworks/frameworks.bats @@ -17,7 +17,7 @@ setup() { run_framework_test() { local framework=$1 local test="$2" - run ./gradlew cleanTest test_${framework} --tests "$test" + run gradlew cleanTest test_${framework} --tests "$test" } @test "metadata captured on success for junit" { diff --git a/agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.jar b/agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135c..00000000 Binary files a/agent/test/test-frameworks/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/agent/test/test-frameworks/gradlew.bat b/agent/test/test-frameworks/gradlew.bat deleted file mode 100644 index 6689b85b..00000000 --- a/agent/test/test-frameworks/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/annotation/build.gradle b/annotation/build.gradle index 228bd7ba..45119a50 100644 --- a/annotation/build.gradle +++ b/annotation/build.gradle @@ -12,7 +12,7 @@ plugins { } repositories { - jcenter() + mavenCentral() } @@ -40,8 +40,7 @@ version = parameterizedVersion ?: ( travisVersionValid ? travisVersion : default group = publishGroupId compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + options.release = 8 } check { @@ -50,7 +49,7 @@ check { // extra artifacts used in publishing task sourcesJar(type: Jar) { from sourceSets.main.allJava - classifier = 'sources' + archiveClassifier = 'sources' } // for some reason this block generates empty Javadoc @@ -59,7 +58,7 @@ javadoc { exclude 'com/appland/**' } task mockJavadocJar(type: Jar) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } @@ -74,8 +73,8 @@ publishing { // 1. coordinates (parameterized) - groupId publishGroupId - artifactId publishArtifactId + groupId = publishGroupId + artifactId = publishArtifactId // version defined globally diff --git a/build.gradle b/build.gradle index da8cc324..d1fad7b0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ */ plugins { - id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' id "com.dorongold.task-tree" version "2.1.0" } @@ -46,7 +46,11 @@ subprojects { apply plugin: 'checkstyle' checkstyle { - toolVersion = "9.3" + toolVersion = "13.0.0" configFile = rootProject.file("config/checkstyle/checkstyle.xml") } + + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:-options" + } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 874a521b..3ed1e411 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,12 +1,12 @@ buildscript { repositories { - jcenter() + mavenCentral() maven { - url "https://plugins.gradle.org/m2/" + url = "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' + classpath 'com.gradleup.shadow:shadow-gradle-plugin:9.3.1' } } @@ -16,14 +16,14 @@ plugins { } repositories { - jcenter() + mavenCentral() maven { - url "https://plugins.gradle.org/m2/" + url = "https://plugins.gradle.org/m2/" } } dependencies { - implementation 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' + implementation 'com.gradleup.shadow:shadow-gradle-plugin:9.3.1' } diff --git a/buildSrc/src/main/groovy/com/appland/tasks/ShadowRelocation.groovy b/buildSrc/src/main/groovy/com/appland/tasks/ShadowRelocation.groovy index 7f93a9ec..2eb073f6 100644 --- a/buildSrc/src/main/groovy/com/appland/tasks/ShadowRelocation.groovy +++ b/buildSrc/src/main/groovy/com/appland/tasks/ShadowRelocation.groovy @@ -25,7 +25,7 @@ class ShadowRelocation extends DefaultTask { @InputFiles @Optional List getConfigurations() { - return target.configurations + return target.configurations.get().toList() } @TaskAction diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a..f8e1ee31 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fce..23449a2b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..adff685a 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,22 +132,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -165,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..e509b2dd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/runtime/build.gradle b/runtime/build.gradle index b134b5dd..aaeac339 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -3,13 +3,11 @@ plugins { } repositories { - jcenter() mavenCentral() } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + options.release = 8 } // Don't put dependencies here.