diff --git a/.github/workflows/cf_tests.yml b/.github/workflows/cf_tests.yml index 0983675..9778ff0 100644 --- a/.github/workflows/cf_tests.yml +++ b/.github/workflows/cf_tests.yml @@ -15,6 +15,6 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Build agent - run: mvn package -U -B -pl agent --also-make -DskipTests + run: make package-agent - name: Run CF Tests - run: mvn verify -U -B -P cf-it-tests -pl it-tests/cf-tests --also-make \ No newline at end of file + run: make cf-tests \ No newline at end of file diff --git a/.github/workflows/on_push.yml b/.github/workflows/on_push.yml index 2bee699..5bf9510 100644 --- a/.github/workflows/on_push.yml +++ b/.github/workflows/on_push.yml @@ -21,7 +21,7 @@ jobs: cache: 'maven' - name: Run tests - run: mvn -U -B clean verify + run: make test # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph @@ -39,7 +39,7 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Check Formatting - run: mvn -U -B validate -Plint,examples,cf-it-tests + run: make lint pmd: runs-on: ubuntu-latest @@ -53,7 +53,7 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Run PMD check - run: mvn -U -B verify -Ppmd + run: make pmd docs: @@ -68,5 +68,5 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Run docs check - run: mvn -s .ci-settings.xml clean package javadoc:jar -DskipTests -P release-ossrh -B -U -pl agent,deep --also-make + run: make docs diff --git a/.idea/google-java-format.xml b/.idea/google-java-format.xml new file mode 100644 index 0000000..8b57f45 --- /dev/null +++ b/.idea/google-java-format.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/kubernetes-settings.xml b/.idea/kubernetes-settings.xml new file mode 100644 index 0000000..b8b3279 --- /dev/null +++ b/.idea/kubernetes-settings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c723228..612ec51 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.idea/runConfigurations/VisitorTest.xml b/.idea/runConfigurations/VisitorTest.xml new file mode 100644 index 0000000..fdc3e62 --- /dev/null +++ b/.idea/runConfigurations/VisitorTest.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..1d93138 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=13.0.2-open diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..c077b4b --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,26 @@ +# Development + +Here we have a few hints on how to develop this module. + +## Environment + +### Java SDK + +To set up the java environment we recommend using [SDKMAN](https://sdkman.io/). Once sdkman is installed simply run `sdk env` in the project +root to set the JDK. + +### Building + +To simplify the building the maven build is split into a few profiles to reduce the build time. There are a few commands in +the [`Makefile`](./Makefile) that can help with the builds. + +## CF Debugging + +To debug in CF I found the easiest way is to use the docker. We can do this with this command: + +```bash +docker run --name cf_2018 --rm -p 8500:8500 -e acceptEULA=YES -e password:admin -e JAVA_OPTS="-Ddeep.service.url=172.17.0.1:43315 -Ddeep.logging.level=FINE -Ddeep.service.secure=false -Ddeep.transform.path=/opt/dispath" -v ${PWD}/dispath:/opt/dispath -v ${PWD}/agent/target/agent-1.0-SNAPSHOT.jar:/opt/deep/deep.jar ghcr.io/intergral/deep:coldfusion +``` + +Changing the -v paths to point to the locations on your machine. You also need to use the debug listen config ("Listen for Docker debug +connections") that should be available with this project in idea. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..dd104e2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ \ No newline at end of file diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..57164d4 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,22 @@ +# Licensing + +License names used in this document are as per [SPDX License List](https://spdx.org/licenses/). + +The default license for this project is [AGPL-3.0-only](LICENSE). + +## Apache-2.0 + +The following folders and their subfolders are licensed under Apache-2.0: + +``` + +``` + +The following file or directories and their subdirectories are licensed under their original upstream licenses: + +``` +agent/src/main/java/com/intergral/deep/agent/resource +agent/src/main/java/com/intergral/deep/agent/IDUtils.java +agent-api/src/main/java/com/intergral/deep/agent/api/resource +agent-api/src/main/java/com/intergral/deep/agent/api/spi +``` \ No newline at end of file diff --git a/Makefile b/Makefile index 30bc37b..92b593b 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,28 @@ + + +.PHONY: pmd +pmd: + mvn -U -B verify -Ppmd -DskipTests $(MVN_ARGS) + +,PHONY: lint +lint: + mvn -U -B validate -Plint,examples,cf-it-tests $(MVN_ARGS) + +.PHONY: test +test: + mvn -U -B clean verify $(MVN_ARGS) + +.PHONY: docs +docs: + mvn -s .ci-settings.xml clean package javadoc:jar -DskipTests -P release-ossrh -B -U -pl agent,deep --also-make $(MVN_ARGS) + +.PHONY: package-agent +package: + mvn package -U -B -pl agent --also-make -DskipTests $(MVN_ARGS) + +.PHONY: cf-tests +cf-tests: + mvn verify -U -B -P cf-it-tests -pl it-tests/cf-tests --also-make $(MVN_ARGS) # This file just contains shortcuts for dev, as there are a lot of options for different builds .PHONY: build @@ -8,3 +33,7 @@ build: .PHONY: install install: mvn clean install -U -B -pl agent,deep --also-make $(MVN_ARGS) + +.PHONY: coverage +coverage: + mvn clean verify -U -B -P coverage -pl '!it-tests/java-tests,!it-tests' diff --git a/README.md b/README.md index 8b8295e..d91cd5d 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,10 @@ Deep.start() There are a couple of examples [available here](./examples/README.md). +## Developing -## CF Debugging +For help developing this see [DEVELOPMENT.md](./DEVELOPMENT.md) -To debug in CF I found the easiest way is to use the docker. We can do this with this command: -```bash -docker run --name cf_2018 --rm -p 8500:8500 -p 8088:8088 -p 5005:5005 -e STRIP_STD=true -e FR_ENABLED=false -e NV_ENABLED=false -e JAVA_OPTS="-javaagent:/opt/deep/deep.jar -Ddeep.service.url=172.17.0.1:43315 -Ddeep.logging.level=FINE -Ddeep.service.secure=false -Ddeep.transform.path=/opt/dispath -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" -v ${PWD}/dispath:/opt/dispath -v ${PWD}/agent/target/agent-1.0-SNAPSHOT.jar:/opt/deep/deep.jar registry.gitlab.com/intergral/docker/servers/coldfusion:2018 -``` +## Licensing -Changing the -v paths to point to the locations on your machine. You also need to use the debug listen config ("Listen for Docker debug connections") that should be available with this project in idea. \ No newline at end of file +For licensing info please see [LICENSING.md](./LICENSING.md) diff --git a/agent-api/src/main/java-templates/com/intergral/deep/agent/api/DeepVersion.java b/agent-api/src/main/java-templates/com/intergral/deep/agent/api/DeepVersion.java index f2ab9eb..85864a7 100644 --- a/agent-api/src/main/java-templates/com/intergral/deep/agent/api/DeepVersion.java +++ b/agent-api/src/main/java-templates/com/intergral/deep/agent/api/DeepVersion.java @@ -17,7 +17,7 @@ package com.intergral.deep.agent.api; -public class DeepVersion { +public interface DeepVersion { - public static final String VERSION = "${project.version}"; + String VERSION = "${project.version}"; } \ No newline at end of file diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/DeepRuntimeException.java b/agent-api/src/main/java/com/intergral/deep/agent/api/DeepRuntimeException.java index d8b7deb..791d397 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/DeepRuntimeException.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/DeepRuntimeException.java @@ -17,12 +17,26 @@ package com.intergral.deep.agent.api; +/** + * A general exception used by deep to throw {@link RuntimeException}. + */ public class DeepRuntimeException extends RuntimeException { + /** + * Create a new exception with a message. + * + * @param message the error message + */ public DeepRuntimeException(final String message) { super(message); } + /** + * Create a new exception with a message and cause. + * + * @param message the error message + * @param cause the cause of the error + */ public DeepRuntimeException(final String message, final Throwable cause) { super(message, cause); } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/IRegistration.java b/agent-api/src/main/java/com/intergral/deep/agent/api/IRegistration.java index df32f9c..c0b8305 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/IRegistration.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/IRegistration.java @@ -28,7 +28,7 @@ public interface IRegistration { void unregister(); /** - * Get the registered item + * Get the registered item. * * @return the item that this registration is for. */ diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/AuthProvider.java b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/AuthProvider.java index 4a4227d..720f1c3 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/AuthProvider.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/AuthProvider.java @@ -19,17 +19,33 @@ import com.intergral.deep.agent.api.DeepRuntimeException; import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.reflection.ReflectionUtils; import com.intergral.deep.agent.api.settings.ISettings; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.Map; -public class AuthProvider { +/** + * A utility class to load the configured {@link IAuthProvider}. + */ +public final class AuthProvider { + + private AuthProvider() { + } private static final NoopProvider NOOP_PROVIDER = new NoopProvider(); - public static IAuthProvider provider(final ISettings settings) { + /** + * Load the configured {@link IAuthProvider}. + *

+ * This will always return a {@link IAuthProvider}. + * + * @param settings the current settings + * @param reflection the reflection service that is available + * @return the loaded {@link IAuthProvider} + */ + public static IAuthProvider provider(final ISettings settings, final IReflection reflection) { final String serviceAuthProvider = settings.getSettingAs("service.auth.provider", String.class); if (serviceAuthProvider == null || serviceAuthProvider.trim().isEmpty()) { return NOOP_PROVIDER; @@ -48,19 +64,14 @@ public static IAuthProvider provider(final ISettings settings) { try { final Class aClass = Class.forName(serviceAuthProvider); - final Constructor constructor = aClass.getConstructor(ISettings.class); - final Object newInstance = constructor.newInstance(settings); - return (IAuthProvider) newInstance; - } catch (ClassNotFoundException - | NoSuchMethodException - | InvocationTargetException - | InstantiationException - | IllegalAccessException e) { + final Constructor constructor = ReflectionUtils.findConstructor(aClass, reflection); + return ReflectionUtils.callConstructor(constructor, settings, reflection); + } catch (ClassNotFoundException e) { throw new RuntimeException(String.format("Cannot load auth provider %s", serviceAuthProvider), e); } } - private static class NoopProvider implements IAuthProvider { + static class NoopProvider implements IAuthProvider { @Override public Map provide() { diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/BasicAuthProvider.java b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/BasicAuthProvider.java index 6a537c4..819a141 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/BasicAuthProvider.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/BasicAuthProvider.java @@ -22,6 +22,18 @@ import java.util.Collections; import java.util.Map; +/** + * This is an {@link IAuthProvider} that will attach basic authorization to the outbound requests. + *

+ * This provider can be set using {@code service.auth.provider=com.intergral.deep.agent.api.auth.BasicAuthProvider}. The username and + * password that is configured onto requests can be set with the setting: + *

    + *
  • {@code service.username=yourusername}
  • + *
  • {@code service.password=yourpassword}
  • + *
+ *

+ * These values are then base64 encoded and attached to the outbound requests as the {@code authorization} header. + */ public class BasicAuthProvider implements IAuthProvider { private final ISettings settings; diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/IAuthProvider.java b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/IAuthProvider.java index dda8c64..dbbfcd9 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/auth/IAuthProvider.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/auth/IAuthProvider.java @@ -19,10 +19,14 @@ import java.util.Map; +/** + * Allows for custom auth providers to be configured. These can be provided as an instantiatable class using the class name via the setting + * {@code service.auth.provider}. Alternatively a plugin can be configured as an auth provider. + */ public interface IAuthProvider { /** - * Provide the headers that should be attached to the GRPC calls + * Provide the headers that should be attached to the GRPC calls. * * @return a Map of the header values */ diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/hook/IDeepHook.java b/agent-api/src/main/java/com/intergral/deep/agent/api/hook/IDeepHook.java index 291f146..72d7044 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/hook/IDeepHook.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/hook/IDeepHook.java @@ -20,9 +20,22 @@ import com.intergral.deep.agent.api.IDeep; import com.intergral.deep.agent.api.reflection.IReflection; +/** + * This type is used to pass an object from the agent to the API. It should not be directly used by clients. + */ public interface IDeepHook { + /** + * Get the deep service from the agent. + * + * @return the deep agent. + */ IDeep deepService(); + /** + * Get the configured reflection api. + * + * @return the reflection api. + */ IReflection reflectionService(); } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/AbstractEvaluator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/AbstractEvaluator.java index f5b5030..80bc961 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/AbstractEvaluator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/AbstractEvaluator.java @@ -34,6 +34,22 @@ public boolean evaluate(final String expression, final Map value } + /** + * Given an input convert to a boolean expression. + *

+ * As Java doesn't have inherit truthiness such as JavaScript. We want to simulate it here, we will convert the following to inputs, all + * other inputs are {@code true}. + *

    + *
  • null - All null values are {@code false}.
  • + *
  • boolean = All booleans are returned as they are.
  • + *
  • 0 = All numbers are {@code true}, except 0 which is {@code false}
  • + *
  • "true" = A string of the value {@code "true"} (ignoring case) is {@code true}, all other strings are {@code false}.
  • + *
+ * + * @param obj the value to convert + * @return the value as a boolean + * @see Boolean#parseBoolean(String) + */ public static boolean objectToBoolean(final Object obj) { if (obj == null) { return false; diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/EvaluationException.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/EvaluationException.java index ccb22b1..7058b90 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/EvaluationException.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/EvaluationException.java @@ -28,7 +28,7 @@ public class EvaluationException extends Exception { private final String expression; /** - * Create a new exception + * Create a new exception. * * @param expression the expression that failed * @param cause the failure @@ -39,7 +39,7 @@ public EvaluationException(final String expression, final Throwable cause) { } /** - * Get the expression + * Get the expression. * * @return {@link #expression} */ diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IEvaluator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IEvaluator.java index 919a1c0..a74ffec 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IEvaluator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IEvaluator.java @@ -19,10 +19,30 @@ import java.util.Map; +/** + * This defines an evaluator, and evaluator is used to evaluate expression at runtime. ie allows a watch or condition to execute within the + * scope of the tracepoint. + */ public interface IEvaluator { + /** + * Evaluate an expression as a boolean response. + * + * @param expression the expression to evaluate + * @param values the variables that the expression can evaluate against + * @return {@code true} if the expression evaluates to truthy value. + * @see AbstractEvaluator#objectToBoolean(Object) + */ boolean evaluate(final String expression, final Map values); + /** + * Evaluate an expression to the value. + * + * @param expression the expression to evaluate + * @param values the variables that the expression can evaluate against + * @return the result of the expression + * @throws Throwable if the expression fails + */ Object evaluateExpression(final String expression, final Map values) throws Throwable; } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IPlugin.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IPlugin.java index 1a05365..4134e11 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IPlugin.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/IPlugin.java @@ -52,7 +52,8 @@ default String name() { * Is this plugin active. *

* By default, this will check the Deep settings for the plugin name. e.g. the setting com.intergral.deep.plugin.JavaPlugin.active=false - * will disable the JavaPlugin. The normal settings rules apply, e.g. deep. or DEEP_ as a prefix when using system properties or environment variables. + * will disable the JavaPlugin. The normal settings rules apply, e.g. deep. or DEEP_ as a prefix when using system properties + * or environment variables. * * @param settings the current deep settings. * @return {@code false} if setting is 'false', otherwise {@code true} @@ -73,10 +74,10 @@ default boolean isActive(final ISettings settings) { interface IPluginRegistration extends IRegistration { /** - * Indicates if this plugin is currently set to be the auth provider + * Indicates if this plugin is currently set to be the auth provider. * - * @return {@code true} if the registered plugin is an {@link com.intergral.deep.agent.api.auth.IAuthProvider} and deep is configured to - * use this provider, else {@code false} + * @return {@code true} if the registered plugin is an {@link com.intergral.deep.agent.api.auth.IAuthProvider} and deep is configured + * to use this provider, else {@code false} */ boolean isAuthProvider(); } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ISnapshotContext.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ISnapshotContext.java index ba1f694..2ec8354 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ISnapshotContext.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ISnapshotContext.java @@ -23,7 +23,7 @@ public interface ISnapshotContext { /** - * Evaluate an expression in the frame of the tracepoint that triggered this snapshot + * Evaluate an expression in the frame of the tracepoint that triggered this snapshot. * * @param expression the express to evaluate * @return the result of the expression as a string diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/LazyEvaluator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/LazyEvaluator.java index c40c40d..dd9821b 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/LazyEvaluator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/LazyEvaluator.java @@ -18,6 +18,8 @@ package com.intergral.deep.agent.api.plugin; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This type allows the evaluator to be loaded only if it is needed. ie. if we have no watches or conditions there is no need for an @@ -25,6 +27,7 @@ */ public class LazyEvaluator extends AbstractEvaluator { + private static final Logger LOGGER = LoggerFactory.getLogger(LazyEvaluator.class); private static final Exception NO_EVALUATOR_EXCEPTION = new RuntimeException( "No evaluator available."); private final IEvaluatorLoader loader; @@ -39,6 +42,9 @@ private IEvaluator load() { try { this.evaluator = this.loader.load(); } catch (Exception e) { + LOGGER.error("Could not load evaluator", e); + } + if (this.evaluator == null) { this.evaluator = new AbstractEvaluator() { @Override public Object evaluateExpression(final String expression, final Map values) throws Throwable { @@ -55,8 +61,17 @@ public Object evaluateExpression(final String expression, final Map clazz, final Field field); - + /** + * Set the method as accessible. + * + * @param clazz the clazz the field is on + * @param method the method to access + * @return could we complete the operation + * @see Method#setAccessible(boolean) + */ boolean setAccessible(final Class clazz, final Method method); + /** + * Set the constructor as accessible. + * + * @param clazz the clazz the field is on + * @param constructor the constructor to set + * @return could we complete the operation + * @see Constructor#setAccessible(boolean) + */ + boolean setAccessible(final Class clazz, final Constructor constructor); + /** + * Call a method on the target object. + *

+ * This will look for the method using {@link #findMethod(Class, String, Class[])} using the input arguments as the argument types of the + * method. The method will then be invoked on the target argument. + * + * @param target the target instance to call the method on + * @param methodName the name of the method to call + * @param args the arguments to the method + * @param the type of the response + * @return the response of the method + */ T callMethod(Object target, String methodName, Object... args); - + /** + * Scan the hierarchy of the input class for a method with the given name. This will scan declared methods and methods, as well as + * stepping up the super class. + * + * @param clazz the class to start the scan on + * @param methodName the name of the method to look for + * @param argTypes the argument types in the method signature + * @return the discovered method or {@code null} + */ Method findMethod(Class clazz, String methodName, Class... argTypes); - + /** + * Get a filed from the target. + * + * @param target the target instance to look on. + * @param fieldName the name of the field to get + * @return the field, or {@code null} + */ Field getField(Object target, String fieldName); /** - * Get a field from an object + * Get a field from an object. * * @param target the object to look at * @param fieldName the field name to look for @@ -52,9 +104,49 @@ public interface IReflection { */ T getFieldValue(Object target, String fieldName); + /** + * Get an iterator that will iterator over all the available fields on the given class. + * + * @param clazz the class to scan + * @return the iterator for the fields + */ Iterator getFieldIterator(Class clazz); + /** + * Call a field on a target. + * + * @param target the object to get the field from + * @param field the field to call + * @param the type of the return + * @return the value of the field + */ T callField(Object target, Field field); + /** + * Get the modifier names of a field. + * + * @param field the field to look at + * @return a set of strings that represent the modifiers. + */ Set getModifiers(Field field); + + /** + * Find a constructor on the given class. + * + * @param clazz the class to look on + * @param args the arguments for the constructor + * @return the constructor or {@code null} + */ + Constructor findConstructor(final Class clazz, final Class... args); + + /** + * Call a constructor with the arguments. + * + * @param constructor the constructor to call + * @param args the arguments for the constructor + * @param the type of the return + * @return the new object, or {@code null} + * @throws DeepRuntimeException if we could not create a new object + */ + T callConstructor(final Constructor constructor, final Object... args) throws DeepRuntimeException; } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/reflection/ReflectionUtils.java b/agent-api/src/main/java/com/intergral/deep/agent/api/reflection/ReflectionUtils.java new file mode 100644 index 0000000..0353c92 --- /dev/null +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/reflection/ReflectionUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.reflection; + +import com.intergral.deep.agent.api.DeepRuntimeException; +import com.intergral.deep.agent.api.settings.ISettings; +import java.lang.reflect.Constructor; + +/** + * A simple util to get create plugins. A plugin can have a default constructor or one that accepts the {@link ISettings} class. So this + * type helps us find and call the appropriate constructor. + */ +public final class ReflectionUtils { + + private ReflectionUtils() { + } + + /** + * Call the constructor. + * + * @param constructor the constructor + * @param settings the settings object + * @param reflection the reflection service to use + * @param the type of the new object + * @return the new object + */ + public static T callConstructor(final Constructor constructor, final ISettings settings, final IReflection reflection) { + if (constructor.getParameterTypes().length == 0) { + return reflection.callConstructor(constructor); + } + return reflection.callConstructor(constructor, settings); + } + + /** + * Find the constructor to use. + *

+ * Will look for the constructor that we can use in the order: + *

    + *
  • constructor(ISettings.class)
  • + *
  • constructor()
  • + *
+ * + * @param clazz the class to look on + * @param reflection the reflection service to use + * @return the constructor that was found + * @throws DeepRuntimeException if we could not find a constructor + */ + public static Constructor findConstructor(final Class clazz, final IReflection reflection) { + + final Constructor constructor = reflection.findConstructor(clazz, ISettings.class); + if (constructor != null) { + return constructor; + } + + final Constructor defaultConstructor = reflection.findConstructor(clazz); + if (defaultConstructor != null) { + return defaultConstructor; + } + final String simpleName = clazz.getSimpleName(); + throw new DeepRuntimeException( + String.format("Cannot create auth provider of type: %s. Class is missing constructor %s(%s) or %s().", clazz.getName(), + simpleName, ISettings.class.getName(), simpleName)); + + } + +} diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ConfigurationException.java b/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ConfigurationException.java deleted file mode 100644 index c560a81..0000000 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ConfigurationException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.intergral.deep.agent.api.resource; - -/** - * An exception that is thrown if the user-provided configuration is invalid. - */ -public final class ConfigurationException extends RuntimeException { - - /** - * Create a new configuration exception with specified {@code message} and without a cause. - * - * @param message The exception message - */ - public ConfigurationException(String message) { - super(message); - } - - /** - * Create a new configuration exception with specified {@code message} and {@code cause}. - * - * @param message The exception message - * @param cause The root cause of this exception - */ - public ConfigurationException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ResourceAttributes.java b/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ResourceAttributes.java index 98872d9..bdcbe5c 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ResourceAttributes.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/resource/ResourceAttributes.java @@ -1,22 +1,14 @@ /* - * Copyright 2023 Intergral GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 */ package com.intergral.deep.agent.api.resource; -public class ResourceAttributes { +/** + * A collection of known keys that are used in the attributes. + */ +public interface ResourceAttributes { /** * Logical name of the service. @@ -31,20 +23,20 @@ public class ResourceAttributes { * MUST be set to {@code unknown_service}. * */ - public static final String SERVICE_NAME = "service.name"; + String SERVICE_NAME = "service.name"; /** * The name of the telemetry SDK as defined above. */ - public static final String TELEMETRY_SDK_NAME = "telemetry.sdk.name"; + String TELEMETRY_SDK_NAME = "telemetry.sdk.name"; /** * The language of the telemetry SDK. */ - public static final String TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language"; + String TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language"; /** * The version string of the telemetry SDK. */ - public static final String TELEMETRY_SDK_VERSION = "telemetry.sdk.version"; + String TELEMETRY_SDK_VERSION = "telemetry.sdk.version"; } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/settings/ISettings.java b/agent-api/src/main/java/com/intergral/deep/agent/api/settings/ISettings.java index 8fa00c0..75e2e4f 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/settings/ISettings.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/settings/ISettings.java @@ -21,10 +21,13 @@ import com.intergral.deep.agent.api.resource.Resource; import java.util.Map; +/** + * The exposed API for the Deep settings. + */ public interface ISettings { /** - * This is the settings key for the configured auth provider + * This is the settings key for the configured auth provider. */ String KEY_AUTH_PROVIDER = "service.auth.provider"; @@ -34,16 +37,62 @@ public interface ISettings { String KEY_ENABLED = "enabled"; /** - * This is the setting key for the service url + * This is the setting key for the service url. */ String KEY_SERVICE_URL = "service.url"; + /** + * This is the setting key for the service secure setting. + */ + String KEY_SERVICE_SECURE = "service.secure"; + + /** + * This is the setting key for the plugin list. + */ + String PLUGINS = "plugins"; + + /** + * To let us calculate the class and file names for JSP classes we need to know the JSP suffix that is being used by monitored service. + *

+ * By default, tomcat take index.jsp and make it into index_jsp.class, but this suffix can be configured. + */ + String JSP_SUFFIX = "jsp.suffix"; + /** + * It is possible to put compiled JSP classes into specified packages, some versions put this in a {@code jsp} package, some use + * {@code org.apache.jsp} (newer). + */ + String JSP_PACKAGES = "jsp.packages"; + + /** + * Define which packages we should include as being part of your app. + */ + String APP_FRAMES_INCLUDES = "in.app.include"; + + /** + * Define which packages we should exclude as being part of your app. + */ + String APP_FRAMES_EXCLUDES = "in.app.include"; + + /** + * Get a setting from the config as a given type. + * + * @param key the key for the setting + * @param clazz the type to return as + * @param the type to return as + * @return the value as the given type + */ T getSettingAs(String key, Class clazz); - Map getMap(String attributeProperty); + /** + * Get the property as a map. + * + * @param key the for the setting + * @return the value as a map + */ + Map getMap(String key); /** - * Returns the resource that describes this client + * Returns the resource that describes this client. * * @return the {@link Resource} */ diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/tracepoint/ITracepoint.java b/agent-api/src/main/java/com/intergral/deep/agent/api/tracepoint/ITracepoint.java index 4216275..8018d65 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/tracepoint/ITracepoint.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/tracepoint/ITracepoint.java @@ -19,28 +19,52 @@ import com.intergral.deep.agent.api.IRegistration; import java.util.Collection; -import java.util.Collections; import java.util.Map; -import java.util.UUID; +/** + * This type describes a tracepoint that has been attached via code, using + * {@link com.intergral.deep.agent.api.IDeep#registerTracepoint(String, int)}. + */ public interface ITracepoint { + /** + * Get the tracepoint path. + * + * @return the tracepoint path + */ String path(); + /** + * Get the tracepoint line number. + * + * @return the line number + */ int line(); - default Map args() { - return Collections.emptyMap(); - } + /** + * Get the args for the tracepoint. + * + * @return the args on the tracepoint + */ + Map args(); - default Collection watches() { - return Collections.emptyList(); - } + /** + * Get the tracepoint watches. + * + * @return the configured watches + */ + Collection watches(); - default String id() { - return UUID.randomUUID().toString(); - } + /** + * The generated ID for the tracepoint. + * + * @return the tracepoint id + */ + String id(); + /** + * Defines the tracepoint registration. + */ interface ITracepointRegistration extends IRegistration { } diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayIterator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayIterator.java index 94d7a09..833b210 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayIterator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayIterator.java @@ -19,6 +19,11 @@ import java.util.Iterator; +/** + * An iterator that will iterate an array. + * + * @param the type of the objects in the array + */ public class ArrayIterator implements Iterator { private final T[] value; diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayObjectIterator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayObjectIterator.java index f39cbe4..ca56559 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayObjectIterator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/ArrayObjectIterator.java @@ -20,6 +20,9 @@ import java.lang.reflect.Array; import java.util.Iterator; +/** + * An iterator that will iterate an array, but does so using the {@link Array} class. + */ public class ArrayObjectIterator implements Iterator { private final int length; diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/CompoundIterator.java b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/CompoundIterator.java index d74354f..80a7901 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/utils/CompoundIterator.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/utils/CompoundIterator.java @@ -18,7 +18,13 @@ package com.intergral.deep.agent.api.utils; import java.util.Iterator; +import java.util.NoSuchElementException; +/** + * An iterator that can iterate across multiple iterators. + * + * @param the type of the object in the iterators + */ public class CompoundIterator implements Iterator { private final Iterator[] iterators; @@ -47,7 +53,7 @@ public boolean hasNext() { @Override public T next() { - return null; + throw new NoSuchElementException("end of compound iterator"); } }; } diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/DeepRuntimeExceptionTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/DeepRuntimeExceptionTest.java new file mode 100644 index 0000000..a56c216 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/DeepRuntimeExceptionTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class DeepRuntimeExceptionTest { + + @Test + void runtimeException() { + assertEquals("Some message", new DeepRuntimeException("Some message").getMessage()); + assertEquals("Some message", new DeepRuntimeException("Some message", new RuntimeException()).getMessage()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/auth/AuthProviderTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/auth/AuthProviderTest.java new file mode 100644 index 0000000..8658a53 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/auth/AuthProviderTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.agent.api.auth.AuthProvider.NoopProvider; +import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class AuthProviderTest { + + @Test + void canProvide() { + final ISettings settings = Mockito.mock(ISettings.class); + final IReflection reflection = Mockito.mock(IReflection.class); + final IAuthProvider provider = AuthProvider.provider(settings, reflection); + + assertEquals(provider.getClass(), NoopProvider.class); + } + + @Test + void canLoadProviderByName() { + final ISettings settings = Mockito.mock(ISettings.class); + Mockito.doReturn(MockAuthProviderPlugin.class.getName()).when(settings).getSettingAs("service.auth.provider", String.class); + final IReflection reflection = Mockito.mock(IReflection.class); + Mockito.when(reflection.findConstructor(Mockito.any(), Mockito.any())) + .thenAnswer(invocationOnMock -> MockAuthProviderPlugin.class.getConstructor()); + Mockito.when(reflection.callConstructor(Mockito.any())).thenReturn(new MockAuthProviderPlugin()); + final IAuthProvider provider = AuthProvider.provider(settings, reflection); + + assertEquals(MockAuthProviderPlugin.class, provider.getClass()); + } + + @Test + void willUseNoopProvider() { + final ISettings settings = Mockito.mock(ISettings.class); + final IReflection reflection = Mockito.mock(IReflection.class); + final IAuthProvider provider = AuthProvider.provider(settings, reflection); + assertEquals(NoopProvider.class, provider.getClass()); + assertEquals(Collections.emptyMap(), provider.provide()); + } + + public static class MockAuthProviderPlugin implements IPlugin, IAuthProvider { + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + return Resource.create(Collections.singletonMap("test", "provider")); + } + + @Override + public Map provide() { + return Collections.singletonMap("test", "provider"); + } + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/auth/BasicAuthProviderTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/auth/BasicAuthProviderTest.java new file mode 100644 index 0000000..5acedc1 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/auth/BasicAuthProviderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.agent.api.settings.ISettings; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class BasicAuthProviderTest { + + @Test + void canProvide() { + final ISettings settings = Mockito.mock(ISettings.class); + final BasicAuthProvider basicAuthProvider = new BasicAuthProvider(settings); + + assertEquals(0, basicAuthProvider.provide().size()); + } + + @Test + void canProvide_settings() { + final ISettings settings = Mockito.mock(ISettings.class); + Mockito.doReturn("username").doReturn("password").when(settings).getSettingAs(Mockito.anyString(), Mockito.any()); + final BasicAuthProvider basicAuthProvider = new BasicAuthProvider(settings); + + final Map provide = basicAuthProvider.provide(); + assertEquals(1, provide.size()); + assertEquals("basic%20dXNlcm5hbWU6cGFzc3dvcmQ=", provide.get("authorization")); + } + + @Test + void canProvide_missingUsername() { + final ISettings settings = Mockito.mock(ISettings.class); + Mockito.doReturn(null).doReturn("password").when(settings).getSettingAs(Mockito.anyString(), Mockito.any()); + final BasicAuthProvider basicAuthProvider = new BasicAuthProvider(settings); + + final Map provide = basicAuthProvider.provide(); + assertEquals(0, provide.size()); + } + + @Test + void canProvide_missingPassword() { + final ISettings settings = Mockito.mock(ISettings.class); + Mockito.doReturn("username").doReturn(null).when(settings).getSettingAs(Mockito.anyString(), Mockito.any()); + final BasicAuthProvider basicAuthProvider = new BasicAuthProvider(settings); + + final Map provide = basicAuthProvider.provide(); + assertEquals(0, provide.size()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/AbstractEvaluatorTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/AbstractEvaluatorTest.java new file mode 100644 index 0000000..09693c6 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/AbstractEvaluatorTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.plugin; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AbstractEvaluatorTest { + + + private AbstractEvaluator abstractEvaluator; + + @BeforeEach + void setUp() { + abstractEvaluator = new AbstractEvaluator() { + @Override + public Object evaluateExpression(final String expression, final Map values) { + return null; + } + }; + } + + @Test + void evaluate() throws Throwable { + assertNull(abstractEvaluator.evaluateExpression("anything", new HashMap<>())); + assertFalse(abstractEvaluator.evaluate("anything", new HashMap<>())); + } + + @Test + void objectToBoolean() { + assertFalse(LazyEvaluator.objectToBoolean(null)); + assertFalse(LazyEvaluator.objectToBoolean(0)); + assertFalse(LazyEvaluator.objectToBoolean(false)); + assertFalse(LazyEvaluator.objectToBoolean("false")); + + assertTrue(LazyEvaluator.objectToBoolean(1)); + assertTrue(LazyEvaluator.objectToBoolean(true)); + assertTrue(LazyEvaluator.objectToBoolean("true")); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/EvaluationExceptionTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/EvaluationExceptionTest.java new file mode 100644 index 0000000..644119d --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/EvaluationExceptionTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.plugin; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class EvaluationExceptionTest { + + @Test + void getExpression() { + assertEquals("some expression", new EvaluationException("some expression", new RuntimeException()).getExpression()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/IPluginTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/IPluginTest.java new file mode 100644 index 0000000..06de505 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/IPluginTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.plugin; + +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 com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class IPluginTest { + + @Test + void name() { + assertEquals("com.intergral.deep.agent.api.plugin.IPluginTest$MockPlugin", new MockPlugin().name()); + } + + @Test + void isActive() { + final ISettings settings = Mockito.mock(ISettings.class); + Mockito.when(settings.getSettingAs("com.intergral.deep.agent.api.plugin.IPluginTest$MockPlugin.active", Boolean.class)).thenReturn(null) + .thenReturn(true).thenReturn(false); + assertTrue(new MockPlugin().isActive(settings)); + assertTrue(new MockPlugin().isActive(settings)); + assertFalse(new MockPlugin().isActive(settings)); + } + + static class MockPlugin implements IPlugin { + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + return null; + } + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/LazyEvaluatorTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/LazyEvaluatorTest.java new file mode 100644 index 0000000..d46e611 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/plugin/LazyEvaluatorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.plugin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.intergral.deep.agent.api.plugin.LazyEvaluator.IEvaluatorLoader; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class LazyEvaluatorTest { + + private IEvaluatorLoader loader; + private LazyEvaluator lazyEvaluator; + + @BeforeEach + void setUp() { + loader = Mockito.mock(IEvaluatorLoader.class); + Mockito.when(loader.load()).thenReturn(new AbstractEvaluator() { + @Override + public Object evaluateExpression(final String expression, final Map values) { + return null; + } + }); + lazyEvaluator = new LazyEvaluator(loader); + } + + @Test + void evaluateExpression() throws Throwable { + assertNull(lazyEvaluator.evaluateExpression("anything", new HashMap<>())); + assertFalse(lazyEvaluator.evaluate("anything", new HashMap<>())); + + Mockito.verify(loader, Mockito.times(1)).load(); + } + + @Test + void canHandleFailure() { + Mockito.when(loader.load()).thenReturn(null); + final RuntimeException something = assertThrows(RuntimeException.class, + () -> lazyEvaluator.evaluateExpression("something", new HashMap<>())); + assertEquals("No evaluator available.", something.getMessage()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/resource/ResourceTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/resource/ResourceTest.java new file mode 100644 index 0000000..746d8b2 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/resource/ResourceTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.resource; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +class ResourceTest { + + @Test + void canCreateDefault() { + assertNotNull(Resource.DEFAULT); + assertNotNull(Resource.DEFAULT.getAttributes().get(ResourceAttributes.TELEMETRY_SDK_NAME)); + assertNotNull(Resource.DEFAULT.getAttributes().get(ResourceAttributes.TELEMETRY_SDK_LANGUAGE)); + assertNotNull(Resource.DEFAULT.getAttributes().get(ResourceAttributes.TELEMETRY_SDK_VERSION)); + assertNull(Resource.DEFAULT.getSchemaUrl()); + } + + @Test + void canMerge() { + assertNotNull(Resource.DEFAULT.merge(null)); + assertNotNull(Resource.DEFAULT.merge(Resource.create(new HashMap<>()))); + + final HashMap attributes = new HashMap<>(); + attributes.put("merged", "thing"); + attributes.put(ResourceAttributes.TELEMETRY_SDK_VERSION, "test_override"); + final Resource merged = Resource.DEFAULT.merge(Resource.create(attributes)); + assertEquals("thing", merged.getAttributes().get("merged")); + assertEquals("test_override", merged.getAttributes().get(ResourceAttributes.TELEMETRY_SDK_VERSION)); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/spi/OrderedTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/spi/OrderedTest.java new file mode 100644 index 0000000..d3ec096 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/spi/OrderedTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.spi; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class OrderedTest { + + @Test + void order() { + assertEquals(0, new Ordered() { + }.order()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayIteratorTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayIteratorTest.java new file mode 100644 index 0000000..6046624 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayIteratorTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ArrayIteratorTest { + + @Test + void next() { + final ArrayIterator iterator = new ArrayIterator<>(new String[]{"one", "two", "three"}); + + assertTrue(iterator.hasNext()); + assertEquals("one", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("two", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("three", iterator.next()); + assertFalse(iterator.hasNext()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayObjectIteratorTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayObjectIteratorTest.java new file mode 100644 index 0000000..29bb34c --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/ArrayObjectIteratorTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ArrayObjectIteratorTest { + + @Test + void next() { + final ArrayObjectIterator iterator = new ArrayObjectIterator(new String[]{"one", "two", "three"}); + + assertTrue(iterator.hasNext()); + assertEquals("one", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("two", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("three", iterator.next()); + assertFalse(iterator.hasNext()); + } +} \ No newline at end of file diff --git a/agent-api/src/test/java/com/intergral/deep/agent/api/utils/CompoundIteratorTest.java b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/CompoundIteratorTest.java new file mode 100644 index 0000000..ff4e7a9 --- /dev/null +++ b/agent-api/src/test/java/com/intergral/deep/agent/api/utils/CompoundIteratorTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; +import org.junit.jupiter.api.Test; + +class CompoundIteratorTest { + + @Test + void next() { + final CompoundIterator iterator = new CompoundIterator<>(new ArrayIterator<>(new String[]{"one", "two", "three"}), + new ArrayIterator<>(new String[]{"one_1", "two_2", "three_3"})); + + assertTrue(iterator.hasNext()); + assertEquals("one", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("two", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("three", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("one_1", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("two_2", iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals("three_3", iterator.next()); + assertFalse(iterator.hasNext()); + final NoSuchElementException noSuchElementException = assertThrows(NoSuchElementException.class, iterator::next); + assertEquals("end of compound iterator", noSuchElementException.getMessage()); + } +} \ No newline at end of file diff --git a/agent/pom.xml b/agent/pom.xml index 9303b4a..3e935b6 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -150,6 +150,23 @@ asm-tree compile + + + + com.intergral.deep.tests + test-utils + 1.0-SNAPSHOT + test + + + + + + org.apache.tomcat + tomcat-jasper + 9.0.6 + test + @@ -463,17 +480,9 @@ false - - - - - - - - - deep.logging.level - INFO + deep.callback.class + com.intergral.deep.agent.tracepoint.handler.Callback diff --git a/agent/src/main/java/com/intergral/deep/agent/Agent.java b/agent/src/main/java/com/intergral/deep/agent/Agent.java index 10230ab..bac01e1 100644 --- a/agent/src/main/java/com/intergral/deep/agent/Agent.java +++ b/agent/src/main/java/com/intergral/deep/agent/Agent.java @@ -27,10 +27,16 @@ import java.util.Map; import java.util.jar.JarFile; -public class Agent { +/** + * This is the main entry point for the Deep agent. + */ +public final class Agent { + + private Agent() { + } /** - * This is called when the agent is dynamically attached to the VM + * This is called when the agent is dynamically attached to the VM. * * @param arg the agent args * @param inst a system instrumentation @@ -43,7 +49,7 @@ public static void agentmain(final String arg, final Instrumentation inst) { /** - * This is called when the agent is attached from the CLI + * This is called when the agent is attached from the CLI. * * @param arg the agent args * @param inst a system instrumentation @@ -55,7 +61,7 @@ public static void premain(final String arg, final Instrumentation inst) { } /** - * A common start for NV + * A common start for NV. * * @param args the NV args * @param inst a system instrumentation @@ -81,12 +87,13 @@ private static void startNv(final Map args, final Instrumentatio System.err.println( "ERROR: Failed to start deep agent: " + t.getClass().getName() + " " + t.getMessage()); System.err.println("----------------------------------------------------------"); + //noinspection CallToPrintStackTrace t.printStackTrace(); System.err.println("----------------------------------------------------------"); } } - protected static Map parseArgs(final String args) { + static Map parseArgs(final String args) { if (args == null) { return new HashMap<>(); } @@ -107,7 +114,7 @@ protected static Map parseArgs(final String args) { return arguments; } - protected static List splitArgs(final String args) { + private static List splitArgs(final String args) { final List rtn = new ArrayList<>(); boolean escaped = false; diff --git a/agent/src/main/java/com/intergral/deep/agent/AgentImpl.java b/agent/src/main/java/com/intergral/deep/agent/AgentImpl.java index 91d6358..94c4bd2 100644 --- a/agent/src/main/java/com/intergral/deep/agent/AgentImpl.java +++ b/agent/src/main/java/com/intergral/deep/agent/AgentImpl.java @@ -27,11 +27,23 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; +/** + * This type is called from the {@link Agent} via reflection to load the agent after the jar we are in has been attached to the class path. + */ public class AgentImpl { + private AgentImpl() { + } + private static final CountDownLatch LATCH = new CountDownLatch(1); private static DeepAgent deepAgent; + /** + * Start the deep agent. + * + * @param inst the instrumentation object + * @param args the agent arguments + */ public static void startup(final Instrumentation inst, final Map args) { final Settings settings = Settings.build(args); Logger.configureLogging(settings); @@ -47,11 +59,25 @@ public static void startup(final Instrumentation inst, final Map LATCH.countDown(); } + /** + * await the load of the dep api. + * + * @return the loaded api object + * @throws InterruptedException if interupted + */ + // called via reflection public static Object awaitLoadAPI() throws InterruptedException { LATCH.await(); return loadDeepAPI(); } + /** + * Load the deep API to be used outside the agent. + *

+ * This method is defined as returning {@link Object} to not cause unwanted class loading of the type {@link IDeepHook}. + * + * @return a new instance of {@link IDeepHook} + */ public static Object loadDeepAPI() { if (deepAgent == null) { throw new IllegalStateException("Must start DEEP first"); diff --git a/agent/src/main/java/com/intergral/deep/agent/DeepAgent.java b/agent/src/main/java/com/intergral/deep/agent/DeepAgent.java index 48126b1..73a63cf 100644 --- a/agent/src/main/java/com/intergral/deep/agent/DeepAgent.java +++ b/agent/src/main/java/com/intergral/deep/agent/DeepAgent.java @@ -41,6 +41,9 @@ import java.util.List; import java.util.Map; +/** + * This is the agent that is provided via the API, and is what holds all deep together. + */ public class DeepAgent implements IDeep { private final Settings settings; @@ -49,6 +52,12 @@ public class DeepAgent implements IDeep { private final TracepointConfigService tracepointConfig; private final PushService pushService; + /** + * Create a new deep agent. + * + * @param settings the settings + * @param tracepointInstrumentationService the tracepoint instrumentation service + */ public DeepAgent(final Settings settings, TracepointInstrumentationService tracepointInstrumentationService) { this.settings = settings; @@ -60,10 +69,13 @@ public DeepAgent(final Settings settings, Callback.init(settings, tracepointConfig, pushService); } + /** + * Start deep. + */ public void start() { final Resource resource = ResourceDetector.configureResource(settings, DeepAgent.class.getClassLoader()); - final List iLoadedPlugins = PluginLoader.loadPlugins(settings); + final List iLoadedPlugins = PluginLoader.loadPlugins(settings, ReflectionUtils.getReflection()); this.settings.setPlugins(iLoadedPlugins); this.settings.setResource(Resource.DEFAULT.merge(resource)); this.grpcService.start(); @@ -75,6 +87,7 @@ public String getVersion() { return DeepVersion.VERSION; } + @Override public IPluginRegistration registerPlugin(final IPlugin plugin) { this.settings.addPlugin(plugin); final boolean isAuthProvider; @@ -151,7 +164,7 @@ public String id() { @Override public boolean isEnabled() { - return this.settings.getSettingAs(ISettings.KEY_ENABLED, Boolean.class); + return this.settings.isActive(); } @Override diff --git a/agent/src/main/java/com/intergral/deep/agent/IDUtils.java b/agent/src/main/java/com/intergral/deep/agent/IDUtils.java index 24a2f22..f06fc38 100644 --- a/agent/src/main/java/com/intergral/deep/agent/IDUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/IDUtils.java @@ -1,30 +1,29 @@ /* - * Copyright (C) 2023 Intergral GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 */ package com.intergral.deep.agent; import java.util.concurrent.ThreadLocalRandom; -public class IDUtils { +/** + * Utilities related to snapshot ids. + */ +public final class IDUtils { + + private IDUtils() { + } static final int BYTE_BASE16 = 2; private static final String ALPHABET = "0123456789abcdef"; private static final char[] ENCODING = buildEncodingArray(); + /** + * Create a new random id for a snapshot. + * + * @return the new id + */ public static String randomId() { final ThreadLocalRandom current = ThreadLocalRandom.current(); final long longId = current.nextLong(); @@ -49,7 +48,7 @@ private static char[] buildEncodingArray() { * @param dest the destination char array. * @param destOffset the starting offset in the destination char array. */ - public static void longToBase16String(long value, char[] dest, int destOffset) { + static void longToBase16String(long value, char[] dest, int destOffset) { byteToBase16((byte) (value >> 56 & 0xFFL), dest, destOffset); byteToBase16((byte) (value >> 48 & 0xFFL), dest, destOffset + BYTE_BASE16); byteToBase16((byte) (value >> 40 & 0xFFL), dest, destOffset + 2 * BYTE_BASE16); @@ -67,7 +66,7 @@ public static void longToBase16String(long value, char[] dest, int destOffset) { * @param dest the destination char array. * @param destOffset the starting offset in the destination char array. */ - public static void byteToBase16(byte value, char[] dest, int destOffset) { + static void byteToBase16(byte value, char[] dest, int destOffset) { int b = value & 0xFF; dest[destOffset] = ENCODING[b]; dest[destOffset + 1] = ENCODING[b | 0x100]; diff --git a/agent/src/main/java/com/intergral/deep/agent/ReflectionUtils.java b/agent/src/main/java/com/intergral/deep/agent/ReflectionUtils.java index 4233568..bce76ad 100644 --- a/agent/src/main/java/com/intergral/deep/agent/ReflectionUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/ReflectionUtils.java @@ -24,12 +24,18 @@ import java.util.Iterator; import java.util.Set; -public class ReflectionUtils { +/** + * A collection of utils that simplify the use of reflection. + */ +public final class ReflectionUtils { + + private ReflectionUtils() { + } private static final IReflection reflection; static { - if (Utils.getVersion() >= 9) { + if (Utils.getJavaVersion() >= 9) { reflection = new com.intergral.deep.reflect.Java9ReflectionImpl(); } else { reflection = new ReflectionImpl(); @@ -40,30 +46,14 @@ public static IReflection getReflection() { return reflection; } - public static boolean setAccessible(final Class clazz, final Field field) { - return getReflection().setAccessible(clazz, field); - } - - - public static boolean setAccessible(final Class clazz, final Method method) { - return getReflection().setAccessible(clazz, method); - } - - public static T callMethod(Object target, String methodName, Object... args) { return getReflection().callMethod(target, methodName, args); } - public static Method findMethod(Class clazz, String methodName, Class... argTypes) { return getReflection().findMethod(clazz, methodName, argTypes); } - - public static Field getField(Object target, String fieldName) { - return getReflection().getField(target, fieldName); - } - public static T getFieldValue(Object target, String fieldName) { return getReflection().getFieldValue(target, fieldName); } diff --git a/agent/src/main/java/com/intergral/deep/agent/Utils.java b/agent/src/main/java/com/intergral/deep/agent/Utils.java index 208076b..b368a46 100644 --- a/agent/src/main/java/com/intergral/deep/agent/Utils.java +++ b/agent/src/main/java/com/intergral/deep/agent/Utils.java @@ -18,13 +18,23 @@ package com.intergral.deep.agent; import java.time.Instant; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -public class Utils { +/** + * Collection of utilities for general java related tasks. + */ +public final class Utils { + + private Utils() { + } - public static int getVersion() { + /** + * Get the current version of Java running in this JVM. + * + * @return the java version number + */ + public static int getJavaVersion() { String version = System.getProperty("java.version"); return extractVersion(version); } @@ -55,38 +65,49 @@ public static long[] currentTimeNanos() { return new long[]{now.toEpochMilli(), Long.parseLong(format)}; } + /** + * Create a new map from the input. + * + * @param map the input map + * @param the key type + * @return a new map with the same values are the input, or a new empty map + */ public static Map newMap(final Map map) { if (map == null) { - return Collections.emptyMap(); + return new HashMap<>(); } return new HashMap<>(map); } /** - * FROM: https://stackoverflow.com/a/38947571 + * Check if a string ends with a value, ignoring the case. * * @param str the string to search * @param suffix the value to serch for * @return true if {@code str} ends with {@code suffix}, disregarding case sensitivity + * @see Source */ public static boolean endsWithIgnoreCase(String str, String suffix) { int suffixLength = suffix.length(); return str.regionMatches(true, str.length() - suffixLength, suffix, 0, suffixLength); } + /** + * This will create a string representation of the object passed in. + * + * @param obj the value to create a string from + * @return the string form of the object + */ public static String valueOf(final Object obj) { if (obj == null) { return "null"; } String hash; + // sometimes (on bad objects) .toString will fail. So we need to protect against that. try { - final String tmp = String.valueOf(obj); - // FR-5298 - Protected again NullPointerException when stepping in - //Stringbuilder. - tmp.length(); - return tmp; + return String.valueOf(obj); } catch (final Throwable e1) { // From Object.toString(); hash = String.valueOf(System.identityHashCode(obj)); @@ -95,15 +116,29 @@ public static String valueOf(final Object obj) { } - public static String trim(String str, final String trimStr) { - while (str.startsWith(trimStr)) { + /** + * Trim a string from another string. + * + * @param str the target string + * @param prefix the value to remove from the string + * @return the new string + */ + public static String trimPrefix(String str, final String prefix) { + while (str.startsWith(prefix)) { str = str.substring(1); } return str; } - public static ITrimResult trim(final String str, final int maxLength) { + /** + * Trim a string to a specified length. + * + * @param str the target string + * @param maxLength the max length to make the string + * @return a {@link ITrimResult}, so we can know if the string was trimmed + */ + public static ITrimResult truncate(final String str, final int maxLength) { if (str.length() > maxLength) { return new ITrimResult() { @Override @@ -134,12 +169,12 @@ public boolean truncated() { /** - * The result of a trim operation + * The result of a trim operation. */ public interface ITrimResult { /** - * The value to use, might be truncated + * The value to use, might be truncated. * * @return the value */ @@ -147,7 +182,7 @@ public interface ITrimResult { /** - * Has the value been truncated + * Has the value been truncated. * * @return {@code true} if the value was truncated */ diff --git a/agent/src/main/java/com/intergral/deep/agent/grpc/GrpcService.java b/agent/src/main/java/com/intergral/deep/agent/grpc/GrpcService.java index e9bb9ac..a83326b 100644 --- a/agent/src/main/java/com/intergral/deep/agent/grpc/GrpcService.java +++ b/agent/src/main/java/com/intergral/deep/agent/grpc/GrpcService.java @@ -17,8 +17,10 @@ package com.intergral.deep.agent.grpc; +import com.intergral.deep.agent.ReflectionUtils; import com.intergral.deep.agent.api.auth.AuthProvider; import com.intergral.deep.agent.api.auth.IAuthProvider; +import com.intergral.deep.agent.api.settings.ISettings; import com.intergral.deep.agent.settings.Settings; import com.intergral.deep.proto.poll.v1.PollConfigGrpc; import com.intergral.deep.proto.tracepoint.v1.SnapshotServiceGrpc; @@ -41,6 +43,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This service handles the grpc channel and attaching the metadata to the outbound services. + */ public class GrpcService { private static final Logger LOGGER = LoggerFactory.getLogger(GrpcService.class); @@ -52,6 +57,9 @@ public GrpcService(final Settings settings) { this.settings = settings; } + /** + * Start the grpc service and connect the channel. + */ public void start() { try { setupChannel(); @@ -94,7 +102,7 @@ private void setupChannel() { } // Select secure or not - if (this.settings.getSettingAs("service.secure", Boolean.class)) { + if (this.settings.getSettingAs(ISettings.KEY_SERVICE_SECURE, Boolean.class)) { ncBuilder.useTransportSecurity(); } else { ncBuilder.usePlaintext(); @@ -114,6 +122,11 @@ private ManagedChannel getChannel() { return channel; } + /** + * Get the grpc service for polling configs. + * + * @return the service to use + */ public PollConfigGrpc.PollConfigBlockingStub pollService() { final PollConfigGrpc.PollConfigBlockingStub blockingStub = PollConfigGrpc.newBlockingStub( getChannel()); @@ -124,6 +137,11 @@ public PollConfigGrpc.PollConfigBlockingStub pollService() { metadata)); } + /** + * Get the grpc service for sending snapshots. + * + * @return the service to use + */ public SnapshotServiceGrpc.SnapshotServiceStub snapshotService() { final SnapshotServiceGrpc.SnapshotServiceStub snapshotServiceStub = SnapshotServiceGrpc.newStub( getChannel()); @@ -134,7 +152,7 @@ public SnapshotServiceGrpc.SnapshotServiceStub snapshotService() { } private Metadata buildMetaData() { - final IAuthProvider provider = AuthProvider.provider(this.settings); + final IAuthProvider provider = AuthProvider.provider(this.settings, ReflectionUtils.getReflection()); final Map headers = provider.provide(); final Metadata metadata = new Metadata(); diff --git a/agent/src/main/java/com/intergral/deep/agent/logging/Logger.java b/agent/src/main/java/com/intergral/deep/agent/logging/Logger.java index 175b38d..8c3f5c0 100644 --- a/agent/src/main/java/com/intergral/deep/agent/logging/Logger.java +++ b/agent/src/main/java/com/intergral/deep/agent/logging/Logger.java @@ -26,8 +26,19 @@ import java.util.logging.SimpleFormatter; import org.slf4j.LoggerFactory; -public class Logger { +/** + * Logger utility methods. + */ +public final class Logger { + + private Logger() { + } + /** + * Create and configure the java.util.logger for use with deep. + * + * @param settings the settings for deep + */ public static void configureLogging(final Settings settings) { final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("com.intergral"); logger.setUseParentHandlers(false); diff --git a/agent/src/main/java/com/intergral/deep/agent/plugins/PluginLoader.java b/agent/src/main/java/com/intergral/deep/agent/plugins/PluginLoader.java index 453d000..de51ac3 100644 --- a/agent/src/main/java/com/intergral/deep/agent/plugins/PluginLoader.java +++ b/agent/src/main/java/com/intergral/deep/agent/plugins/PluginLoader.java @@ -18,6 +18,8 @@ package com.intergral.deep.agent.plugins; import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.reflection.ReflectionUtils; import com.intergral.deep.agent.settings.Settings; import java.lang.reflect.Constructor; import java.util.ArrayList; @@ -25,19 +27,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PluginLoader { +/** + * this type deals with loading the plugins. + */ +public final class PluginLoader { + + private PluginLoader() { + } private static final Logger LOGGER = LoggerFactory.getLogger(PluginLoader.class); - public static List loadPlugins(final Settings settings) { + /** + * Using the current config load as many plugins as we can. + * + * @param settings the settings for deep + * @param reflection the reflection service to use + * @return the list of active and loaded plugins + */ + public static List loadPlugins(final Settings settings, final IReflection reflection) { final List plugins = settings.getAsList("plugins"); final List loadedPlugins = new ArrayList<>(); for (String plugin : plugins) { try { final Class aClass = Class.forName(plugin); - final Constructor constructor = aClass.getConstructor(); - final Object newInstance = constructor.newInstance(); - final IPlugin asPlugin = (IPlugin) newInstance; + final Constructor constructor = ReflectionUtils.findConstructor(aClass, reflection); + final IPlugin asPlugin = ReflectionUtils.callConstructor(constructor, settings, reflection); if (asPlugin.isActive(settings)) { loadedPlugins.add(asPlugin); } diff --git a/agent/src/main/java/com/intergral/deep/agent/poll/DriftAwareThread.java b/agent/src/main/java/com/intergral/deep/agent/poll/DriftAwareThread.java index d3d5112..e2dc444 100644 --- a/agent/src/main/java/com/intergral/deep/agent/poll/DriftAwareThread.java +++ b/agent/src/main/java/com/intergral/deep/agent/poll/DriftAwareThread.java @@ -17,9 +17,13 @@ package com.intergral.deep.agent.poll; +import com.intergral.deep.agent.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * A thread that can run a {@link ITimerTask} accounting for drifting time. + */ public class DriftAwareThread extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(DriftAwareThread.class); @@ -33,6 +37,8 @@ public class DriftAwareThread extends Thread { /** + * Create a new thread. + * * @param name the name for the thread * @param runnable the {@link ITimerTask} to execute * @param interval the interval in ms between each execution @@ -72,7 +78,9 @@ public void run() { try { // calculate if we woke up early - long now = System.currentTimeMillis(); + long[] nowTuple = Utils.currentTimeNanos(); + // the drift aware only calculates at ms accuracy - but we want to use ns for the time later + long now = nowTuple[0]; long startDelay = checkForEarlyWake(now, this.nextExecutionTime); // if we woke early @@ -96,7 +104,8 @@ public void run() { samplerLock.wait(startDelay); } } - now = System.currentTimeMillis(); + nowTuple = Utils.currentTimeNanos(); + now = nowTuple[0]; startDelay = checkForEarlyWake(now, this.nextExecutionTime); // quick exit if we have been stopped @@ -109,7 +118,7 @@ public void run() { try { debug("Running task."); - this.runnable.run(now); + this.runnable.run(nowTuple[1]); } catch (final Exception e) { error("Exception during task execution: " + e.getMessage(), e); } @@ -174,7 +183,7 @@ protected long checkForEarlyWake(final long now, final long nextExecutionTime) } - public void stopTask() { + void stopTask() { synchronized (this.samplerLock) { this.stopSampler(); this.samplerLock.notifyAll(); diff --git a/agent/src/main/java/com/intergral/deep/agent/poll/ITimerTask.java b/agent/src/main/java/com/intergral/deep/agent/poll/ITimerTask.java index 939747e..632f3ae 100644 --- a/agent/src/main/java/com/intergral/deep/agent/poll/ITimerTask.java +++ b/agent/src/main/java/com/intergral/deep/agent/poll/ITimerTask.java @@ -17,10 +17,15 @@ package com.intergral.deep.agent.poll; +/** + * A task to run in the timer. + * + * @see DriftAwareThread + */ public interface ITimerTask { /** - * This method is called by the {@link DriftAwareThread} at the end of each interval + * This method is called by the {@link DriftAwareThread} at the end of each interval. * * @param now the current time * @throws Exception if the task fails diff --git a/agent/src/main/java/com/intergral/deep/agent/poll/LongPollService.java b/agent/src/main/java/com/intergral/deep/agent/poll/LongPollService.java index bb7ed72..1a23e2c 100644 --- a/agent/src/main/java/com/intergral/deep/agent/poll/LongPollService.java +++ b/agent/src/main/java/com/intergral/deep/agent/poll/LongPollService.java @@ -17,7 +17,6 @@ package com.intergral.deep.agent.poll; -import com.intergral.deep.agent.Utils; import com.intergral.deep.agent.api.resource.Resource; import com.intergral.deep.agent.grpc.GrpcService; import com.intergral.deep.agent.settings.Settings; @@ -35,12 +34,21 @@ import java.util.List; import java.util.stream.Collectors; +/** + * This service deals with polling the remote service for tracepoint configs. + */ public class LongPollService implements ITimerTask { private final Settings settings; private final GrpcService grpcService; private final DriftAwareThread thread; private ITracepointConfig tracepointConfig; + /** + * Create a new service. + * + * @param settings the deep settings + * @param grpcService the deep grpc service + */ public LongPollService(final Settings settings, final GrpcService grpcService) { this.settings = settings; this.grpcService = grpcService; @@ -49,6 +57,10 @@ public LongPollService(final Settings settings, final GrpcService grpcService) { settings.getSettingAs("poll.timer", Integer.class)); } + void setTracepointConfig(final ITracepointConfig tracepointConfig) { + this.tracepointConfig = tracepointConfig; + } + public void start(final ITracepointConfig tracepointConfig) { this.tracepointConfig = tracepointConfig; thread.start(0); @@ -69,7 +81,7 @@ public void run(long now) { } final PollRequest pollRequest = builder - .setTsNanos(Utils.currentTimeNanos()[1]) + .setTsNanos(now) .setResource(buildResource()) .build(); diff --git a/agent/src/main/java/com/intergral/deep/agent/push/PushService.java b/agent/src/main/java/com/intergral/deep/agent/push/PushService.java index 525a265..6a39348 100644 --- a/agent/src/main/java/com/intergral/deep/agent/push/PushService.java +++ b/agent/src/main/java/com/intergral/deep/agent/push/PushService.java @@ -17,8 +17,8 @@ package com.intergral.deep.agent.push; -import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.resource.Resource; import com.intergral.deep.agent.grpc.GrpcService; import com.intergral.deep.agent.settings.Settings; @@ -30,6 +30,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This service deals with pushing the collected data to the remote services. + */ public class PushService { private static final Logger LOGGER = LoggerFactory.getLogger(PushService.class); @@ -42,25 +45,16 @@ public PushService(final Settings settings, final GrpcService grpcService) { this.grpcService = grpcService; } + /** + * Decorate and push the provided snapshot. + * + * @param snapshot the snapshot to push + * @param context the context of where the snapshot is collected + */ public void pushSnapshot(final EventSnapshot snapshot, final ISnapshotContext context) { decorate(snapshot, context); final Snapshot grpcSnapshot = PushUtils.convertToGrpc(snapshot); - this.grpcService.snapshotService().send(grpcSnapshot, new StreamObserver() { - @Override - public void onNext(final SnapshotResponse value) { - LOGGER.debug("Sent snapshot: {}", snapshot.getID()); - } - - @Override - public void onError(final Throwable t) { - LOGGER.error("Error sending snapshot: {}", snapshot.getID(), t); - } - - @Override - public void onCompleted() { - - } - }); + this.grpcService.snapshotService().send(grpcSnapshot, new LoggingObserver(snapshot.getID())); } private void decorate(final EventSnapshot snapshot, final ISnapshotContext context) { @@ -77,4 +71,29 @@ private void decorate(final EventSnapshot snapshot, final ISnapshotContext conte } snapshot.close(); } + + static class LoggingObserver implements StreamObserver { + + private final String id; + + public LoggingObserver(final String id) { + this.id = id; + } + + @Override + public void onNext(final SnapshotResponse value) { + LOGGER.debug("Sent snapshot: {}", id); + + } + + @Override + public void onError(final Throwable t) { + LOGGER.error("Error sending snapshot: {}", id, t); + } + + @Override + public void onCompleted() { + + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/push/PushUtils.java b/agent/src/main/java/com/intergral/deep/agent/push/PushUtils.java index cc9e359..33d9ca5 100644 --- a/agent/src/main/java/com/intergral/deep/agent/push/PushUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/push/PushUtils.java @@ -26,6 +26,7 @@ import com.intergral.deep.proto.common.v1.KeyValue; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.StackFrame; +import com.intergral.deep.proto.tracepoint.v1.StackFrame.Builder; import com.intergral.deep.proto.tracepoint.v1.TracePointConfig; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.WatchResult; @@ -34,8 +35,20 @@ import java.util.Map; import java.util.stream.Collectors; -public class PushUtils { +/** + * Utilities to convert to grpc snapshot types. + */ +public final class PushUtils { + + private PushUtils() { + } + /** + * Convert an internal snapshot into a grpc snapshot. + * + * @param snapshot the internal snapshot to convert + * @return the converted snapshot + */ public static Snapshot convertToGrpc(final EventSnapshot snapshot) { return Snapshot.newBuilder() .setID(ByteString.copyFromUtf8(snapshot.getID())) @@ -109,28 +122,43 @@ private static com.intergral.deep.proto.tracepoint.v1.VariableID convertVariable private static Iterable convertFrames( final Collection frames) { - return frames.stream().map(stackFrame -> StackFrame.newBuilder() - .setFileName(stackFrame.getFileName()) - .setMethodName(stackFrame.getMethodName()) - .setLineNumber(stackFrame.getLineNumber()) - .setClassName(stackFrame.getClassName()) -// .setIsAsync( false ) -// .setColumnNumber( 0 ) - //todo update for JSP/CFM -// .setTranspiledFileName( "" ) -// .setTranspiledLineNumber( 0 ) -// .setTranspiledColumnNumber( 0 ) - .addAllVariables(covertVariables(stackFrame.getFrameVariables())) - .setAppFrame(stackFrame.isAppFrame()) - .build()).collect(Collectors.toList()); + return frames.stream().map(stackFrame -> { + final Builder builder = StackFrame.newBuilder() + .setFileName(stackFrame.getFileName()) + .setMethodName(stackFrame.getMethodName()) + .setLineNumber(stackFrame.getLineNumber()) + .setClassName(stackFrame.getClassName()) + // Java does not have async frames or column Numbers + //.setIsAsync( false ) + //.setColumnNumber( 0 ) + // .setTranspiledColumnNumber(0) + .addAllVariables(covertVariables(stackFrame.getFrameVariables())) + .setAppFrame(stackFrame.isAppFrame()) + .setNativeFrame(stackFrame.isNativeFrame()); + + // only set transpiled if they are set + if (stackFrame.getTranspiledFile() != null) { + builder.setTranspiledFileName(stackFrame.getTranspiledFile()); + } + if (stackFrame.getTranspiledLine() != -1) { + builder.setTranspiledLineNumber(stackFrame.getTranspiledLine()); + } + return builder.build(); + }).collect(Collectors.toList()); } - private static Iterable covertVariables( + public static Collection covertVariables( final Collection frameVariables) { return frameVariables.stream().map(PushUtils::convertVariableID).collect(Collectors.toList()); } - private static Map convertVarLookup( + /** + * Convert a variable lookup into grpc variables. + * + * @param varLookup the lookup to convert + * @return the converted variables + */ + public static Map convertVarLookup( final Map varLookup) { return varLookup.entrySet() .stream() diff --git a/agent/src/main/java/com/intergral/deep/agent/resource/JavaResourceDetector.java b/agent/src/main/java/com/intergral/deep/agent/resource/JavaResourceDetector.java new file mode 100644 index 0000000..ff250cf --- /dev/null +++ b/agent/src/main/java/com/intergral/deep/agent/resource/JavaResourceDetector.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.resource; + +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.ResourceProvider; +import java.util.Collections; + +/** + * A resource provider that detects the hava version to add to the resource. + */ +public class JavaResourceDetector implements ResourceProvider { + + @Override + public Resource createResource(final ISettings settings) { + final String property = System.getProperty("java.version"); + if (property == null) { + return null; + } + return Resource.create(Collections.singletonMap("java_version", property)); + } +} diff --git a/agent/src/main/java/com/intergral/deep/agent/resource/ResourceDetector.java b/agent/src/main/java/com/intergral/deep/agent/resource/ResourceDetector.java index 8f13691..178de41 100644 --- a/agent/src/main/java/com/intergral/deep/agent/resource/ResourceDetector.java +++ b/agent/src/main/java/com/intergral/deep/agent/resource/ResourceDetector.java @@ -1,23 +1,11 @@ /* - * Copyright (C) 2023 Intergral GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 */ package com.intergral.deep.agent.resource; -import com.intergral.deep.agent.api.resource.ConfigurationException; +import com.intergral.deep.agent.api.DeepRuntimeException; import com.intergral.deep.agent.api.resource.Resource; import com.intergral.deep.agent.api.resource.ResourceAttributes; import com.intergral.deep.agent.api.spi.ConditionalResourceProvider; @@ -32,22 +20,35 @@ import java.util.Map; import java.util.Set; -public class ResourceDetector { +/** + * Utilities to create the resource for this agent. + */ +public final class ResourceDetector { + + private ResourceDetector() { + } // Visible for testing static final String ATTRIBUTE_PROPERTY = "deep.resource.attributes"; static final String SERVICE_NAME_PROPERTY = "deep.service.name"; static final String DISABLED_ATTRIBUTE_KEYS = "deep.resource.disabled.keys"; + static final String ENABLED_PROVIDERS_KEY = "deep.java.enabled.resource.providers"; + static final String DISABLED_PROVIDERS_KEY = "deep.java.disabled.resource.providers"; - public static Resource configureResource( - Settings config, - ClassLoader serviceClassLoader) { + /** + * Create and configure a resource for this agent. + * + * @param settings the settings for the agent + * @param serviceClassLoader the class loader to use to load the SPI services + * @return the loaded resource + */ + public static Resource configureResource(Settings settings, ClassLoader serviceClassLoader) { Resource result = Resource.create(Collections.emptyMap()); Set enabledProviders = - new HashSet<>(config.getAsList("deep.java.enabled.resource.providers")); + new HashSet<>(settings.getAsList(ENABLED_PROVIDERS_KEY)); Set disabledProviders = - new HashSet<>(config.getAsList("deep.java.disabled.resource.providers")); + new HashSet<>(settings.getAsList(DISABLED_PROVIDERS_KEY)); for (ResourceProvider resourceProvider : SpiUtil.loadOrdered(ResourceProvider.class, serviceClassLoader)) { @@ -59,29 +60,29 @@ public static Resource configureResource( continue; } if (resourceProvider instanceof ConditionalResourceProvider - && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) { + && !((ConditionalResourceProvider) resourceProvider).shouldApply(settings, result)) { continue; } - result = result.merge(resourceProvider.createResource(config)); + result = result.merge(resourceProvider.createResource(settings)); } - result = result.merge(createEnvironmentResource(config)); + result = result.merge(createEnvironmentResource(settings)); - result = filterAttributes(result, config); + result = filterAttributes(result, settings); return result; } - private static Resource createEnvironmentResource(Settings config) { - return Resource.create(getAttributes(config), null); + private static Resource createEnvironmentResource(Settings settings) { + return Resource.create(getAttributes(settings), null); } // visible for testing - static Map getAttributes(Settings configProperties) { + static Map getAttributes(Settings settings) { Map resourceAttributes = new HashMap<>(); try { for (Map.Entry entry : - configProperties.getMap(ATTRIBUTE_PROPERTY).entrySet()) { + settings.getMap(ATTRIBUTE_PROPERTY).entrySet()) { resourceAttributes.put( entry.getKey(), // Attributes specified via deep.resource.attributes follow the W3C Baggage spec and @@ -91,9 +92,9 @@ static Map getAttributes(Settings configProperties) { } } catch (UnsupportedEncodingException e) { // Should not happen since always using standard charset - throw new ConfigurationException("Unable to decode resource attributes.", e); + throw new DeepRuntimeException("Unable to decode resource attributes.", e); } - String serviceName = configProperties.getSettingAs(SERVICE_NAME_PROPERTY, String.class); + String serviceName = settings.getSettingAs(SERVICE_NAME_PROPERTY, String.class); if (serviceName != null) { resourceAttributes.put(ResourceAttributes.SERVICE_NAME, serviceName); } @@ -101,8 +102,8 @@ static Map getAttributes(Settings configProperties) { } // visible for testing - static Resource filterAttributes(Resource resource, Settings configProperties) { - Set disabledKeys = new HashSet<>(configProperties.getAsList( + static Resource filterAttributes(Resource resource, Settings settings) { + Set disabledKeys = new HashSet<>(settings.getAsList( DISABLED_ATTRIBUTE_KEYS)); final Map attributes = resource.getAttributes(); diff --git a/agent/src/main/java/com/intergral/deep/agent/resource/SpiUtil.java b/agent/src/main/java/com/intergral/deep/agent/resource/SpiUtil.java index 733153d..fe2bee7 100644 --- a/agent/src/main/java/com/intergral/deep/agent/resource/SpiUtil.java +++ b/agent/src/main/java/com/intergral/deep/agent/resource/SpiUtil.java @@ -1,18 +1,6 @@ /* - * Copyright (C) 2023 Intergral GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 */ package com.intergral.deep.agent.resource; @@ -23,7 +11,13 @@ import java.util.List; import java.util.ServiceLoader; -public class SpiUtil { +/** + * Utilities to load SPI services. + */ +public final class SpiUtil { + + private SpiUtil() { + } static List loadOrdered(Class spiClass, ClassLoader serviceClassLoader) { diff --git a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java index 90e7866..60f4b61 100644 --- a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java +++ b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java @@ -39,18 +39,27 @@ import java.util.logging.Level; import java.util.regex.Pattern; +/** + * A service that handles the general config of the deep agent. + */ public class Settings implements ISettings { private static final AtomicBoolean IS_ACTIVE = new AtomicBoolean(true); private final Properties properties; - private Resource resource; - private Collection plugins; private final Collection customPlugins = new ArrayList<>(); + private Resource resource; + private Collection plugins = Collections.emptyList(); private Settings(Properties properties) { this.properties = properties; } + /** + * Build a new settings service from the input arguments from the agent. + * + * @param agentArgs the agent input arguments + * @return the new settings service + */ public static Settings build(final Map agentArgs) { final String settingFile = readProperty("deep.settings", agentArgs); final InputStream propertiesStream; @@ -79,6 +88,7 @@ static Settings build(final Map agentArgs, final InputStream str properties.load(resourceAsStream); } catch (IOException e) { // logging is not initialized until after the settings class + //noinspection CallToPrintStackTrace e.printStackTrace(); } @@ -119,6 +129,14 @@ private static String readSystemProperty(final String key) { return System.getProperty("deep." + key); } + /** + * Coerce a value into a given type. + * + * @param str the value to coerce + * @param type the type to change to + * @param the type to return as + * @return the value as the given type, or {@code null} + */ @SuppressWarnings("unchecked") public static T coerc(final String str, final Class type) { if (str == null) { @@ -128,9 +146,9 @@ public static T coerc(final String str, final Class type) { if (type == Boolean.class || type == boolean.class) { return (T) Boolean.valueOf(str); } else if (type == Integer.class || type == int.class) { - return (T) Integer.valueOf(str); + return (T) Integer.valueOf(Double.valueOf(str).intValue()); } else if (type == Long.class || type == long.class) { - return (T) Long.valueOf(str); + return (T) Long.valueOf(Double.valueOf(str).longValue()); } else if (type == String.class) { return (T) str; } else if (type == Double.class || type == double.class) { @@ -185,9 +203,10 @@ private static List makeList(final String str) { return Arrays.asList(split); } + @Override public T getSettingAs(String key, Class clazz) { // special handling for enabled key - if(key.equals(ISettings.KEY_ENABLED)){ + if (key.equals(ISettings.KEY_ENABLED)) { return coerc(String.valueOf(isActive()), clazz); } final String property = this.properties.getProperty(key); @@ -217,30 +236,42 @@ public Map getMap(String key) { return settingAs; } + /** + * Get the deep service host name. + * + * @return the service host name + * @throws InvalidConfigException if the value cannot be obtained + */ public String getServiceHost() { final String serviceUrl = getSettingAs(ISettings.KEY_SERVICE_URL, String.class); - if (serviceUrl.contains("://")) { + if (serviceUrl != null && serviceUrl.contains("://")) { try { return new URL(serviceUrl).getHost(); } catch (MalformedURLException e) { throw new InvalidConfigException(ISettings.KEY_SERVICE_URL, serviceUrl, e); } - } else if (serviceUrl.contains(":")) { + } else if (serviceUrl != null && serviceUrl.contains(":")) { return serviceUrl.split(":")[0]; } throw new InvalidConfigException(ISettings.KEY_SERVICE_URL, serviceUrl); } + /** + * Get the deep service port number. + * + * @return the service port number + * @throws InvalidConfigException if the value cannot be obtained + */ public int getServicePort() { final String serviceUrl = getSettingAs(ISettings.KEY_SERVICE_URL, String.class); - if (serviceUrl.contains("://")) { + if (serviceUrl != null && serviceUrl.contains("://")) { try { return new URL(serviceUrl).getPort(); } catch (MalformedURLException e) { throw new InvalidConfigException(ISettings.KEY_SERVICE_URL, serviceUrl, e); } - } else if (serviceUrl.contains(":")) { + } else if (serviceUrl != null && serviceUrl.contains(":")) { return Integer.parseInt(serviceUrl.split(":")[1]); } @@ -252,10 +283,21 @@ public Resource getResource() { return this.resource; } + /** + * Get the resource value for this agent. + * + * @param resource the resource that describes this agent + */ public void setResource(Resource resource) { this.resource = resource; } + /** + * Get the value as a list. + * + * @param key the key for the setting + * @return the value as a list + */ public List getAsList(final String key) { final List settingAs = getSettingAs(key, List.class); if (settingAs == null) { @@ -264,28 +306,50 @@ public List getAsList(final String key) { return settingAs; } + /** + * Get all configured plugins. + * + * @return the full list on plugins + */ public Collection getPlugins() { final ArrayList actualPlugins = new ArrayList<>(this.plugins); actualPlugins.addAll(this.customPlugins); return actualPlugins; } + /** + * Set configured plugins. + * + * @param plugins the plugins to use + */ public void setPlugins(Collection plugins) { this.plugins = plugins; } + /** + * Add a custom plugin. + * + * @param plugin the plugin to add + * @see com.intergral.deep.agent.api.IDeep#registerPlugin(IPlugin) + */ public void addPlugin(final IPlugin plugin) { - final Optional first = this.customPlugins.stream().filter(iPlugin -> iPlugin.name().equals(plugin.name())).findFirst(); + final Optional first = this.customPlugins.stream().filter(current -> current.name().equals(plugin.name())).findFirst(); if (first.isPresent()) { throw new IllegalStateException(String.format("Cannot add duplicate named (%s) plugin", plugin.name())); } this.customPlugins.add(plugin); } + /** + * Remove a plugin that was added via {@link #addPlugin(IPlugin)}. + * + * @param plugin the plugin to remove + */ public void removePlugin(final IPlugin plugin) { - this.plugins.removeIf(iPlugin -> iPlugin.name().equals(plugin.name())); + this.customPlugins.removeIf(current -> current.name().equals(plugin.name())); } + @Override public IPlugin getPlugin(final String name) { final Collection allPlugins = this.getPlugins(); @@ -297,14 +361,29 @@ public IPlugin getPlugin(final String name) { return null; } + /** + * Is deep currently active. + * + * @return {@code true} if deep is active, else {@code false} + */ public boolean isActive() { return IS_ACTIVE.get(); } + /** + * Allows enabling or disabled deep. + *

+ * A disabled deep will remove installed tracepoints and stop polling. It will not remove itself. + * + * @param state the new state + */ public void setActive(boolean state) { IS_ACTIVE.set(state); } + /** + * Used to indicate an invalid config value. + */ public static class InvalidConfigException extends RuntimeException { public InvalidConfigException(String key, String value) { diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/ITracepointConfig.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/ITracepointConfig.java index 5e2603b..04f1ccd 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/ITracepointConfig.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/ITracepointConfig.java @@ -20,13 +20,41 @@ import com.intergral.deep.agent.types.TracePointConfig; import java.util.Collection; +/** + * This is the interface to the config services. The implementation of this service should act as the manager between incoming tracepoint + * configs and instrumentation. To help reduce the time spent in the instrumentor. + */ public interface ITracepointConfig { + /** + * Called when there is no change to the config so just update last seen. + * + * @param tsNano the time of the config update + */ void noChange(final long tsNano); + /** + * This indicates that the config from the servers has changed, and we should inform the instrumentation services. + * + * @param tsNano the time of the update + * @param hash the new config hash + * @param tracepoints the new config + */ void configUpdate(final long tsNano, final String hash, final Collection tracepoints); + /** + * Get the hash of the config last used to update the config. This hash should be sent with the calls for new configs, so the server knows + * what the clients config is and can detect changes. + * + * @return the current hash. + */ String currentHash(); + /** + * Load the full configs for the given tracepoints ids. + * + * @param tracepointId the tracepoint ids + * @return a collection of all the matched tracepoints + */ Collection loadTracepointConfigs(final Collection tracepointId); } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointConfigService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointConfigService.java index 38ff7fb..70ac34b 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointConfigService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointConfigService.java @@ -29,6 +29,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This service deals with mapping the response from polls into actions to install tracepoints. + */ public class TracepointConfigService implements ITracepointConfig { private static final Logger LOGGER = LoggerFactory.getLogger(TracepointConfigService.class); @@ -76,6 +79,16 @@ public Collection loadTracepointConfigs(final Collection args, final Collection watches) { final TracePointConfig tracePointConfig = new TracePointConfig(UUID.randomUUID().toString(), path, line, args, watches); @@ -84,7 +97,20 @@ public TracePointConfig addCustom(final String path, final int line, final Map current.getId().equals(tracePointConfig.getId())); + final boolean removed = this.customTracepoints.removeIf(current -> current.getId().equals(tracePointConfig.getId())); + if (removed) { + this.processChange(); + } + } + + public long lastUpdate() { + return this.lastUpdate; } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointUtils.java index 91d0641..2a9566f 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/TracepointUtils.java @@ -21,10 +21,23 @@ import com.intergral.deep.agent.tracepoint.inst.InstUtils; import com.intergral.deep.agent.types.TracePointConfig; -public class TracepointUtils { +/** + * Utilities for tracepoint configuration. + */ +public final class TracepointUtils { + + private TracepointUtils() { + } + /** + * We normally get set the source file name, we need to convert this to a Java class name. + * + * @param tp the tracepoint to process + * @return the internal class name to install the tracepoint in, or {@code cfm} or {@code jsp} if the computed class is a CFM or JSP + * class. + */ public static String estimatedClassRoot(final TracePointConfig tp) { - // the arg class_name is sent from ATP as it has the full class name already + // we allow the class name to be sent for specific cases final String className = tp.getArg("class_name", String.class, null); if (className != null) { return InstUtils.internalClass(className); @@ -39,11 +52,12 @@ private static String asFullClassName(TracePointConfig tp) { } - static String parseFullClassName(final String rawRelPath, final String srcRootArg) { + private static String parseFullClassName(final String rawRelPath, final String srcRootArg) { + // cf classes are handled specially if (rawRelPath.endsWith(".cfm") || rawRelPath.endsWith(".cfc")) { return "cfm"; } - + // jsp classes are handled specially if (rawRelPath.endsWith(".jsp")) { return "jsp"; } @@ -60,23 +74,23 @@ static String parseFullClassName(final String rawRelPath, final String srcRootAr } if (srcRootArg != null) { if (relPath.startsWith(srcRootArg)) { - return Utils.trim(relPath.substring(srcRootArg.length()), "/"); + return Utils.trimPrefix(relPath.substring(srcRootArg.length()), "/"); } } else if (relPath.contains("/src/main/")) { final String mainDir = relPath.substring(relPath.indexOf("/src/main/") + 11); final int i = mainDir.indexOf('/'); - return Utils.trim(mainDir.substring(i), "/"); + return Utils.trimPrefix(mainDir.substring(i), "/"); } else if (relPath.contains("/src/test/")) { final String mainDir = relPath.substring(relPath.indexOf("/src/test/") + 11); final int i = mainDir.indexOf('/'); - return Utils.trim(mainDir.substring(i), "/"); + return Utils.trimPrefix(mainDir.substring(i), "/"); } - final String trim = Utils.trim(relPath, "/"); + final String trim = Utils.trimPrefix(relPath, "/"); // this is just to ensure the file name is never empty // this only happens on non class files such as '.gitignore' // rather then return empty name we return the raw path if (trim.isEmpty()) { - return Utils.trim(rawRelPath, "/"); + return Utils.trimPrefix(rawRelPath, "/"); } return trim; } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluator.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluator.java index 3834f4f..b9c04ee 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluator.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluator.java @@ -18,12 +18,16 @@ package com.intergral.deep.agent.tracepoint.cf; import com.intergral.deep.agent.api.plugin.AbstractEvaluator; -import java.lang.reflect.InvocationTargetException; +import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.api.plugin.LazyEvaluator.IEvaluatorLoader; import java.lang.reflect.Method; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * The evaluator to use when running a CF Callback. + */ public class CFEvaluator extends AbstractEvaluator { private static final Logger LOGGER = LoggerFactory.getLogger(CFEvaluator.class); @@ -42,9 +46,27 @@ public CFEvaluator(final Object page, final Method evaluate) { public Object evaluateExpression(final String expression, final Map values) { try { return evaluate.invoke(page, expression); - } catch (IllegalAccessException | InvocationTargetException e) { + } catch (Throwable e) { LOGGER.debug("Unable to evaluate expression {}", expression); } return null; } + + /** + * The loader to use when executing a CF Callback. + */ + public static class Loader implements IEvaluatorLoader { + + private final Map variables; + + public Loader(final Map variables) { + + this.variables = variables; + } + + @Override + public IEvaluator load() { + return CFUtils.findCfEval(variables); + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFFrameProcessor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFFrameProcessor.java index 6f85202..86078cb 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFFrameProcessor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFFrameProcessor.java @@ -27,6 +27,10 @@ import java.util.HashMap; import java.util.Map; +/** + * We want to map the variables from the Java variables to the CF variables. So we have a custom processor for CF that lets us perform this + * mapping. + */ public class CFFrameProcessor extends FrameProcessor { public CFFrameProcessor(final Settings settings, @@ -56,7 +60,7 @@ protected boolean isAppFrame(final StackTraceElement stackTraceElement) { @Override protected Map selectVariables(final int frameIndex) { - if (frameIndex != 0 || getTracepointConfig("cf.raw", Boolean.class, false)) { + if (frameIndex != 0 || frameConfig.isCfRaw()) { return super.selectVariables(frameIndex); } @@ -128,7 +132,7 @@ Map mapCFScopes(final Map variables) { /** - * Special handling for lucee scopes + * Special handling for lucee scopes. * * @param variables the variables * @return the scopes for lucee diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFUtils.java index 1ef9d72..e577807 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/cf/CFUtils.java @@ -18,22 +18,40 @@ package com.intergral.deep.agent.tracepoint.cf; import com.intergral.deep.agent.ReflectionUtils; +import com.intergral.deep.agent.Utils; import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.types.TracePointConfig; import java.lang.reflect.Method; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -public class CFUtils { +/** + * Utilities to help with CF related item. + */ +public final class CFUtils { private CFUtils() { } - public static IEvaluator findCfEval(final Map map) { - final Object that = map.get("this"); + /** + * Find the evaluator to use for CF. + *

+ * Cf provides an {@code Evaluate} method on the page object that can be used to evaluate strings. This method tries to find that method. + * + * @param variables the variables to scan + * @return the evaluate to use, or {@code null} + */ + public static IEvaluator findCfEval(final Map variables) { + final Object that = variables.get("this"); if (isLucee(that)) { - return findLuceeEvaluator(map); + return findLuceeEvaluator(variables); } - final Object page = CFUtils.findPage(map); + final Object page = CFUtils.findPage(variables); if (page == null) { return null; } @@ -64,9 +82,17 @@ private static IEvaluator findLuceeEvaluator(final Map map) { } + /** + * CF doesn't use the java method name, so we look for the UDF method name in the variables. + * + * @param variables the variables to look in + * @param className the name of the class + * @param stackIndex the stack index + * @return the name of the UDF method, or {@code null} + */ public static String findUdfName(final Map variables, final String className, - final int i) { - if (i == 0) { + final int stackIndex) { + if (stackIndex == 0) { final Object aThis = variables.get("this"); if (aThis == null || !isUdfMethod(aThis)) { return null; @@ -95,13 +121,12 @@ static boolean isScope(final Object varScope) { } - static boolean isLuceeScope(final Object varScope) { - return varScope instanceof Map - && varScope.getClass().getName().startsWith("lucee") - && varScope.getClass().getName().contains("scope"); - } - - + /** + * Find the page object. + * + * @param localVars the variables to scan + * @return the page object or {@code null} + */ public static Object findPage(final Map localVars) { final Object aThis = localVars.get("this"); if (aThis == null) { @@ -115,6 +140,12 @@ public static Object findPage(final Map localVars) { } + /** + * Find the page context for cf. + * + * @param localVars the variables to search + * @return the page context or {@code null} + */ public static Object findPageContext(final Map localVars) { final Object page = findPage(localVars); if (page == null) { @@ -124,11 +155,23 @@ public static Object findPageContext(final Map localVars) { } + /** + * Is this class a possible cf class. + * + * @param classname the class name + * @return {@code true} if the class is cf + */ public static boolean isCfClass(final String classname) { return classname.startsWith("cf") || classname.endsWith("$cf"); } + /** + * Is the file a possible CF file. + * + * @param fileName the file name to check + * @return {@code true} if the file is a cf file, else {@code false} + */ public static boolean isCFFile(final String fileName) { if (fileName == null) { return false; @@ -137,7 +180,12 @@ public static boolean isCFFile(final String fileName) { || fileName.endsWith(".cfml"); } - + /** + * Are we a lucee page. + * + * @param that the object to check + * @return {@code true} if we are a lucee object + */ public static boolean isLucee(final Object that) { return that != null && that.getClass().getSuperclass() != null @@ -146,7 +194,14 @@ public static boolean isLucee(final Object that) { } + /** + * When running on Lucee servers we can guess the source from the class name. + * + * @param classname the class we are processing + * @return the source file name, or {@code null} + */ public static String guessSource(final String classname) { + // if the classname isn't a lucee class then skip it if (!classname.endsWith("$cf")) { return null; } @@ -155,4 +210,56 @@ public static String guessSource(final String classname) { .substring(0, classname.length() - 3) .replace("_", "."); } + + + /** + * Load the CF tracepoints based on the location url. + * + * @param location the location to look for + * @param values the tracepoints to look at + * @return the set of tracepoints that match this location + */ + public static Set loadCfTracepoints(final URL location, + final Map values) { + final Set iBreakpoints = new HashSet<>(); + final Collection breakpoints = values.values(); + for (TracePointConfig breakpoint : breakpoints) { + final String srcRoot = breakpoint.getArgs().get("src_root"); + final String relPathFromNv = breakpoint.getPath(); + final String locationString = location.toString(); + if (srcRoot != null && locationString.endsWith(relPathFromNv.substring(srcRoot.length())) + || locationString.endsWith(relPathFromNv) + || relPathFromNv.startsWith("/src/main/cfml") + && locationString.endsWith(relPathFromNv.substring("/src/main/cfml".length())) + ) { + iBreakpoints.add(breakpoint); + } + } + return iBreakpoints; + } + + /** + * Load the CF tracepoints based on the location string. + * + * @param location the location to look for + * @param values the tracepoints to look at + * @return the set of tracepoints that match this location + */ + public static Set loadCfTracepoints( + final String location, + final Map values) { + if (location == null) { + return Collections.emptySet(); + } + final Set iBreakpoints = new HashSet<>(); + final Collection breakpoints = values.values(); + for (TracePointConfig breakpoint : breakpoints) { + final String relPathFromNv = breakpoint.getPath(); + // some versions of lucee use lowercase file names + if (Utils.endsWithIgnoreCase(relPathFromNv, location)) { + iBreakpoints.add(breakpoint); + } + } + return iBreakpoints; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorService.java index 8277cd0..d165e1d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorService.java @@ -22,12 +22,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class EvaluatorService { +/** + * General wrapper around non CF evaluators. + */ +public final class EvaluatorService { + + private EvaluatorService() { + } private static final Logger LOGGER = LoggerFactory.getLogger(EvaluatorService.class); private static final Exception NO_EVALUATOR_EXCEPTION = new RuntimeException( "No evaluator available."); + /** + * Create an evaluator. + * + * @return the new evaluator. + */ public static IEvaluator createEvaluator() { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); final IEvaluator iEvaluator = NashornReflectEvaluator.loadEvaluator(contextClassLoader); diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluator.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluator.java index 66ad4e3..b895472 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluator.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluator.java @@ -46,6 +46,12 @@ private NashornReflectEvaluator(final Object engine) { this.engine = engine; } + /** + * Load the evaluator. + * + * @param loader the class loader to use + * @return the evaluator or {@code null} if Nashorn is not available. + */ public static IEvaluator loadEvaluator(final ClassLoader loader) { // this stops us from trying to load nashorn again if it failed if (!NASHORN_AVAILABLE) { @@ -99,13 +105,13 @@ public Object evaluateExpression(final String expression, final Map> CALLBACKS = new ThreadLocal>() -// { -// @Override -// protected Deque initialValue() -// { -// return new ArrayDeque<>(); -// } -// }; + // public static final ThreadLocal> CALLBACKS = new ThreadLocal>() + // { + // @Override + // protected Deque initialValue() + // { + // return new ArrayDeque<>(); + // } + // }; private static final Logger LOGGER = LoggerFactory.getLogger(Callback.class); @SuppressWarnings("AnonymousHasLambdaAlternative") private static final ThreadLocal FIRING = new ThreadLocal() { @@ -58,15 +64,23 @@ protected Boolean initialValue() { }; private static Settings SETTINGS; - private static TracepointConfigService BREAKPOINT_SERVICE; + private static TracepointConfigService TRACEPOINT_SERVICE; private static PushService PUSH_SERVICE; private static int offset; - public static void init(final Settings settings, - final TracepointConfigService breakpointService, + /** + * Initialise the callback with the deep services. + * + * @param settings the deep settings + * @param tracepointConfigService the tracepoint service + * @param pushService the push service + */ + public static void init( + final Settings settings, + final TracepointConfigService tracepointConfigService, final PushService pushService) { Callback.SETTINGS = settings; - Callback.BREAKPOINT_SERVICE = breakpointService; + Callback.TRACEPOINT_SERVICE = tracepointConfigService; Callback.PUSH_SERVICE = pushService; if (Visitor.CALLBACK_CLASS == Callback.class) { offset = 3; @@ -77,7 +91,7 @@ public static void init(final Settings settings, /** - * The main entry point for CF ASM injected breakpoints + * The main entry point for CF ASM injected breakpoints. * * @param bpIds the bp ids to trigger * @param filename the filename of the breakpoint hit @@ -89,7 +103,7 @@ public static void callBackCF(final List bpIds, final int lineNo, final Map variables) { try { - final IEvaluator evaluator = new LazyEvaluator(() -> CFUtils.findCfEval(variables)); + final IEvaluator evaluator = new LazyEvaluator(new CFEvaluator.Loader(variables)); commonCallback(bpIds, filename, lineNo, variables, evaluator, CFFrameProcessor::new); } catch (Throwable t) { LOGGER.debug("Unable to process tracepoint {}:{}", filename, lineNo, t); @@ -98,7 +112,7 @@ public static void callBackCF(final List bpIds, /** - * The main entry point for non CF ASM injected breakpoints + * The main entry point for non CF ASM injected breakpoints. * * @param bpIds the bp ids to trigger * @param filename the filename of the breakpoint hit @@ -134,11 +148,11 @@ private static void commonCallback(final List tracepointIds, LOGGER.trace("callBack for {}:{} -> {}", filename, lineNo, tracepointIds); // possible race condition but unlikely - if (Callback.BREAKPOINT_SERVICE == null) { + if (Callback.TRACEPOINT_SERVICE == null) { return; } - final Collection tracePointConfigs = Callback.BREAKPOINT_SERVICE.loadTracepointConfigs( + final Collection tracePointConfigs = Callback.TRACEPOINT_SERVICE.loadTracepointConfigs( tracepointIds); StackTraceElement[] stack = Thread.currentThread().getStackTrace(); @@ -171,132 +185,133 @@ private static void commonCallback(final List tracepointIds, } } - // the below methods are here as they are called from instrumented classes. This is a throwback to NV that we will address in later releases + // the below methods are here as they are called from instrumented classes. + // This is a throwback to NV that we will address in later releases public static void callBackException(final Throwable t) { -// System.out.println( "callBackException" ); -// try -// { -// LOGGER.debug( "Capturing throwable", t ); -// final CallbackHook element = CALLBACKS.get().peekLast(); -// if( element != null && element.isHook()) -// { -// element.setThrowable( t ); -// } -// } -// catch( Throwable tt ) -// { -// LOGGER.debug( "Error processing callback", tt ); -// } + // System.out.println( "callBackException" ); + // try + // { + // LOGGER.debug( "Capturing throwable", t ); + // final CallbackHook element = CALLBACKS.get().peekLast(); + // if( element != null && element.isHook()) + // { + // element.setThrowable( t ); + // } + // } + // catch( Throwable tt ) + // { + // LOGGER.debug( "Error processing callback", tt ); + // } } public static void callBackFinally(final Set breakpointIds, final Map map) { -// System.out.println( "callBackFinally" ); -// for( String breakpointId : breakpointIds ) -// { -// try -// { -// LOGGER.debug( "{}: Processing finally", breakpointId ); -// final Deque hooks = CALLBACKS.get(); -// final CallbackHook pop = hooks.pollLast(); -// LOGGER.debug( "Dequeue state: {}", hooks ); -// if( pop == null || !pop.isHook() ) -// { -// LOGGER.debug( "No callback pending. {}", pop ); -// continue; -// } -// -// final boolean processFrameStack = (pop.value.getType().equals( ITracepoint.STACK_TYPE )) || pop.value.getType() -// .equals( ITracepoint.FRAME_TYPE ) || pop.value.getType().equals( ITracepoint.LOG_POINT_TYPE ) -// || pop.value.getType().equals( ITracepoint.NO_FRAME_TYPE ); -// -// final List frames; -// if( pop.value.getArg( ITracepoint.LINE_HOOK_ARG_KEY ).equals( ITracepoint.LINE_HOOK_DATA_RIGHT ) ) -// { -// frames = pop.snapshotHandler.processFrames( map, processFrameStack, System.currentTimeMillis() ); -// } -// else -// { -// frames = pop.frames; -// } -// -// // trim frames to our type -// @SuppressWarnings({ "RedundantTypeArguments", "Convert2Diamond" }) -// final IRequestDecorator iRequestDecorator = pop.snapshotHandler.generateSnapshotData( pop.watchValues, pop.value, -// frames, Collections.emptySet(), -// NVError.fromThrowable( pop.throwable, new HashMap() ), -// System.currentTimeMillis(), Callback.CLIENT_CONFIG.getTags() ); -// -// final EventSnapshot eventSnapshot = iRequestDecorator.getBody(); -// addDynamicTags( pop.value, eventSnapshot ); -// -// sendEvent( iRequestDecorator, eventSnapshot ); -// } -// catch( Throwable t ) -// { -// LOGGER.debug( "Error processing callback", t ); -// } -// } + // System.out.println( "callBackFinally" ); + // for( String breakpointId : breakpointIds ) + // { + // try + // { + // LOGGER.debug( "{}: Processing finally", breakpointId ); + // final Deque hooks = CALLBACKS.get(); + // final CallbackHook pop = hooks.pollLast(); + // LOGGER.debug( "Dequeue state: {}", hooks ); + // if( pop == null || !pop.isHook() ) + // { + // LOGGER.debug( "No callback pending. {}", pop ); + // continue; + // } + // + // final boolean processFrameStack = (pop.value.getType().equals( ITracepoint.STACK_TYPE )) || pop.value.getType() + // .equals( ITracepoint.FRAME_TYPE ) || pop.value.getType().equals( ITracepoint.LOG_POINT_TYPE ) + // || pop.value.getType().equals( ITracepoint.NO_FRAME_TYPE ); + // + // final List frames; + // if( pop.value.getArg( ITracepoint.LINE_HOOK_ARG_KEY ).equals( ITracepoint.LINE_HOOK_DATA_RIGHT ) ) + // { + // frames = pop.snapshotHandler.processFrames( map, processFrameStack, System.currentTimeMillis() ); + // } + // else + // { + // frames = pop.frames; + // } + // + // // trim frames to our type + // @SuppressWarnings({ "RedundantTypeArguments", "Convert2Diamond" }) + // final IRequestDecorator iRequestDecorator = pop.snapshotHandler.generateSnapshotData( pop.watchValues, pop.value, + // frames, Collections.emptySet(), + // NVError.fromThrowable( pop.throwable, new HashMap() ), + // System.currentTimeMillis(), Callback.CLIENT_CONFIG.getTags() ); + // + // final EventSnapshot eventSnapshot = iRequestDecorator.getBody(); + // addDynamicTags( pop.value, eventSnapshot ); + // + // sendEvent( iRequestDecorator, eventSnapshot ); + // } + // catch( Throwable t ) + // { + // LOGGER.debug( "Error processing callback", t ); + // } + // } } -// public static class CallbackHook -// { -// -// private final SnapshotHandler snapshotHandler; -// private final List watchValues; -// private final ITracepoint value; -// private final List frames; -// private Throwable throwable; -// -// -// public CallbackHook( final SnapshotHandler snapshotHandler, -// final List watchValues, -// final ITracepoint value, -// final List frames ) -// { -// this.snapshotHandler = snapshotHandler; -// this.watchValues = watchValues; -// this.value = value; -// this.frames = frames; -// } -// -// -// public CallbackHook() -// { -// this.snapshotHandler = null; -// this.watchValues = null; -// this.value = null; -// this.frames = null; -// } -// -// -// /** -// * This will return {@code true} when there is a live hook for this. -// * -// * @return {@code true} if this is a hook else {@code false}. -// */ -// public boolean isHook() -// { -// return this.snapshotHandler != null; -// } -// -// -// void setThrowable( final Throwable t ) -// { -// this.throwable = t; -// } -// -// -// @Override -// public String toString() -// { -// if( value == null ) -// { -// return "Marker for non callback"; -// } -// return String.format( "%s:%s", value.getRelPath(), value.getLineNo() ); -// } -// } + // public static class CallbackHook + // { + // + // private final SnapshotHandler snapshotHandler; + // private final List watchValues; + // private final ITracepoint value; + // private final List frames; + // private Throwable throwable; + // + // + // public CallbackHook( final SnapshotHandler snapshotHandler, + // final List watchValues, + // final ITracepoint value, + // final List frames ) + // { + // this.snapshotHandler = snapshotHandler; + // this.watchValues = watchValues; + // this.value = value; + // this.frames = frames; + // } + // + // + // public CallbackHook() + // { + // this.snapshotHandler = null; + // this.watchValues = null; + // this.value = null; + // this.frames = null; + // } + // + // + // /** + // * This will return {@code true} when there is a live hook for this. + // * + // * @return {@code true} if this is a hook else {@code false}. + // */ + // public boolean isHook() + // { + // return this.snapshotHandler != null; + // } + // + // + // void setThrowable( final Throwable t ) + // { + // this.throwable = t; + // } + // + // + // @Override + // public String toString() + // { + // if( value == null ) + // { + // return "Marker for non callback"; + // } + // return String.format( "%s:%s", value.getRelPath(), value.getLineNo() ); + // } + // } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameCollector.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameCollector.java index 575f768..5cb5e61 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameCollector.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameCollector.java @@ -17,11 +17,16 @@ package com.intergral.deep.agent.tracepoint.handler; +import com.intergral.deep.agent.Utils; import com.intergral.deep.agent.api.plugin.IEvaluator; import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; import com.intergral.deep.agent.settings.Settings; import com.intergral.deep.agent.tracepoint.handler.bfs.Node; import com.intergral.deep.agent.tracepoint.inst.InstUtils; +import com.intergral.deep.agent.tracepoint.inst.jsp.JSPUtils; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SourceMap; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SourceMapLookup; import com.intergral.deep.agent.types.TracePointConfig; import com.intergral.deep.agent.types.snapshot.StackFrame; import com.intergral.deep.agent.types.snapshot.Variable; @@ -37,24 +42,67 @@ import java.util.Set; import java.util.stream.Collectors; +/** + * This type allows the collection of frame data, ie stack frames, watchers and other tracepoint data. + */ public class FrameCollector extends VariableProcessor { + /** + * The current settings use my deep. + */ protected final Settings settings; + + /** + * The evaluator that should be used by this callback. + */ protected final IEvaluator evaluator; + + /** + * The variables that have been captured by the callback. + */ protected final Map variables; - private final StackTraceElement[] stack; - private final Map varCache = new HashMap<>(); + /** + * The stack trace elements captured by this callback. + */ + private final StackTraceElement[] stack; - public FrameCollector(final Settings settings, final IEvaluator evaluator, + /** + * We cache this value in the constructor to reduce look up later. + */ + private final String jspSuffix; + + /** + * We cache this value in the constructor to reduce look up later. + */ + private final List jspPackages; + + /** + * Create a frame collector to collect the frame data. + * + * @param settings the current settings being used by deep + * @param evaluator the evaluator to use for this callback + * @param variables the variables captured by the callback + * @param stack the stack captured by the callback + */ + public FrameCollector( + final Settings settings, + final IEvaluator evaluator, final Map variables, final StackTraceElement[] stack) { this.settings = settings; this.evaluator = evaluator; this.variables = variables; this.stack = stack; + this.jspSuffix = settings.getSettingAs(ISettings.JSP_SUFFIX, String.class); + this.jspPackages = settings.getAsList(ISettings.JSP_PACKAGES); } + /** + * Processes and collects all the data for the captured frame. + * + * @return the result of the process {@link IFrameResult} + */ protected IFrameResult processFrame() { final ArrayList frames = new ArrayList<>(); @@ -83,14 +131,24 @@ public Map variables() { }; } - private StackFrame processFrame(final StackTraceElement stackTraceElement, + /** + * Process an individual stack frame into a {@link StackFrame} including the variables, if this frame should collect variables. + * + * @param stackTraceElement the stack frame to collect + * @param collectVars {@code true} if we should collect variables + * @param frameIndex the index of the frame we should collect, index {@code 0} indicates the highest frame (e.g. current executing + * frame) + * @return the result of the frame as {@link StackFrame} + */ + private StackFrame processFrame( + final StackTraceElement stackTraceElement, final boolean collectVars, final int frameIndex) { + final String className = stackTraceElement.getClassName(); - final int lineNumber = stackTraceElement.getLineNumber(); - final String fileName = getFileName(stackTraceElement); final boolean nativeMethod = stackTraceElement.isNativeMethod(); final boolean appFrame = isAppFrame(stackTraceElement); + // we use a method to get the method name, so we can override it in CF (as CF uses different method names) final String methodName = getMethodName(stackTraceElement, variables, frameIndex); final Collection varIds; @@ -99,16 +157,72 @@ private StackFrame processFrame(final StackTraceElement stackTraceElement, } else { varIds = Collections.emptyList(); } + // map the file name - this deals with transpiled jsp source files + final FileNameMapping mapping = getFileNameMapping(stackTraceElement, className); - return new StackFrame(fileName, - lineNumber, + return new StackFrame(mapping.fileName, + mapping.lineNumber, className, methodName, appFrame, nativeMethod, - varIds); + varIds, + mapping.transpiledFile, + mapping.transpiledLine); + } + + /** + * We need to be careful when selecting the file name as it is not always available. + *

    + *
  • native frames do not have file names, or line numbers
  • + *
  • JSP classes can be mad up of multiple source files so we need to use the {@link SourceMap}
  • + *
  • it is possible to not include source names when compiling Java classes
  • + *
+ * + * @param stackTraceElement the stack frame element + * @param className the classname + * @return the mapped file name and line numbers + */ + private FileNameMapping getFileNameMapping(final StackTraceElement stackTraceElement, final String className) { + + if (!JSPUtils.isJspClass(jspSuffix, jspPackages, className)) { + return new FileNameMapping(getFileName(stackTraceElement), stackTraceElement.getLineNumber(), null, -1); + } + + Class forName = null; + try { + forName = Class.forName(className); + } catch (ClassNotFoundException ignored) { + // not possible + // we are literally executing the code, so the class has to be loaded + // in theory this could happen with some complex class loaders, but it shouldn't + // if it does there is nothing we can do anyway + } + + if (forName == null) { + return new FileNameMapping(getFileName(stackTraceElement), stackTraceElement.getLineNumber(), null, -1); + } + + final SourceMap sourceMap = JSPUtils.getSourceMap(forName); + if (sourceMap == null) { + return new FileNameMapping(getFileName(stackTraceElement), stackTraceElement.getLineNumber(), null, -1); + } + + final SourceMapLookup lookup = sourceMap.lookup(stackTraceElement.getLineNumber()); + final String jspFilename = lookup.getFilename(); + final int jspLine = lookup.getLineNumber(); + final String transpiledFile = getFileName(stackTraceElement); + final int transpiledLine = stackTraceElement.getLineNumber(); + return new FileNameMapping(jspFilename, jspLine, transpiledFile, transpiledLine); } + /** + * Select from the available captured variables the variables we want to process for this frame. This is mainly here to allow for an easy + * way for CF to map the variables from their capture Java types to the expected CF types. + * + * @param frameIndex the index of the frame we are processing + * @return the variables available at this frame + */ protected Map selectVariables(final int frameIndex) { if (frameIndex == 0) { return this.variables; @@ -117,6 +231,15 @@ protected Map selectVariables(final int frameIndex) { return Collections.emptyMap(); } + /** + * This is where we start the Breadth first search (BFS) of the selected variables. + *

+ * Here we are essentially dealing with the BFS nodes and linking to the VariableProcessor to do the processing. + * + * @param variables the variables to process + * @return the variable ref used in the snapshot + * @see VariableProcessor + */ protected List processVars(final Map variables) { final List frameVars = new ArrayList<>(); @@ -133,6 +256,14 @@ protected List processVars(final Map variables) { return frameVars; } + /** + * This is where we take a node from BFS queue and process it back onto the queue. + *

+ * Essentially, we take a node, we process this node, the add the child nodes back onto the BFS queue. + * + * @param node the node to process + * @return {@code true} if we should continue to process more nodes, else {@code false}. + */ protected boolean processNode(final Node node) { if (!this.checkVarCount()) { // we have exceeded the var count, so do not continue @@ -160,26 +291,17 @@ protected boolean processNode(final Node node) { return true; } - private boolean checkVarCount() { - return varCache.size() <= this.frameConfig.maxVariables(); - } - - @Override - protected String checkId(final String identity) { - return this.varCache.get(identity); - } - - @Override - protected String newVarId(final String identity) { - final int size = this.varCache.size(); - final String newId = String.valueOf(size + 1); - this.varCache.put(identity, newId); - return newId; - } - + /** + * An app frame is defined via the settings {@link ISettings#APP_FRAMES_INCLUDES} and {@link ISettings#APP_FRAMES_EXCLUDES}. This gives a + * way to tell deep that the frame is part of your app and not part of the framework. This is primarily used as a way to filter frames in + * the UI. + * + * @param stackTraceElement the stack frame to process + * @return {@code true} if the class name is in the included packages, and not in the excluded packages, else {@code false}. + */ protected boolean isAppFrame(final StackTraceElement stackTraceElement) { - final List inAppInclude = settings.getAsList("in.app.include"); - final List inAppExclude = settings.getAsList("in.app.exclude"); + final List inAppInclude = settings.getAsList(ISettings.APP_FRAMES_INCLUDES); + final List inAppExclude = settings.getAsList(ISettings.APP_FRAMES_EXCLUDES); final String className = stackTraceElement.getClassName(); @@ -197,19 +319,45 @@ protected boolean isAppFrame(final StackTraceElement stackTraceElement) { return false; } + /** + * Get the file name from the stack trace element, as we always need a file name. If there is not a file name then use the short class + * name. + * + * @param stackTraceElement the frame to process + * @return the file name, or class name + */ private String getFileName(final StackTraceElement stackTraceElement) { if (stackTraceElement.getFileName() == null) { return InstUtils.shortClassName(stackTraceElement.getClassName()); } - return stackTraceElement.getFileName(); + return Utils.trimPrefix(stackTraceElement.getFileName(), "/"); } + /** + * Get the method name from the stack frame. + * + * @param stackTraceElement the stack frame to process + * @param variables the variables for the frame + * @param frameIndex the frame index + * @return the name of the method + */ protected String getMethodName(final StackTraceElement stackTraceElement, final Map variables, final int frameIndex) { return stackTraceElement.getMethodName(); } + /** + * Evaluate a watch expression into a {@link IExpressionResult}. + *

+ * We always need a result from a watch expression, however, it is possible to have bad watches that error. In some cases it is possible + * to not have a valid evaluator. + *

+ * So if we cannot get a result from the {@link IEvaluator} then we return an error result. + * + * @param watch the watch expression to evaluate + * @return a {@link IExpressionResult} + */ protected IExpressionResult evaluateWatchExpression(final String watch) { try { final Object result = this.evaluator.evaluateExpression(watch, this.variables); @@ -242,6 +390,23 @@ public Map variables() { } } + /** + * Using the current tracepoint config, create a {@link Resource} the can be used as the attributes. + *

+ * The basic attributes created are: + *

    + *
  • tracepoint - the id of the tracepoint
  • + *
  • path - the path of the tracepoint
  • + *
  • line - the line of the tracepoint
  • + *
  • stack - the stack type of the tracepoint
  • + *
  • frame - the frame type of the tracepoint
  • + *
  • has_watchers - boolean indicating there are watchers
  • + *
  • has_condition - boolean indicating there is a condition
  • + *
+ * + * @param tracepoint the current config + * @return the new {@link Resource} + */ protected Resource processAttributes(final TracePointConfig tracepoint) { final HashMap attributes = new HashMap<>(); attributes.put("tracepoint", tracepoint.getId()); @@ -261,6 +426,11 @@ protected Resource processAttributes(final TracePointConfig tracepoint) { return Resource.create(attributes); } + /** + * The result of processing the frames. + * + * @see #processFrame() + */ protected interface IFrameResult { Collection frames(); @@ -268,10 +438,56 @@ protected interface IFrameResult { Map variables(); } + /** + * The result of evaluating an expression. + * + * @see #evaluateWatchExpression(String) + */ protected interface IExpressionResult { WatchResult result(); Map variables(); } + + /** + * A small type to wrap the mapped file and line numbers into an easy return. + */ + private static class FileNameMapping { + + /** + * Should always have a value. Will be the source file name if available, or the class name if not. + *

+ * If the class being processed is a transpiled type, e.g. JSP then this will be the original source file. + */ + private final String fileName; + /** + * Can be negative. + *

+ * If the class being processed is a transpiled type, e.g. JSP then this will be the original source file line number. + * + * @see StackTraceElement#getLineNumber() + */ + private final int lineNumber; + + /** + * If the class being processed is a transpiled type, e.g. JSP then this will be the filename before it is mapped. + */ + private final String transpiledFile; + /** + * Can be negative. + *

+ * If the class being processed is a transpiled type, e.g. JSP then this will be the linenumber before it is mapped. + * + * @see StackTraceElement#getLineNumber() + */ + private final int transpiledLine; + + public FileNameMapping(final String fileName, final int lineNumber, final String transpiledFile, final int transpiledLine) { + this.fileName = fileName; + this.lineNumber = lineNumber; + this.transpiledFile = transpiledFile; + this.transpiledLine = transpiledLine; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameConfig.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameConfig.java index 966af39..c3bc06d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameConfig.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameConfig.java @@ -19,6 +19,10 @@ import com.intergral.deep.agent.types.TracePointConfig; +/** + * This config holds the general config to use when processing a callback. The config is defined from all the tracepoints that are trigger + * on a line. + */ public class FrameConfig { private static final int DEFAULT_MAX_VAR_DEPTH = 5; @@ -37,6 +41,13 @@ public class FrameConfig { private int maxWatchVars = -1; private int maxTpProcessTime = -1; + private boolean cfRaw = false; + + /** + * Process a tracepoint into the config. + * + * @param tracePointConfig the tracepoint to process + */ public void process(final TracePointConfig tracePointConfig) { maxVarDepth = Math.max(tracePointConfig.getArg("MAX_VAR_DEPTH", Integer.class, -1), maxVarDepth); @@ -69,8 +80,16 @@ public void process(final TracePointConfig tracePointConfig) { this.stackType = stackType; } } + + final Boolean cfRaw = tracePointConfig.getArg("cf.raw", Boolean.class, null); + if (cfRaw != null && cfRaw) { + this.cfRaw = true; + } } + /** + * When we have finished processing all the tracepoints we {@code close} the config to set the final config for this callback. + */ public void close() { maxVarDepth = maxVarDepth == -1 ? DEFAULT_MAX_VAR_DEPTH : maxVarDepth; maxVariables = maxVariables == -1 ? DEFAULT_MAX_VARIABLES : maxVariables; @@ -83,6 +102,12 @@ public void close() { stackType = stackType == null ? TracePointConfig.STACK : stackType; } + /** + * Using the {@link #frameType} should we collect the variables on this frame. + * + * @param currentFrameIndex the current frame index. + * @return {@code true} if we should collect the variables. + */ public boolean shouldCollectVars(final int currentFrameIndex) { if (this.frameType.equals(TracePointConfig.NO_FRAME_TYPE)) { return false; @@ -95,19 +120,49 @@ public boolean shouldCollectVars(final int currentFrameIndex) { return this.frameType.equals(TracePointConfig.ALL_FRAME_TYPE); } + /** + * The max number of variables this callback should collect. + * + * @return the max variables + */ public int maxVariables() { return this.maxVariables; } + /** + * The max length of any string. + * + * @return the max string length + */ public int maxStringLength() { return this.maxStrLength; } + /** + * The max depth of variables to collect. + * + * @return the max variable depth + */ public int maxDepth() { return this.maxVarDepth; } + /** + * The max number of items in a collection we should collect. + * + * @return the max collection size + */ public int maxCollectionSize() { return this.maxCollectionSize; } + + /** + * Is this frame set to cf raw. + * cf raw allows the collection of the java variables instead of the mapped cf variables. + * + * @return {@code true} if we want to collect the raw cf variables. + */ + public boolean isCfRaw() { + return this.cfRaw; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessor.java index d9636c6..a36e697 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessor.java @@ -30,13 +30,37 @@ import java.util.Map; import java.util.stream.Collectors; +/** + * This type deals with matching tracepoints to the current state and working out if we can collect the data. + */ public class FrameProcessor extends FrameCollector implements ISnapshotContext { + /** + * The tracepoints that have been triggered by the Callback. + */ private final Collection tracePointConfigs; + /** + * The time as a tuple when this line Callback started. + * + * @see Utils#currentTimeNanos() + */ private final long[] lineStart; + + /** + * These are the tracepoints that are filtered based on state and conditions to be valid to trigger collection. + */ private Collection filteredTracepoints; - private TracePointConfig currentTracePointConfig; + /** + * Create a new processor for this Callback. + * + * @param settings the current settings being used + * @param evaluator the evaluator to use for watchers and conditions + * @param variables the variables we have at this state + * @param tracePointConfigs the tracepoints that are part of this Callback + * @param lineStart the Tuple of the time this Callback started + * @param stack the stack trace to use + */ public FrameProcessor(final Settings settings, final IEvaluator evaluator, final Map variables, @@ -47,15 +71,30 @@ public FrameProcessor(final Settings settings, this.lineStart = lineStart; } + /** + * Using the {@link #tracePointConfigs} can we collect any data at this point. + *

+ * This method will check the tracepoint config fire count, rate limits, windows and conditions and populate the + * {@link #filteredTracepoints} + * + * @return {@code true}, if the {@link #filteredTracepoints} have any values + */ public boolean canCollect() { this.filteredTracepoints = this.tracePointConfigs.stream() .filter(tracePointConfig -> tracePointConfig.canFire(this.lineStart[0]) && this.conditionPasses(tracePointConfig)) .collect(Collectors.toList()); - return this.filteredTracepoints.size() != 0; + return !this.filteredTracepoints.isEmpty(); } + /** + * Process the tracepoints condition with the evaluator to see if the condition is {@code true}. + * + * @param tracePointConfig the config to check + * @return {@code false} if the condition on the tracepoint evaluates to a false + * @see IEvaluator#evaluate(String, Map) + */ private boolean conditionPasses(final TracePointConfig tracePointConfig) { final String condition = tracePointConfig.getCondition(); if (condition == null || condition.trim().isEmpty()) { @@ -65,20 +104,27 @@ private boolean conditionPasses(final TracePointConfig tracePointConfig) { return this.evaluator.evaluate(condition, variables); } + /** + * Using the {@link #filteredTracepoints} update the config to reflect the collection config for this Callback. + *

+ * If there are multiple tracepoints being process, the config will reflect the most inclusive, ie the higher number. + */ public void configureSelf() { - for (TracePointConfig tracePointConfig : this.filteredTracepoints) { - this.frameConfig.process(tracePointConfig); - } - this.frameConfig.close(); + configureSelf(this.filteredTracepoints); } + /** + * Collect the data into {@link EventSnapshot}. + * + * @return the collected {@link EventSnapshot} + */ public Collection collect() { final Collection snapshots = new ArrayList<>(); final IFrameResult processedFrame = super.processFrame(); for (final TracePointConfig tracepoint : filteredTracepoints) { - try (AutoCloseable ignored = withTracepoint(tracepoint)) { + try { final EventSnapshot snapshot = new EventSnapshot(tracepoint, this.lineStart[1], this.settings.getResource(), @@ -103,18 +149,9 @@ public Collection collect() { return snapshots; } - private AutoCloseable withTracepoint(final TracePointConfig tracepoint) { - this.currentTracePointConfig = tracepoint; - return () -> currentTracePointConfig = null; - } - - protected T getTracepointConfig(final String key, final Class clazz, T def) { - if (currentTracePointConfig == null) { - return def; - } - return currentTracePointConfig.getArg(key, clazz, def); - } - + /** + * {@inheritDoc} + */ @Override public String evaluateExpression(final String expression) throws EvaluationException { try { @@ -125,8 +162,24 @@ public String evaluateExpression(final String expression) throws EvaluationExcep } } + /** + * This defines a functional interface to allow for creating difference processors in the Callback. + */ public interface IFactory { + /** + * Create a new processor. + * + * @param settings the current settings being used + * @param evaluator the evaluator to use for watchers and conditions + * @param variables the variables we have at this state + * @param tracePointConfigs the tracepoints that are part of this Callback + * @param lineStart the Tuple of the time this Callback started + * @param stack the stack trace to use + * @return the new {@link FrameProcessor} + * @see FrameProcessor + * @see com.intergral.deep.agent.tracepoint.cf.CFFrameProcessor + */ FrameProcessor provide(Settings settings, IEvaluator evaluator, Map variables, Collection tracePointConfigs, long[] lineStart, StackTraceElement[] stack); diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessor.java index ab95aa6..2ed6d08 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessor.java @@ -22,8 +22,12 @@ import com.intergral.deep.agent.api.utils.ArrayObjectIterator; import com.intergral.deep.agent.api.utils.CompoundIterator; import com.intergral.deep.agent.tracepoint.handler.bfs.Node; +import com.intergral.deep.agent.tracepoint.handler.bfs.Node.IParent; +import com.intergral.deep.agent.tracepoint.inst.InstUtils; +import com.intergral.deep.agent.types.TracePointConfig; import com.intergral.deep.agent.types.snapshot.Variable; import com.intergral.deep.agent.types.snapshot.VariableID; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.Collection; import java.util.Collections; @@ -33,8 +37,47 @@ import java.util.Map; import java.util.Set; +/** + * This type deals with processing the variables. Dealing with: + *

    + *
  • type definition - how the type of the variable is captured
  • + *
  • string values - how the string representation of the variable value is captured
  • + *
  • child variables - how many child variables are processed
  • + *
  • variable limits - total limits on how many variables are processed
  • + *
  • depth limits - how deep will process down the reference chain
  • + *
  • variable deduplication - ensuring we do not process the same variable multiple times
  • + *
  • variable referencing - using variable ids to reference already processed variables
  • + *
+ *

+ * While processing a variable or frame, we will process using a Breadth first approach. These means given the tree: + *

+ *   1 -> 1.1
+ *        1.2
+ *        1.3 -> 1.3.1
+ *   2 -> 2.1
+ *   3 -> 3.1 -> 3.1.1
+ * 
+ * We will attempt to gather the variables in the order: + *
    + *
  • 1
  • + *
  • 2
  • + *
  • 3
  • + *
  • 1.1
  • + *
  • 1.2
  • + *
  • 1.3
  • + *
  • 2.1
  • + *
  • 3.1
  • + *
  • 1.3.1
  • + *
  • 3.1.1
  • + *
+ * This ensures that we capture the variables closer to the tracepoint before we go deeper. + */ public abstract class VariableProcessor { + /** + * These are teh class names of types that we do not want to collect child variables from. These are mostly primitive or object types that + * the string value will provide the full value of, or where child values do not add value. + */ private static final Set> NO_CHILDREN_TYPES = new HashSet<>(); static { @@ -60,8 +103,22 @@ public abstract class VariableProcessor { NO_CHILDREN_TYPES.add(Iterator.class); } + /** + * Some config values from the triggered tracepoints affect all tracepoints at the point of collection. This {@link FrameConfig} + * calculates the most encompassing config for all triggered tracepoints. + */ protected final FrameConfig frameConfig = new FrameConfig(); - private Map varLookup = new HashMap<>(); + /** + * This is the cache we use while building this lookup, this cache essentially maps {@link System#identityHashCode(Object)} to an internal + * id (monotonically incrementing id) used to deduplicate the variables. Essentially allows us to map the same object via different + * references, meaning if we are processing a type that references its self, or has multiple paths to the same object we will be process + * it again. + */ + private final Map varCache = new HashMap<>(); + /** + * This is the lookup that we are building from this processor. It will contain the deduplicated variables by reference. + */ + Map varLookup = new HashMap<>(); //todo i do not like this approach to getting an empty var look up for watches protected Map closeLookup() { @@ -70,6 +127,18 @@ protected Map closeLookup() { return lookup; } + /** + * We need to configure the {@link #frameConfig} based on the tracepoints we are capturing. + * + * @param configs the tracepoints to configure with + */ + public void configureSelf(final Iterable configs) { + for (TracePointConfig tracePointConfig : configs) { + this.frameConfig.process(tracePointConfig); + } + this.frameConfig.close(); + } + protected Set processChildNodes(final VariableID variableId, final Object value, final int depth) { // if the type is a type we do not want children from - return empty @@ -83,7 +152,18 @@ protected Set processChildNodes(final VariableID variableId, final Object return Collections.emptySet(); } - final Node.IParent parent = (node) -> this.appendChild(variableId.getId(), node); + final Node.IParent parent = new IParent() { + @Override + public void addChild(final VariableID child) { + VariableProcessor.this.appendChild(variableId.getId(), child); + } + + @Override + public boolean isCollection() { + return VariableProcessor.this.collectionType(value) || VariableProcessor.this.mapType(value) || VariableProcessor.this.arrayType( + value); + } + }; return findChildrenForParent(parent, value); } @@ -93,24 +173,36 @@ private Set findChildrenForParent(final Node.IParent parent, final Object return processNamedIterable(parent, new NamedIterable<>(new ArrayObjectIterator(value))); } if (collectionType(value)) { + //noinspection unchecked return processNamedIterable(parent, new NamedIterable<>(((Collection) value).iterator())); } if (mapType(value)) { + //noinspection unchecked return processNamedIterable(parent, new NamedMapIterator(((Map) value).entrySet().iterator())); } return processNamedIterable(parent, - new NamesFieldIterator(value, new FieldIterator(value.getClass()))); + new NamedFieldIterator(value, new FieldIterator(value.getClass()))); } + /** + * To allow for a common way to process variables from the multiple ways we can gather variables. We introduce the {@link NamedIterable}, + * which allows as to iterate maps, arrays and class fields. + *

+ * This method lets us convert the iterable of names items into {@link Node} for our Breadth First search algorithm. + * + * @param parent the parent node these values belong to + * @param iterable the iterable to process + * @return the new set of nodes to process + */ private Set processNamedIterable(final Node.IParent parent, - final NamedIterable objectNamedIterable) { + final NamedIterable iterable) { final HashSet nodes = new HashSet<>(); - while (objectNamedIterable.hasNext()) { - final NamedIterable.INamedItem next = objectNamedIterable.next(); + while (iterable.hasNext()) { + final NamedIterable.INamedItem next = iterable.next(); final Node node = new Node(new Node.NodeValue(next.name(), next.item(), @@ -118,7 +210,7 @@ private Set processNamedIterable(final Node.IParent parent, next.modifiers()), parent); nodes.add(node); - if (nodes.size() > this.frameConfig.maxCollectionSize()) { + if (parent.isCollection() && nodes.size() > this.frameConfig.maxCollectionSize()) { break; } } @@ -126,19 +218,50 @@ private Set processNamedIterable(final Node.IParent parent, return nodes; } + /** + * Is the passed value a type of map. + * + * @param value the object to check + * @return {@code true} if the object is a {@link Map} type, else {@code false} + */ private boolean mapType(final Object value) { return value instanceof Map; } + /** + * Is the object a type of array. + * + * @param value the object to check + * @return {@code true} if the object is a form of array, else {@code false} + * @see Class#isArray() + */ private boolean arrayType(final Object value) { return value != null && value.getClass().isArray(); } + /** + * Is the object a type of collection, not including array. + * + * @param value the object to check + * @return {@code true} if the object is a {@link Collection}, else {@code false}. + */ private boolean collectionType(final Object value) { return value instanceof Collection; } + /** + * Process the given node into a {@link VariableResponse}. + *

+ * If the provided value has already been processed, ie the {@link System#identityHashCode(Object)} of the object is already in the + * {@link #varCache} then we do not process the variable, and simply return a pointer to the reference. + *

+ * If the value has not already been process then we must gather the type, value, children etc. Then process the data into the + * {@link #varLookup}. + * + * @param value the node value to process + * @return the {@link VariableResponse} + */ protected VariableResponse processVariable(final Node.NodeValue value) { final Object objValue = value.getValue(); // get the variable hash id @@ -153,7 +276,7 @@ protected VariableResponse processVariable(final Node.NodeValue value) { if (cacheId != null) { return new VariableResponse(new VariableID(cacheId, - value.getKey(), + value.getName(), value.getModifiers(), value.getOriginalName()), false); } @@ -161,7 +284,7 @@ protected VariableResponse processVariable(final Node.NodeValue value) { // if we do not have a cache_id - then create one final String varId = newVarId(identityCode); final VariableID variableId = new VariableID(varId, - value.getKey(), + value.getName(), value.getModifiers(), value.getOriginalName()); @@ -172,7 +295,7 @@ protected VariableResponse processVariable(final Node.NodeValue value) { varType = objValue.getClass().getName(); } - final Utils.ITrimResult iTrimResult = Utils.trim(this.valueToString(objValue), + final Utils.ITrimResult iTrimResult = Utils.truncate(this.valueToString(objValue), this.frameConfig.maxStringLength()); final Variable variable = new Variable(varType, iTrimResult.value(), identityCode, @@ -183,14 +306,26 @@ protected VariableResponse processVariable(final Node.NodeValue value) { return new VariableResponse(variableId, true); } + /** + * Create a string representation of the value. + *

+ * For most objects this is simply the result of {@link Object#toString()}, however for {@link Collection}, {@link Map}, {@link Iterator} + * and arrays, we create a simplified value as we collect the collection items as children. + * + * @param value the value to stringify + * @return a {@link String} that represents the value + */ private String valueToString(final Object value) { - if (value instanceof Collection) { - return String.format("Collection of size: %s", ((Collection) value).size()); + if (collectionType(value)) { + return String.format("%s of size: %s", InstUtils.shortClassName(value.getClass().getName()), ((Collection) value).size()); } else if (value instanceof Iterator) { - return String.format("Iterator of type: %s", value.getClass().getSimpleName()); - } else if (value instanceof Map) { - return String.format("Map of size: %s", ((Map) value).size()); + return String.format("Iterator of type: %s", InstUtils.shortClassName(value.getClass().getName())); + } else if (mapType(value)) { + return String.format("%s of size: %s", InstUtils.shortClassName(value.getClass().getName()), ((Map) value).size()); + } else if (arrayType(value)) { + return String.format("Array of length: %s", Array.getLength(value)); } + // we must use utils to create the string as it is possible for toString to fail return Utils.valueOf(value); } @@ -207,10 +342,24 @@ protected boolean checkDepth(final int depth) { return depth + 1 < this.frameConfig.maxDepth(); } - protected abstract String checkId(final String identity); + protected boolean checkVarCount() { + return varCache.size() <= this.frameConfig.maxVariables(); + } - protected abstract String newVarId(final String identity); + protected String checkId(final String identity) { + return this.varCache.get(identity); + } + protected String newVarId(final String identity) { + final int size = this.varCache.size(); + final String newId = String.valueOf(size + 1); + this.varCache.put(identity, newId); + return newId; + } + + /** + * This type is essentially a way to return the {@link VariableID} and to indicate if we need to process the children of this variable. + */ protected static class VariableResponse { private final VariableID variableId; @@ -230,6 +379,11 @@ public boolean processChildren() { } } + /** + * This is the basic named iterator that wraps iterator items with a name, e.g. the field name, collection index, or map key + * + * @param the type of value we are iterating + */ private static class NamedIterable implements Iterator { @@ -272,6 +426,9 @@ public Set modifiers() { }; } + /** + * The interface of an item named by the named iterator. + */ public interface INamedItem { String name(); @@ -284,6 +441,9 @@ public interface INamedItem { } } + /** + * Use the named iterator to name map values with the map key. + */ private static class NamedMapIterator extends NamedIterable> { public NamedMapIterator(final Iterator> iterator) { @@ -317,11 +477,15 @@ public Set modifiers() { } } - private static class NamesFieldIterator extends NamedIterable { + /** + * Use the named iterator to name the items with the field names. + */ + private static class NamedFieldIterator extends NamedIterable { private final Object target; + private final HashSet seenNames = new HashSet<>(); - public NamesFieldIterator(final Object target, final Iterator iterator) { + public NamedFieldIterator(final Object target, final Iterator iterator) { super(iterator); this.target = target; } @@ -329,10 +493,12 @@ public NamesFieldIterator(final Object target, final Iterator iterator) { @Override public INamedItem next() { final Field next = this.iterator.next(); + final String name = getFieldName(next); + seenNames.add(next.getName()); return new INamedItem() { @Override public String name() { - return next.getName(); + return name; } @Override @@ -342,7 +508,11 @@ public Object item() { @Override public String originalName() { - return null; + // if we prefix the name with class name, then set the original name to the field name + if (name.equals(next.getName())) { + return null; + } + return next.getName(); } @Override @@ -351,8 +521,28 @@ public Set modifiers() { } }; } + + private String getFieldName(final Field next) { + final String name; + // it is possible to have a field that is declared as a private value on a super type. + // here we will prefix the declaring class name of the field if we have already seen the simple name + if (seenNames.contains(next.getName())) { + if (next.getDeclaringClass() == this.target.getClass()) { + name = next.getName(); + } else { + name = String.format("%s.%s", next.getDeclaringClass().getSimpleName(), next.getName()); + } + } else { + name = next.getName(); + } + return name; + } } + /** + * A simple iterator that used {@link ReflectionUtils} to create iterators for the {@link Field} that exist on an object. This allows us + * to feed the fields into the {@link NamedFieldIterator}. + */ private static class FieldIterator implements Iterator { private final Class clazz; diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/bfs/Node.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/bfs/Node.java index 11753ca..8d99a34 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/bfs/Node.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/bfs/Node.java @@ -24,6 +24,9 @@ import java.util.List; import java.util.Set; +/** + * A node is a value to process in the BFS. It also links children to parents to ensure hierarchy in variables. + */ public class Node { final NodeValue value; @@ -32,17 +35,36 @@ public class Node { int depth = 0; + /** + * Create a new node for the BFS. + * + * @param value the value to wrap + * @param parent the parent of this node + */ public Node(final NodeValue value, final IParent parent) { this(value, new HashSet<>(), parent); } + /** + * Create a new node for the BFS. + * + * @param value the value to wrap + * @param children the children of this node + * @param parent the parent of this node + */ public Node(final NodeValue value, final Set children, final IParent parent) { this.value = value; this.parent = parent; this.children = children; } + /** + * Start the breadth first search of the nodes. + * + * @param root the root node to start from + * @param consumer the consumer to use to collect more nodes. + */ public static void breadthFirstSearch(final Node root, final IConsumer consumer) { final List queue = new LinkedList<>(); @@ -60,6 +82,11 @@ public static void breadthFirstSearch(final Node root, final IConsumer consumer) } } + /** + * Add child nodes. + * + * @param children the children to add + */ public void addChildren(final Set children) { for (Node child : children) { child.depth = this.depth + 1; @@ -82,21 +109,34 @@ public int depth() { } + /** + * The parent of a processed node. + */ public interface IParent { void addChild(final VariableID child); + + default boolean isCollection() { + return false; + } } + /** + * The consumer of the nodes when running a BFS. + */ public interface IConsumer { boolean processNode(Node node); } + /** + * This type wraps an Object that we are to process. They simply acts as a reference to the read data during the Breadth First search. + */ public static class NodeValue { - private final String key; + private final String name; private final Object value; private final String originalName; private final Set modifiers; @@ -105,16 +145,24 @@ public NodeValue(final String key, final Object value) { this(key, value, null, Collections.emptySet()); } - public NodeValue(final String key, final Object value, final String originalName, + /** + * Create a new node value. + * + * @param name the name of the value + * @param value the value to wrap + * @param originalName the original name of the value + * @param modifiers the value modifiers + */ + public NodeValue(final String name, final Object value, final String originalName, final Set modifiers) { - this.key = key; + this.name = name; this.value = value; this.originalName = originalName; this.modifiers = modifiers; } - public String getKey() { - return key; + public String getName() { + return name; } public Object getValue() { diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CFClassScanner.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CFClassScanner.java index ca20e67..df35782 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CFClassScanner.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CFClassScanner.java @@ -26,31 +26,38 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Scans the classes for CF classes we want to modify. + */ public class CFClassScanner implements IClassScanner { private static final Logger LOGGER = LoggerFactory.getLogger(CFClassScanner.class); - private final Map removedBreakpoints; + protected final Map tracePointConfigMap; - public CFClassScanner(final Map removedBreakpoints) { - this.removedBreakpoints = removedBreakpoints; + public CFClassScanner(final Map tracePointConfigMap) { + this.tracePointConfigMap = tracePointConfigMap; } @Override public boolean scanClass(final Class loadedClass) { - if (removedBreakpoints.isEmpty()) { + // if the map is empty we have already found all our tracepoints + if (tracePointConfigMap.isEmpty()) { // stop as soon as we run out of classes return false; } + // if this is a CF class then process it if (CFUtils.isCfClass(loadedClass.getName())) { try { - final Set breakpoints = loadCfBreakpoints(loadedClass, - removedBreakpoints); + // try to load CF tracepoint - we try-catch as sometimes cfm won't give us a location we can use + final Set breakpoints = loadCfTracepoint(loadedClass, tracePointConfigMap); + // if we found some tracepoints if (!breakpoints.isEmpty()) { + // remove them from our config so we can end fast for (TracePointConfig breakpoint : breakpoints) { - removedBreakpoints.remove(breakpoint.getId()); + tracePointConfigMap.remove(breakpoint.getId()); } return true; } @@ -63,15 +70,29 @@ public boolean scanClass(final Class loadedClass) { } - private Set loadCfBreakpoints(final Class loadedClass, - final Map values) { + /** + * We need to convert the class name from CF which is normally some hashed version of the file path e.g. cftestList2ecfm1060358347, into a + * file path to source. + * + * @param loadedClass the class we are processing + * @param values the current tracepoint configs we are looking to match + * @return the matched tracepoints + */ + private Set loadCfTracepoint( + final Class loadedClass, + final Map values + ) { final URL location = getLocation(loadedClass); + // Lucee will not give us the code location using protection domain if (location == null) { - return TracepointInstrumentationService.loadCfBreakpoints( + // if we cannot get the code source then we guess the source using the provided path name + return CFUtils.loadCfTracepoints( CFUtils.guessSource(loadedClass.getName()), values); } - return TracepointInstrumentationService.loadCfBreakpoints(location, values); + // Adobe CF should provide the location to the source, so we can use that. + // todo it is possible to run precompiled CF code which would possibly have a different source location + return CFUtils.loadCfTracepoints(location, values); } @@ -83,4 +104,9 @@ URL getLocation(final Class loadedClass) { URL getLocation(final ProtectionDomain protectionDomain) { return protectionDomain.getCodeSource().getLocation(); } + + @Override + public boolean isComplete() { + return tracePointConfigMap.isEmpty(); + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CompositeClassScanner.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CompositeClassScanner.java index 9895321..efbde89 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CompositeClassScanner.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/CompositeClassScanner.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.Set; +/** + * A collection of scanners to run to collect classes to modify. + */ public class CompositeClassScanner implements IClassScanner { private final Set scanner = new HashSet<>(); @@ -46,6 +49,12 @@ public boolean scanClass(final Class clazz) { } + /** + * Scan the loaded classes for classes we should modify. + * + * @param inst the instrumentation + * @return an array of classes to modify + */ public Class[] scanAll(final Instrumentation inst) { final List> classes = new ArrayList<>(); final Class[] allLoadedClasses = inst.getAllLoadedClasses(); @@ -55,7 +64,21 @@ public Class[] scanAll(final Instrumentation inst) { && scanClass(allLoadedClass)) { classes.add(allLoadedClass); } + // stop iteration as soon as scanners are complete + if (isComplete()) { + break; + } } return classes.toArray(new Class[0]); } + + @Override + public boolean isComplete() { + for (IClassScanner classScanner : scanner) { + if (!classScanner.isComplete()) { + return false; + } + } + return true; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/IClassScanner.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/IClassScanner.java index db9a213..394b39a 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/IClassScanner.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/IClassScanner.java @@ -17,7 +17,23 @@ package com.intergral.deep.agent.tracepoint.inst; +/** + * Used to define a method to scan the loaded classes. + */ public interface IClassScanner { + /** + * Scan this class. + * + * @param clazz the class to sacn + * @return {@code true} if we should include this class + */ boolean scanClass(final Class clazz); + + /** + * Is this class scanner complete. + * + * @return {@code true} if this scanner has nothing more to find + */ + boolean isComplete(); } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/InstUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/InstUtils.java index 33321ea..ea1f551 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/InstUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/InstUtils.java @@ -17,13 +17,31 @@ package com.intergral.deep.agent.tracepoint.inst; -public class InstUtils { +/** + * Utilities to help with instrumentation. + */ +public final class InstUtils { + + private InstUtils() { + } + /** + * Get the external class name. + * + * @param className the class name + * @return the external class name + */ public static String externalClassName(final String className) { return className.replaceAll("/", "."); } + /** + * Get the name of the file from the path. + * + * @param relPath the path to a file + * @return the name of the file + */ public static String fileName(final String relPath) { final int lastIndexOf = relPath.lastIndexOf('/'); if (lastIndexOf == -1) { @@ -33,27 +51,59 @@ public static String fileName(final String relPath) { } - public static String internalClassStripInner(final Class allLoadedClass) { - return internalClassStripInner(allLoadedClass.getName()); + /** + * Convert the class name to the internal class name, remove any inner class names. + * + * @param clazz the class + * @return the internal class name without the inner classes + */ + public static String internalClassStripInner(final Class clazz) { + return internalClassStripInner(clazz.getName()); } - public static String internalClassStripInner(final String allLoadedClass) { - final int index = allLoadedClass.indexOf('$'); + /** + * Convert the class name to the internal class name, remove any inner class names. + * + * @param className the name of the class + * @return the internal class name without the inner classes + */ + public static String internalClassStripInner(final String className) { + final int index = className.indexOf('$'); if (index == -1) { - return internalClass(allLoadedClass); + return internalClass(className); } - return internalClass(allLoadedClass.substring(0, index)); + return internalClass(className.substring(0, index)); } + /** + * Get the internal class name. + * + * @param clazz the name of the class + * @return the internal name of the class + */ public static String internalClass(final String clazz) { return clazz.replaceAll("\\.", "/"); } + /** + * Get the internal class name. + * + * @param clazz the class + * @return the internal name of the class + */ public static String internalClass(final Class clazz) { return internalClass(clazz.getName()); } + /** + * Get the short version of the class name. + *

+ * Sometimes {@link Class#getSimpleName()} doesn't return a name. So we need one that always returns a name. + * + * @param className the class name + * @return the name of the class without the package + */ public static String shortClassName(final String className) { return className.substring(className.lastIndexOf('.') + 1); } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/JSPClassScanner.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/JSPClassScanner.java index 0a1174b..1425dae 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/JSPClassScanner.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/JSPClassScanner.java @@ -18,8 +18,6 @@ package com.intergral.deep.agent.tracepoint.inst; -import static com.intergral.deep.agent.tracepoint.inst.TracepointInstrumentationService.loadJspBreakpoints; - import com.intergral.deep.agent.tracepoint.inst.jsp.JSPUtils; import com.intergral.deep.agent.types.TracePointConfig; import java.util.List; @@ -27,17 +25,27 @@ import java.util.Set; +/** + * This scanner is meant to find JSP classes that have tracepoints. + */ public class JSPClassScanner implements IClassScanner { - private final Map removedBreakpoints; + private final Map tracePointConfigMap; private final String jspSuffix; private final List jspPackages; - public JSPClassScanner(final Map removedBreakpoints, + /** + * Create a new JSP scanner. + * + * @param tracepoints the tracepoints + * @param jspSuffix the jsp suffix + * @param jspPackages the jsp packages + */ + public JSPClassScanner(final Map tracepoints, final String jspSuffix, final List jspPackages) { - this.removedBreakpoints = removedBreakpoints; + this.tracePointConfigMap = tracepoints; this.jspSuffix = jspSuffix; this.jspPackages = jspPackages; } @@ -45,19 +53,30 @@ public JSPClassScanner(final Map removedBreakpoints, @Override public boolean scanClass(final Class loadedClass) { - if (removedBreakpoints.isEmpty()) { + // if the map is empty we have already found all our tracepoints + if (tracePointConfigMap.isEmpty()) { // stop as soon as we run out of classes return false; } + // if this class is a jsp class then we should process it if (JSPUtils.isJspClass(this.jspSuffix, this.jspPackages, loadedClass.getName())) { - final Set breakpoints = loadJspBreakpoints(loadedClass, removedBreakpoints); - if (!breakpoints.isEmpty()) { - for (TracePointConfig breakpoint : breakpoints) { - removedBreakpoints.remove(breakpoint.getId()); + // load (from our config) the JSP version of a tracepoint. + final Set tracePointConfigs = JSPUtils.loadJSPTracepoints(loadedClass, tracePointConfigMap); + // if we have some tracepoints + if (!tracePointConfigs.isEmpty()) { + // remove them from our config so we can end fast + for (TracePointConfig tracePointConfig : tracePointConfigs) { + tracePointConfigMap.remove(tracePointConfig.getId()); } + // this class is a JSP class with at least on tracepoints return true; } } return false; } + + @Override + public boolean isComplete() { + return tracePointConfigMap.isEmpty(); + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/SetClassScanner.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/SetClassScanner.java index 438652b..066d070 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/SetClassScanner.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/SetClassScanner.java @@ -20,6 +20,9 @@ import java.util.Set; +/** + * Scans a set of classes for classes we want to modify. + */ public class SetClassScanner implements IClassScanner { private final Set classNames; @@ -37,4 +40,9 @@ public boolean scanClass(final Class allLoadedClass) { || allLoadedClass.getName().contains("$") && this.classNames.contains(InstUtils.internalClassStripInner(allLoadedClass)); } + + @Override + public boolean isComplete() { + return classNames.isEmpty(); + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java index 1cafb51..dc0a10e 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java @@ -27,8 +27,8 @@ import com.intergral.deep.agent.tracepoint.inst.asm.Visitor; import com.intergral.deep.agent.tracepoint.inst.jsp.JSPMappedBreakpoint; import com.intergral.deep.agent.tracepoint.inst.jsp.JSPUtils; -import com.intergral.deep.agent.tracepoint.inst.jsp.SourceMap; -import com.intergral.deep.agent.tracepoint.inst.jsp.SourceMapLineStartEnd; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SourceMap; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SourceMapLineStartEnd; import com.intergral.deep.agent.types.TracePointConfig; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; @@ -48,6 +48,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This service deals with detecting which classes need to be transformed and uses the visitor to instrument the classes as needed. + */ public class TracepointInstrumentationService implements ClassFileTransformer { public static final long COMPUTE_ON_CLASS_VERSION = Long.getLong("nv.compute.class.version", 50L); @@ -59,9 +62,25 @@ public class TracepointInstrumentationService implements ClassFileTransformer { private final String jspSuffix; private final List jspPackages; - private Map> classPrefixBreakpoints = new ConcurrentHashMap<>(); + /** + * We process JSP classes specially, so we collect them all under this class key. + */ + private static final String JSP_CLASS_KEY = "jsp"; + + /** + * We process CFM classes specially, so we collect them all under this class key. + */ + private static final String CFM_CLASS_KEY = "cfm"; + + private Map> classPrefixTracepoints = new ConcurrentHashMap<>(); + /** + * Create a new service. + * + * @param inst the instrumentation service + * @param settings the deep settings + */ public TracepointInstrumentationService(final Instrumentation inst, final Settings settings) { this.inst = inst; this.disPath = settings.getSettingAs("transform.path", String.class); @@ -70,65 +89,13 @@ public TracepointInstrumentationService(final Instrumentation inst, final Settin this.jspSuffix = settings.getSettingAs("jsp.suffix", String.class); } - static Set loadJspBreakpoints(final Class loadedClass, - final Map jsp) { - return loadJspBreakpoints(JSPUtils.getSourceMap(loadedClass), jsp); - } - - static Set loadJspBreakpoints(final SourceMap sourceMap, - final Map jsp) { - if (sourceMap == null) { - return Collections.emptySet(); - } - - final Set matchedJsp = new HashSet<>(); - final List filenames = sourceMap.getFilenames(); - for (Map.Entry entry : jsp.entrySet()) { - final TracePointConfig value = entry.getValue(); - final String fileName = InstUtils.fileName(value.getPath()); - if (filenames.contains(fileName)) { - matchedJsp.add(value); - } - } - return matchedJsp; - } - - static Set loadCfBreakpoints(final URL location, - final Map values) { - final Set iBreakpoints = new HashSet<>(); - final Collection breakpoints = values.values(); - for (TracePointConfig breakpoint : breakpoints) { - final String srcRoot = breakpoint.getArgs().get("src_root"); - final String relPathFromNv = breakpoint.getPath(); - final String locationString = location.toString(); - if (srcRoot != null && locationString.endsWith(relPathFromNv.substring(srcRoot.length())) - || locationString.endsWith(relPathFromNv) - || relPathFromNv.startsWith("/src/main/cfml") - && locationString.endsWith(relPathFromNv.substring("/src/main/cfml".length())) - ) { - iBreakpoints.add(breakpoint); - } - } - return iBreakpoints; - } - - static Set loadCfBreakpoints(final String location, - final Map values) { - if (location == null) { - return Collections.emptySet(); - } - final Set iBreakpoints = new HashSet<>(); - final Collection breakpoints = values.values(); - for (TracePointConfig breakpoint : breakpoints) { - final String relPathFromNv = breakpoint.getPath(); - // some versions of lucee use lowercase file names - if (Utils.endsWithIgnoreCase(relPathFromNv, location)) { - iBreakpoints.add(breakpoint); - } - } - return iBreakpoints; - } - + /** + * Initialise the tracepoint service with the deep services. + * + * @param inst the instrumentation to use + * @param settings the settings for deep + * @return the configured service + */ public static TracepointInstrumentationService init(final Instrumentation inst, final Settings settings) { final TracepointInstrumentationService tracepointInstrumentationService = new TracepointInstrumentationService( @@ -141,15 +108,15 @@ public static TracepointInstrumentationService init(final Instrumentation inst, } /** - * Process the new config from the services and determine which classes need to be transformed, - * and trigger transformation. + * Process the new config from the services and determine which classes need to be transformed, and trigger transformation. * * @param breakpointResponse the new list of tracepoints that have been received from the server. */ public synchronized void processBreakpoints( final Collection breakpointResponse) { - final Map> existingBreakpoints = this.classPrefixBreakpoints; - final Set newBreakpointOnExistingClasses = new HashSet<>(); + // keep record of existing tracepoints + final Map> existingTracepoints = this.classPrefixTracepoints; + final Set newTracepointOnExistingClasses = new HashSet<>(); final Map> newBreakpoints = new HashMap<>(); // process new breakpoints mapping to new breakpoints map { className -> { breakpoint id -> breakpoint } } @@ -163,49 +130,49 @@ public synchronized void processBreakpoints( newBreakpoints.put(fullClass, value); } - final Map existingConfig = existingBreakpoints.get(fullClass); + // track new tracepoints added to class that already has tracepoints on it + final Map existingConfig = existingTracepoints.get(fullClass); if (existingConfig != null && !existingConfig.containsKey(tracePointConfig.getId())) { - newBreakpointOnExistingClasses.add(fullClass); + newTracepointOnExistingClasses.add(fullClass); } } - this.classPrefixBreakpoints = new ConcurrentHashMap<>(newBreakpoints); + // set the new config of the tracepoints + this.classPrefixTracepoints = new ConcurrentHashMap<>(newBreakpoints); // build class scanners final CompositeClassScanner compositeClassScanner = new CompositeClassScanner(); // scanner to handle classes that no longer have classes and need transformed final IClassScanner removedTracepoints = reTransformClassesThatNoLongerHaveTracePoints( - new HashSet<>(existingBreakpoints.keySet()), new HashSet<>(newBreakpoints.keySet())); + new HashSet<>(existingTracepoints.keySet()), new HashSet<>(newBreakpoints.keySet())); compositeClassScanner.addScanner(removedTracepoints); // scanner to handle classes that now have tracepoints and need transformed final IClassScanner newClasses = reTransformClassesThatAreNew( - new HashSet<>(existingBreakpoints.keySet()), + new HashSet<>(existingTracepoints.keySet()), new HashSet<>(newBreakpoints.keySet())); compositeClassScanner.addScanner(newClasses); // scanner to handle classes that have tracepoints already, but the configs have changed - final SetClassScanner modifiedClasses = new SetClassScanner(newBreakpointOnExistingClasses); + final SetClassScanner modifiedClasses = new SetClassScanner(newTracepointOnExistingClasses); compositeClassScanner.addScanner(modifiedClasses); // scanner to handle JSP classes - if (this.classPrefixBreakpoints.containsKey("jsp") || existingBreakpoints.containsKey("jsp")) { - final Map jsp = this.classPrefixBreakpoints.get("jsp"); - final IClassScanner jspScanner = reTransFormJSPClasses(new HashMap<>( - jsp == null ? Collections.emptyMap() : jsp), - Utils.newMap(existingBreakpoints.get("jsp"))); + if (this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY) || existingTracepoints.containsKey(JSP_CLASS_KEY)) { + final Map jsp = this.classPrefixTracepoints.get(JSP_CLASS_KEY); + final IClassScanner jspScanner = reTransFormJSPClasses(Utils.newMap(jsp), + Utils.newMap(existingTracepoints.get(JSP_CLASS_KEY))); compositeClassScanner.addScanner(jspScanner); } // scanner to handle CFM classes - if (this.classPrefixBreakpoints.containsKey("cfm") || existingBreakpoints.containsKey("cfm")) { - final Map cfm = this.classPrefixBreakpoints.get("cfm"); - final IClassScanner cfmScanner = reTransFormCfClasses(new HashMap<>( - cfm == null ? Collections.emptyMap() : cfm), - Utils.newMap(existingBreakpoints.get("cfm"))); + if (this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) || existingTracepoints.containsKey(CFM_CLASS_KEY)) { + final Map cfm = this.classPrefixTracepoints.get(CFM_CLASS_KEY); + final IClassScanner cfmScanner = reTransFormCfClasses(Utils.newMap(cfm), + Utils.newMap(existingTracepoints.get(CFM_CLASS_KEY))); compositeClassScanner.addScanner(cfmScanner); } - LOGGER.debug("New breakpoint config {}", this.classPrefixBreakpoints); + LOGGER.debug("New breakpoint config {}", this.classPrefixTracepoints); try { // scan loaded classes and transform @@ -219,48 +186,98 @@ public synchronized void processBreakpoints( } } - private IClassScanner reTransFormJSPClasses(final Map jsp, - final Map oldJsp) { - final Map removedBreakpoints = withRemoved(jsp, oldJsp); - return new JSPClassScanner(removedBreakpoints, this.jspSuffix, this.jspPackages); + /** + * Calculate the classes to scan for JSP. + * + * @param newJSPState the new JSP state + * @param previousJSPState the previous JSP state + * @return the JSP class scanner + */ + private IClassScanner reTransFormJSPClasses( + final Map newJSPState, + final Map previousJSPState) { + final Map allTracepoints = withRemoved(newJSPState, previousJSPState); + return new JSPClassScanner(allTracepoints, this.jspSuffix, this.jspPackages); } - Map withRemoved(final Map jsp, - final Map oldJsp) { - final Set newIds = jsp.keySet(); - final Set oldIds = oldJsp.keySet(); + /** + * Add the removed tracepoints info the new state. + * + * @param newState the new state of tracepoints + * @param previousState the previous state of tracepoints + * @return the newState plus any previous that have been removed + */ + private Map withRemoved( + final Map newState, + final Map previousState) { + final Set newIds = newState.keySet(); + final Set oldIds = previousState.keySet(); oldIds.removeAll(newIds); for (String oldId : oldIds) { - jsp.put(oldId, oldJsp.get(oldId)); + newState.put(oldId, previousState.get(oldId)); } - return jsp; + return newState; } - IClassScanner reTransFormCfClasses(final Map cfm, - final Map oldCfm) { - final Map removedBreakpoints = withRemoved(cfm, oldCfm); + /** + * Calculate the classes to scan for CFM. + * + * @param newCFMState the new CFM state + * @param previousCFMState the previous CFM state + * @return the CFM class scanner + */ + //exposed for testing + protected CFClassScanner reTransFormCfClasses( + final Map newCFMState, + final Map previousCFMState) { + final Map allTracepoints = withRemoved(newCFMState, previousCFMState); - return new CFClassScanner(removedBreakpoints); + return new CFClassScanner(allTracepoints); } - public URL getLocation(final ProtectionDomain protectionDomain) { + //exposed for tests + protected URL getLocation(final ProtectionDomain protectionDomain) { return protectionDomain.getCodeSource().getLocation(); } - private IClassScanner reTransformClassesThatAreNew(final Set existingClasses, - final Set newClasses) { - newClasses.removeAll(existingClasses); - return new SetClassScanner(newClasses); + /** + * Calculate the classes that now have tracepoints but did not before. + *

+ * e.g. If the classes {a, b, c} are the previous set and {a, d, e} is the new set. The result would be a set of {d,e}. + * + * @param previousState the list of classes that had tracepoints on them + * @param newState the list of classes that now have tracepoints on them + * @return the set difference between the new and new previous + */ + private IClassScanner reTransformClassesThatAreNew(final Set previousState, + final Set newState) { + newState.removeAll(previousState); + // jsp and cfm are processed special so ignore them here + previousState.remove(JSP_CLASS_KEY); + previousState.remove(CFM_CLASS_KEY); + return new SetClassScanner(newState); } + /** + * Calculate the classes that did have tracepoints but no longer do. + *

+ * e.g. If the classes {a, b, c} are the previous set and {a, d, e} is the new set. The result would be a set of {b,c}. + * + * @param previousState the list of classes that had tracepoints on them + * @param newState the list of classes that now have tracepoints on them + * @return the set difference between the previous and new state + */ private IClassScanner reTransformClassesThatNoLongerHaveTracePoints( - final Set existingClasses, - final Set newClasses) { - existingClasses.removeAll(newClasses); - return new SetClassScanner(existingClasses); + final Set previousState, + final Set newState) { + previousState.removeAll(newState); + // jsp and cfm are processed special so ignore them here + previousState.remove(JSP_CLASS_KEY); + previousState.remove(CFM_CLASS_KEY); + return new SetClassScanner(previousState); } @Override @@ -279,15 +296,15 @@ public byte[] transform(final ClassLoader loader, final Collection matchedTracepoints = matchTracepoints(className, shortClassName); // no breakpoints for this class or any CF classes - if (matchedTracepoints.isEmpty() && !this.classPrefixBreakpoints.containsKey("cfm") - && !this.classPrefixBreakpoints.containsKey("jsp")) { + if (matchedTracepoints.isEmpty() && !this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) + && !this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY)) { return null; - } - // no breakpoints for this class, but we have a cfm breakpoints, and this is a cfm class - else if (matchedTracepoints.isEmpty() - && this.classPrefixBreakpoints.containsKey("cfm") + } else if (matchedTracepoints.isEmpty() + && this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) && CFUtils.isCfClass(classNameP)) { - final Map cfm = this.classPrefixBreakpoints.get("cfm"); + // no breakpoints for this class, but we have a cfm breakpoints, and this is a cfm class + + final Map cfm = this.classPrefixTracepoints.get(CFM_CLASS_KEY); final URL location = getLocation(protectionDomain); if (location == null) { reader = new ClassReader(classfileBuffer); @@ -295,20 +312,19 @@ else if (matchedTracepoints.isEmpty() // no need to expand frames here as we only need the version and source file reader.accept(cn, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE); final String sourceFile = cn.sourceFile; - iBreakpoints = loadCfBreakpoints(sourceFile, cfm); + iBreakpoints = CFUtils.loadCfTracepoints(sourceFile, cfm); } else { - iBreakpoints = loadCfBreakpoints(location, cfm); + iBreakpoints = CFUtils.loadCfTracepoints(location, cfm); } if (iBreakpoints.isEmpty()) { return null; } isCf = true; - } - // no breakpoints for this class, but we have a jsp breakpoints, and this is a jsp class - else if (matchedTracepoints.isEmpty() - && this.classPrefixBreakpoints.containsKey("jsp") + } else if (matchedTracepoints.isEmpty() + && this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY) && JSPUtils.isJspClass(this.jspSuffix, this.jspPackages, InstUtils.externalClassName(className))) { + // no breakpoints for this class, but we have a jsp breakpoints, and this is a jsp class isCf = false; final SourceMap sourceMap = JSPUtils.getSourceMap(classfileBuffer); if (sourceMap == null) { @@ -316,8 +332,8 @@ else if (matchedTracepoints.isEmpty() return null; } - final Collection rawBreakpoints = loadJspBreakpoints(sourceMap, - this.classPrefixBreakpoints.get("jsp")); + final Collection rawBreakpoints = JSPUtils.loadJSPTracepoints(sourceMap, + this.classPrefixTracepoints.get(JSP_CLASS_KEY)); if (rawBreakpoints.isEmpty()) { LOGGER.debug("Cannot load tracepoints for class: {}", className); return null; @@ -330,13 +346,13 @@ else if (matchedTracepoints.isEmpty() if (mappedLines.isEmpty()) { continue; } + // todo should we install on all lines? final int start = mappedLines.get(0).getStart(); iBreakpoints.add(new JSPMappedBreakpoint(rawBreakpoint, start)); } } - } - // else there is a tracepoint for this class - else { + } else { + // else there is a tracepoint for this class isCf = false; iBreakpoints = matchedTracepoints; } @@ -382,12 +398,12 @@ else if (matchedTracepoints.isEmpty() private Collection matchTracepoints(final String className, final String shortClassName) { - final Map classMatches = this.classPrefixBreakpoints.get(className); + final Map classMatches = this.classPrefixTracepoints.get(className); if (classMatches != null) { return classMatches.values(); } - final Map shortClassMatches = this.classPrefixBreakpoints.get( + final Map shortClassMatches = this.classPrefixTracepoints.get( shortClassName); if (shortClassMatches != null) { return shortClassMatches.values(); diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfo.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfo.java index 2962d19..258c3d9 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfo.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfo.java @@ -31,6 +31,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * We need to load classes without loading classes, so we have this type that lets us do this. + */ public class ClassInfo { private static final Logger LOGGER = LoggerFactory.getLogger(ClassInfo.class); @@ -45,6 +48,13 @@ public class ClassInfo { private final ClassLoader loader; + /** + * Create a new Class Info. + * + * @param type the type to load + * @param loader the loader to use + * @param classReader the class reader to use + */ public ClassInfo(final String type, final ClassLoader loader, final ClassReader classReader) { this.loader = loader; this.type = Type.getObjectType(type); @@ -52,16 +62,15 @@ public ClassInfo(final String type, final ClassLoader loader, final ClassReader } /** - * This method should not be used out side of this type! + * This method should not be used outside of this type! *

* The intention for this method is to reduce the number of exceptions being generated. * * @param type the internal type, or class, name to look for. * @param loader the class loader to start from - * @param thro whether or not to throw the exception on a error. + * @param thro whether to throw the exception on a error. * @return either the {@link ClassInfo} for the given type, or null if it could not be found. If - * {@code thro} is {@code true} then a {@link ClassInfoNotFoundException} is throw when the class - * cannot be found. + * {@code thro} is {@code true} then a {@link ClassInfoNotFoundException} is throw when the class cannot be found. */ @SuppressWarnings("Duplicates") static ClassInfo loadClassInfo(final String type, final ClassLoader loader, final boolean thro) { @@ -131,15 +140,15 @@ static ClassInfo loadClassInfo(final String type, final ClassLoader loader, fina return null; } - private static boolean isRailoClassLoader(final ClassLoader loader) { + static boolean isRailoClassLoader(final ClassLoader loader) { return loader.getClass().getName().equals(RAILO_LOADER); } - private static boolean isLuceeClassLoader(final ClassLoader loader) { + static boolean isLuceeClassLoader(final ClassLoader loader) { return loader.getClass().getName().equals(LUCEE_LOADER); } - private static boolean isSafeLoader(final ClassLoader loader) { + static boolean isSafeLoader(final ClassLoader loader) { final String name = loader.getClass().getName(); return SAFE_LOADERS.contains(name); } @@ -204,7 +213,7 @@ boolean isInterface() { } - public boolean implementsInterface(final ClassInfo that) { + boolean implementsInterface(final ClassInfo that) { for (ClassInfo c = this; c != null; c = c.getSuperclass()) { ClassInfo[] tis = c.getInterfaces(); for (ClassInfo ti : tis) { @@ -227,7 +236,7 @@ private boolean isSubclassOf(final ClassInfo that) { } - public boolean isAssignableFrom(final ClassInfo that) { + boolean isAssignableFrom(final ClassInfo that) { if (this == that) { return true; } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundException.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundException.java index f2782ff..8aada8d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundException.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundException.java @@ -17,6 +17,9 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +/** + * Used to indicate that a {@link ClassInfo} could not be loaded for a give type. + */ public class ClassInfoNotFoundException extends RuntimeException { private final String type; diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassLoaderAwareClassWriter.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassLoaderAwareClassWriter.java index d7abe93..6021490 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassLoaderAwareClassWriter.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassLoaderAwareClassWriter.java @@ -23,6 +23,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This type helps improve the ability to {@link #getCommonSuperClass}. Essentially adding support to use the class loader used to load the + * class and dealing with some known cases of optional types. + */ public class ClassLoaderAwareClassWriter extends ClassWriter { private static final Logger LOGGER = LoggerFactory.getLogger(ClassLoaderAwareClassWriter.class); diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/SkipException.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/SkipException.java index cfb25b8..8b905f3 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/SkipException.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/SkipException.java @@ -17,6 +17,9 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +/** + * Used to force ASM to skip a class, if we could not get the source information. + */ public class SkipException extends Error { public SkipException() { diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java index 62bc624..4b6799c 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java @@ -17,21 +17,33 @@ package com.intergral.deep.agent.tracepoint.inst.asm; -import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; - import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.List; -public class TransformerUtils { +/** + * Utilities for transforming the classes. + */ +public final class TransformerUtils { + + private TransformerUtils() { + } private static final List EXCLUDE_PACKAGES = Collections.emptyList(); private static final List EXCLUDE_CONTAINS = Collections.emptyList(); + /** + * Store the transformed bytes to allow us to debug them in enabled. + * + * @param path the path to store the bytes + * @param original the unmodified bytes + * @param transformed the modified bytes + * @param className the class name being modified + * @return {@code true} if we stored the data + */ public static boolean storeUnsafe(String path, byte[] original, byte[] transformed, String className) { try { @@ -40,15 +52,15 @@ public static boolean storeUnsafe(String path, byte[] original, byte[] transform } store(path, original, transformed, className); return true; - } catch (IOException ex) { + } catch (Exception ex) { return false; } } - public static void store(String path, byte[] originalBytes, byte[] transformedBytes, + static void store(String path, byte[] originalBytes, byte[] transformedBytes, String className) - throws FileNotFoundException, IOException { + throws Exception { if (path == null) { return; } @@ -60,41 +72,43 @@ public static void store(String path, byte[] originalBytes, byte[] transformedBy String filename = className.replace("/", "_"); - { - File org = new File(dir, filename + ".class"); - if (org.exists()) { - org.delete(); - } - FileOutputStream out = new FileOutputStream(org, false); - out.write(originalBytes); - out.close(); - - org.setReadable(true, false); - org.setWritable(true, false); - org.setExecutable(true, false); - } + writeToFile(originalBytes, dir, filename + ".class"); - { - File transformed = new File(dir, filename + "_trnfm.class"); - if (transformed.exists()) { - transformed.delete(); - } - FileOutputStream out = new FileOutputStream(transformed, false); - out.write(transformedBytes); - out.close(); + writeToFile(transformedBytes, dir, filename + "_trnfm.class"); + } - transformed.setReadable(true, false); - transformed.setWritable(true, false); - transformed.setExecutable(true, false); + private static void writeToFile(final byte[] originalBytes, final File dir, final String filename) throws IOException { + File org = new File(dir, filename); + if (org.exists()) { + org.delete(); } + FileOutputStream out = new FileOutputStream(org, false); + out.write(originalBytes); + out.close(); + + org.setReadable(true, false); + org.setWritable(true, false); + org.setExecutable(true, false); } - public static boolean isExcludedClass(final Class loadedClass) { - return isExcludedClass(loadedClass.getName()); + /** + * Is the class excluded from transformation. + * + * @param clazz the class + * @return {@code true} if we should not transform this class. + */ + public static boolean isExcludedClass(final Class clazz) { + return isExcludedClass(clazz.getName()); } + /** + * Is the class excluded from transformation. + * + * @param classname the name of the class + * @return {@code true} if we should not transform this class. + */ public static boolean isExcludedClass(final String classname) { for (final String pkg : EXCLUDE_PACKAGES) { if (classname.startsWith(pkg)) { @@ -110,9 +124,4 @@ public static boolean isExcludedClass(final String classname) { return false; } - - - public static boolean isAbstract(final int access) { - return (access & ACC_ABSTRACT) == ACC_ABSTRACT; - } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java index de31ba6..79e6c91 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java @@ -80,6 +80,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +/** + * This visitor is the main magic of deep. It deals with install the callbacks into the user code, based on the tracepoints. + */ @SuppressWarnings({"DuplicatedCode", "CommentedOutCode"}) public class Visitor extends ClassVisitor { @@ -109,9 +113,9 @@ public class Visitor extends ClassVisitor { static { // this is here to make the tests easier. // we cannot use java. classes in the tests without screwing with the class loaders - // so in the tests we use the 'nv.callback.class' which is the CallBack.class - // at runtime we use the ProxyCallback.class so we can bypass the osgi classloading restrictions - final String property = System.getProperty("nv.callback.class"); + // so in the tests we use the 'deep.callback.class' which is the CallBack.class + // at runtime we use the ProxyCallback.class, so we can bypass the osgi classloading restrictions + final String property = System.getProperty("deep.callback.class"); if (property == null) { CALLBACK_CLASS = ProxyCallback.class; } else { @@ -125,6 +129,13 @@ public class Visitor extends ClassVisitor { } } + /** + * Create a new visitor. + * + * @param v the asm visitor that calls this + * @param bps the tracepoints we want to install + * @param isCf is this a cf class + */ public Visitor(final ClassVisitor v, final Collection bps, final boolean isCf) { super(ASM8, v); this.bps = bps; @@ -149,11 +160,6 @@ public boolean wasChanged() { } - public String getFilename() { - return filename; - } - - @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { @@ -177,6 +183,142 @@ public void visitSource(String source, String debug) { filename = source; } + /** + * Converts primatives to objects so we can put them in the map. + * + * @param index variable index + * @param clazz variable class + * @param loadOpcode opcode + * @param primitive is primitive variable + * @return the instruction list + */ + private static InsnList box(final int index, final Type primitive, final Class clazz, + final int loadOpcode) { + final InsnList boxOperations = new InsnList(); + boxOperations.add(new VarInsnNode(loadOpcode, index)); + // Call Double.valueOf or Long.valueOf etc + boxOperations.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(clazz), "valueOf", + Type.getMethodDescriptor(Type.getType(clazz), primitive), false)); + return boxOperations; + } + + /** + * Generates a XXLOAD operation for a specific variable slot type. + * + * @param t variable type + * @param index variable index + * @return the instruction list + */ + static InsnList loadVariable(final Type t, final int index) { + final int sort = t.getSort(); + switch (sort) { + case Type.BYTE: + return box(index, Type.BYTE_TYPE, Byte.class, ILOAD); + case Type.CHAR: + return box(index, Type.CHAR_TYPE, Character.class, ILOAD); + case Type.DOUBLE: + return box(index, Type.DOUBLE_TYPE, Double.class, DLOAD); + case Type.FLOAT: + return box(index, Type.FLOAT_TYPE, Float.class, FLOAD); + case Type.INT: + return box(index, Type.INT_TYPE, Integer.class, ILOAD); + case Type.LONG: + return box(index, Type.LONG_TYPE, Long.class, LLOAD); + case Type.SHORT: + return box(index, Type.SHORT_TYPE, Short.class, ILOAD); + case Type.BOOLEAN: + return box(index, Type.BOOLEAN_TYPE, Boolean.class, ILOAD); + case Type.ARRAY: + case Type.OBJECT: + final InsnList ops = new InsnList(); + ops.add(new VarInsnNode(ALOAD, index)); + return ops; + default: + // Something is very strange + LOGGER.error("loadVariable - Unknown type : {}", t); + final InsnList valueGetter = new InsnList(); + valueGetter.add(new LdcInsnNode("Unknown type :" + t)); + return valueGetter; + } + } + + private boolean isSuperCall(final AbstractInsnNode node) { + // need a method node + if (!(node instanceof MethodInsnNode)) { + return false; + } + + // we need to use INVOKESPECIAL on constructors + if (node.getOpcode() != INVOKESPECIAL) { + return false; + } + + // all constructors have the same name + if (!((MethodInsnNode) node).name.equals("")) { + return false; + } + + // check the super name against this call + return ((MethodInsnNode) node).owner.equals(this.superName); + } + + private boolean isThrow(final AbstractInsnNode node, final LabelNode start) { + if (start == null) { + return false; + } + + if (!(node instanceof InsnNode)) { + return false; + } + + final int opcode = node.getOpcode(); + return opcode == ATHROW; + } + + private boolean isNextLine(final AbstractInsnNode node, final LabelNode start) { + if (start == null) { + return false; + } + + return node.getType() == AbstractInsnNode.LINE; + } + + private boolean isReturn(final AbstractInsnNode node, final LabelNode start) { + if (start == null) { + return false; + } + + if (!(node instanceof InsnNode)) { + return false; + } + + final int opcode = node.getOpcode(); + switch (opcode) { + case Opcodes.RETURN: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.ARETURN: + case Opcodes.DRETURN: + return true; + default: + return false; + } + } + + private boolean isStatic(final int access) { + return (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + } + + private boolean noneOrJustThis(final List localVariables) { + if (localVariables.isEmpty()) { + return true; + } + if (localVariables.size() == 1) { + return localVariables.get(0).name.equals("this"); + } + return false; + } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, @@ -388,7 +530,7 @@ public void visitEnd() { hook.add(new InsnNode(ATHROW)); // } (end catch) hook.add(new LabelNode( - startFinally)); // we start the finally before we end the catch for some reason + startFinally)); // we start the 'finally' before we end the catch for some reason // 'bad' finally { // store exception in next slot hook.add(new VarInsnNode(ASTORE, varOffset + 1)); @@ -408,7 +550,7 @@ public void visitEnd() { hook.add(new LdcInsnNode(bp.getId())); // bp id hook.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(HashSet.class), "add", "(Ljava/lang/Object;)Z")); - hook.add(new InsnNode(POP)); // dont care about return + hook.add(new InsnNode(POP)); // don't care about return } // if we are a next line then we need to remove the start line label from the seen labels so // the variable capture in the finally does not capture variables defined on the line we are wrapping @@ -447,17 +589,17 @@ public void visitEnd() { // we want to insert here - as the throw will be 'caught' by the catch/finally we are inserting instructions.insert(node, hook); } else { - // if we are a next line then we need to insert before the previous - // the previous on next lines will be the 'label' for the next line + // if we are a next line then we need to insert before the previous instruction + // the previous instruction on next lines will be the 'label' for the next line // methodVisitor.visitFieldInsn( PUTFIELD, "com/nerdvision/agent/BPTestTarget", "name", "Ljava/lang/String;" ); - this is the line we are wrapping // Label label1 = new Label(); // methodVisitor.visitLabel( label1 ); <- insert before this one - // methodVisitor.visitLineNumber( 32, label1 ); <- this it the node we are on + // methodVisitor.visitLineNumber( 32, label1 ); <- this is the node we are on // methodVisitor.visitInsn( RETURN ); <- this is the next line instructions.insertBefore(previous, hook); } - // remove the start so we know this line is done. + // remove the start, so we know this line is done. start = null; iBreakpoints.clear(); } @@ -481,6 +623,8 @@ public void visitEnd() { // first line will be hit as all subsequent will be disabled final List thisLineBps = lineNos.remove((long) ln.line); if (thisLineBps != null) { + // we track the breakpoints separate from the lineNos as we need to detect here what tracepoints should be installed, + // but we might need to use them in forth coming instructions for line end etc iBreakpoints.clear(); iBreakpoints.addAll(thisLineBps); } @@ -499,10 +643,18 @@ public void visitEnd() { if (!iBreakpoints.isEmpty()) { start = ln.start; LOGGER.trace("visitMethod {} {} line number {}", classname, name, ln.line); + // we insert the normal hook here, so we can capture the line data. + // the try/catch/finally that is added later can be attached regardless of when this hook is + // added as long as we have appropriate label instructions (which we add later) final InsnList hook = getAbstractInsnNodes(seenLabels, ln, iBreakpoints); changed = true; instructions.insert(ln, hook); + if (isCf) { + // if we are CF we do not support line capture, so we always clear the tracepoints + // for non-cf the tracepoints are cleared after the try/finally is added (on the next instruction) + iBreakpoints.clear(); + } LOGGER.debug("visitMethod {} {} patched @ {} {}", classname, name, ln.line, bps); } @@ -662,154 +814,8 @@ private void processLocalVariables(final Set

+ * This will simply act as a proxy to the {@link Callback} which is where we do the real work. + *

+ * This split is to allow us to support Lucee and other OSGi style environments that use isolated class loaders. + */ +@SuppressWarnings("unused") public class ProxyCallback { /** - * The main entry point for CF ASM injected breakpoints + * The main entry point for CF ASM injected breakpoints. * * @param bpIds the bp ids to trigger * @param filename the filename of the breakpoint hit @@ -41,7 +49,7 @@ public static void callBackCF(final List bpIds, /** - * The main entry point for non CF ASM injected breakpoints + * The main entry point for non CF ASM injected breakpoints. * * @param bpIds the bp ids to trigger * @param filename the filename of the breakpoint hit diff --git a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.ResourceProvider b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.ResourceProvider new file mode 100644 index 0000000..b0503ad --- /dev/null +++ b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.ResourceProvider @@ -0,0 +1,18 @@ +# +# Copyright (C) 2023 Intergral GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +com.intergral.deep.agent.resource.JavaResourceDetector \ No newline at end of file diff --git a/agent/src/main/resources/deep_settings.properties b/agent/src/main/resources/deep_settings.properties index 8c4b63d..35cbe98 100644 --- a/agent/src/main/resources/deep_settings.properties +++ b/agent/src/main/resources/deep_settings.properties @@ -32,4 +32,11 @@ in.app.exclude= # Use this to tell GRPC to use PreferHeapByteBufAllocator grpc.heap.allocator=false grpc.allocator=pooled -plugins=com.intergral.deep.plugin.JavaPlugin,com.intergral.deep.plugins.cf.CFPlugin \ No newline at end of file +plugins=com.intergral.deep.plugin.JavaPlugin,com.intergral.deep.plugins.cf.CFPlugin + +# Define what the jsp compilation convention is +# Default tomcat take index.jsp and make it into index_jsp.class +# some versions put this in a jsp package, some use org.apache.jsp (newer) +# but you can configure this in jspc, see packageRoot. +jsp.suffix=_jsp +jsp.packages=org.apache.jsp,jsp diff --git a/agent/src/test/java/coldfusion/package-info.java b/agent/src/test/java/coldfusion/package-info.java new file mode 100644 index 0000000..e1e5247 --- /dev/null +++ b/agent/src/test/java/coldfusion/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * These packages and classes are intended to act as stubs to let us load Cf classes during testing. + */ +package coldfusion; \ No newline at end of file diff --git a/agent/src/test/java/coldfusion/runtime/ArgumentCollection.java b/agent/src/test/java/coldfusion/runtime/ArgumentCollection.java new file mode 100644 index 0000000..9f9d854 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/ArgumentCollection.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class ArgumentCollection extends TestScope { + +} diff --git a/agent/src/test/java/coldfusion/runtime/AttributeCollection.java b/agent/src/test/java/coldfusion/runtime/AttributeCollection.java new file mode 100644 index 0000000..0fc5fad --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/AttributeCollection.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class AttributeCollection { + + public AttributeCollection(Object[] objects) { + } +} diff --git a/agent/src/test/java/coldfusion/runtime/CFPage.java b/agent/src/test/java/coldfusion/runtime/CFPage.java new file mode 100644 index 0000000..77b0955 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/CFPage.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public abstract class CFPage extends CfJspPage { + + @Override + protected abstract Object runPage(); +} diff --git a/agent/src/test/java/coldfusion/runtime/CfJspPage.java b/agent/src/test/java/coldfusion/runtime/CfJspPage.java new file mode 100644 index 0000000..8009178 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/CfJspPage.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +import coldfusion.tagext.io.OutputTag; +import java.io.IOException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.tagext.Tag; + +@SuppressWarnings("ALL") +public abstract class CfJspPage { + + public Tag parent; + public NeoPageContext pageContext = new NeoPageContext(); + + protected void bindPageVariables(VariableScope varscope, LocalScope locscope) { + + } + + protected Variable bindPageVariable(String varName, VariableScope varScope, LocalScope locScope) { + return null; + } + + protected abstract Object runPage(); + + public Tag _initTag(Class clazz, int slot, Tag parent) throws IOException { + return new OutputTag(); + } + + public void _setCurrentLineNo(int lineNo) { + } + + public void _whitespace(JspWriter out, String msg) { + + } +} diff --git a/agent/src/test/java/coldfusion/runtime/LocalScope.java b/agent/src/test/java/coldfusion/runtime/LocalScope.java new file mode 100644 index 0000000..f1e91e4 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/LocalScope.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class LocalScope { + +} diff --git a/agent/src/test/java/coldfusion/runtime/NeoPageContext.java b/agent/src/test/java/coldfusion/runtime/NeoPageContext.java new file mode 100644 index 0000000..0f457fc --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/NeoPageContext.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +import javax.servlet.jsp.JspWriter; +import org.apache.jasper.runtime.JspWriterImpl; + +@SuppressWarnings("ALL") +public class NeoPageContext { + + public JspWriter getOut() { + return new JspWriterImpl(); + } + + public void setPageEncoding(String encoding) { + } +} diff --git a/agent/src/test/java/coldfusion/runtime/TestScope.java b/agent/src/test/java/coldfusion/runtime/TestScope.java new file mode 100644 index 0000000..4fdf592 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/TestScope.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +import java.util.HashMap; + +@SuppressWarnings("ALL") +public class TestScope extends HashMap { + +} diff --git a/agent/src/test/java/coldfusion/runtime/UDFMethod.java b/agent/src/test/java/coldfusion/runtime/UDFMethod.java new file mode 100644 index 0000000..31400aa --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/UDFMethod.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class UDFMethod { + + private final String key; + + + public UDFMethod(final String key) { + this.key = key; + } +} diff --git a/agent/src/test/java/coldfusion/runtime/Variable.java b/agent/src/test/java/coldfusion/runtime/Variable.java new file mode 100644 index 0000000..cb0fa7c --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/Variable.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class Variable { + + public void set(int i) { + + } +} diff --git a/agent/src/test/java/coldfusion/runtime/VariableScope.java b/agent/src/test/java/coldfusion/runtime/VariableScope.java new file mode 100644 index 0000000..e2e73a4 --- /dev/null +++ b/agent/src/test/java/coldfusion/runtime/VariableScope.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.runtime; + +@SuppressWarnings("ALL") +public class VariableScope { + +} diff --git a/agent/src/test/java/coldfusion/tagext/GenericTag.java b/agent/src/test/java/coldfusion/tagext/GenericTag.java new file mode 100644 index 0000000..4486395 --- /dev/null +++ b/agent/src/test/java/coldfusion/tagext/GenericTag.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.tagext; + +import javax.servlet.jsp.tagext.TagSupport; + +@SuppressWarnings("ALL") +public abstract class GenericTag extends TagSupport { + + public void hasEndTag(boolean b) { + + } +} diff --git a/agent/src/test/java/coldfusion/tagext/io/DirectoryTag.java b/agent/src/test/java/coldfusion/tagext/io/DirectoryTag.java new file mode 100644 index 0000000..ae6185d --- /dev/null +++ b/agent/src/test/java/coldfusion/tagext/io/DirectoryTag.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.tagext.io; + +@SuppressWarnings("ALL") +public class DirectoryTag { + +} + diff --git a/agent/src/test/java/coldfusion/tagext/io/FileTag.java b/agent/src/test/java/coldfusion/tagext/io/FileTag.java new file mode 100644 index 0000000..5de709b --- /dev/null +++ b/agent/src/test/java/coldfusion/tagext/io/FileTag.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.tagext.io; + +@SuppressWarnings("ALL") +public class FileTag { + +} diff --git a/agent/src/test/java/coldfusion/tagext/io/OutputTag.java b/agent/src/test/java/coldfusion/tagext/io/OutputTag.java new file mode 100644 index 0000000..96e7526 --- /dev/null +++ b/agent/src/test/java/coldfusion/tagext/io/OutputTag.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.tagext.io; + +import coldfusion.tagext.GenericTag; + +@SuppressWarnings("ALL") +public class OutputTag extends GenericTag { + + public int doStartTag() { + return 1; + } + + public int doAfterBody() { + return 0; + } + + public int doEndTag() { + return 0; + } + + public int doCatch() { + return 0; + } +} diff --git a/agent/src/test/java/coldfusion/tagext/lang/LoopTag.java b/agent/src/test/java/coldfusion/tagext/lang/LoopTag.java new file mode 100644 index 0000000..61639eb --- /dev/null +++ b/agent/src/test/java/coldfusion/tagext/lang/LoopTag.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package coldfusion.tagext.lang; + +@SuppressWarnings("ALL") +public class LoopTag { + +} diff --git a/agent/src/test/java/com/intergral/deep/agent/AgentImplTest.java b/agent/src/test/java/com/intergral/deep/agent/AgentImplTest.java new file mode 100644 index 0000000..efc22a1 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/AgentImplTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; + +import com.intergral.deep.agent.api.hook.IDeepHook; +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class AgentImplTest { + + @Test + void awaitLatch() throws InterruptedException { + try (MockedStatic callback = Mockito.mockStatic(Callback.class, "init")) { + assertThrows(IllegalStateException.class, AgentImpl::loadDeepAPI); + AgentImpl.startup(Mockito.mock(Instrumentation.class), new HashMap<>()); + final Object o = AgentImpl.awaitLoadAPI(); + assertNotNull(o); + + final IDeepHook deepHook = (IDeepHook) o; + + assertNotNull(deepHook.deepService()); + assertNotNull(deepHook.reflectionService()); + + callback.verify(() -> Callback.init(Mockito.any(), Mockito.any(), Mockito.any()), times(1)); + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/AgentTest.java b/agent/src/test/java/com/intergral/deep/agent/AgentTest.java new file mode 100644 index 0000000..84c5c69 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/AgentTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AgentTest { + + + @Test + void parseArgs() { + final Map stringStringMap = Agent.parseArgs("me=you"); + assertEquals(stringStringMap, Collections.singletonMap("me", "you")); + } + + + @Test + void parseArgsWithTags() { + final Map stringStringMap = Agent.parseArgs("tags=testKey=testVal;testkey2=testval2"); + assertEquals(stringStringMap, Collections.singletonMap("tags", "testKey=testVal;testkey2=testval2")); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/DeepAgentTest.java b/agent/src/test/java/com/intergral/deep/agent/DeepAgentTest.java new file mode 100644 index 0000000..037cecb --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/DeepAgentTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent; + +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.assertTrue; +import static org.mockito.Mockito.times; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IPlugin.IPluginRegistration; +import com.intergral.deep.agent.api.tracepoint.ITracepoint; +import com.intergral.deep.agent.api.tracepoint.ITracepoint.ITracepointRegistration; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.handler.Callback; +import com.intergral.deep.agent.tracepoint.inst.TracepointInstrumentationService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class DeepAgentTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointInstrumentationService tracepointInstrumentationService = Mockito.mock(TracepointInstrumentationService.class); + private DeepAgent deepAgent; + + @BeforeEach + void setUp() { + + Mockito.when(settings.getSettingAs("poll.timer", Integer.class)).thenReturn(1010); + try (MockedStatic callback = Mockito.mockStatic(Callback.class, "init")) { + deepAgent = new DeepAgent(settings, tracepointInstrumentationService); + callback.verify(() -> Callback.init(Mockito.any(), Mockito.any(), Mockito.any()), times(1)); + } + } + + @Test + void start_shouldSetPluginsAndResource() { + deepAgent.start(); + Mockito.verify(settings).setPlugins(Mockito.anyCollection()); + Mockito.verify(settings).setResource(Mockito.any()); + } + + @Test + void registerPlugin() { + final IPluginRegistration iPluginRegistration = deepAgent.registerPlugin((settings, snapshot) -> null); + + assertNotNull(iPluginRegistration.get()); + assertFalse(iPluginRegistration.isAuthProvider()); + + Mockito.verify(settings, times(1)).addPlugin(Mockito.any()); + + iPluginRegistration.unregister(); + + Mockito.verify(settings, times(1)).removePlugin(Mockito.any()); + } + + @Test + void registerTracepoint() { + final ITracepointRegistration iTracepointRegistration = deepAgent.registerTracepoint("some/path", 123); + + final ITracepoint iTracepoint = iTracepointRegistration.get(); + assertEquals("some/path", iTracepoint.path()); + assertEquals(123, iTracepoint.line()); + assertNotNull(iTracepoint.id()); + assertNotNull(iTracepoint.watches()); + assertNotNull(iTracepoint.args()); + + iTracepointRegistration.unregister(); + } + + @Test + void isEnabled() { + Mockito.when(settings.isActive()).thenReturn(false).thenReturn(false).thenReturn(true); + + assertFalse(deepAgent.isEnabled()); + + deepAgent.setEnabled(true); + + assertTrue(deepAgent.isEnabled()); + } + + @Test + void getVersion() { + assertEquals(DeepVersion.VERSION, deepAgent.getVersion()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/IDUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/IDUtilsTest.java new file mode 100644 index 0000000..d58e197 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/IDUtilsTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class IDUtilsTest { + + @Test + void randomId() { + assertNotNull(IDUtils.randomId()); + assertEquals(16, IDUtils.randomId().length()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/UtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/UtilsTest.java new file mode 100644 index 0000000..29dd466 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/UtilsTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent; + +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.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +class UtilsTest { + + @Test + void javaVersion() { + assertTrue(Utils.getJavaVersion() > 7); + } + + @Test + void currentTimeNanos() throws InterruptedException { + final long[] start = Utils.currentTimeNanos(); + Thread.sleep(1000); + final long[] after = Utils.currentTimeNanos(); + + assertTrue(after[0] > start[0]); + assertTrue(after[1] > start[1]); + } + + @Test + void newMap() { + assertEquals(Collections.emptyMap(), Utils.newMap(new HashMap<>())); + + assertEquals("value", Utils.newMap(Collections.singletonMap("key", "value")).get("key")); + } + + @Test + void endsWithIgnoreCase() { + assertTrue(Utils.endsWithIgnoreCase("someString", "string")); + assertTrue(Utils.endsWithIgnoreCase("someString", "String")); + assertFalse(Utils.endsWithIgnoreCase("someString", "qtring")); + } + + @Test + void valueOf() { + class BadToString { + + @Override + public String toString() { + throw new NullPointerException(); + } + + } + + assertEquals("some value", Utils.valueOf("some value")); + assertEquals("1", Utils.valueOf("1")); + assertEquals("null", Utils.valueOf(null)); + final String valueOf = Utils.valueOf(new BadToString()); + assertNotNull(valueOf); + assertTrue(valueOf.startsWith(BadToString.class.getName())); + assertTrue(valueOf.endsWith("toString() failed")); + + } + + @Test + void trim() { + assertEquals("value", Utils.trimPrefix("//value", "/")); + assertEquals("test", Utils.trimPrefix(".....test", ".")); + } + + @Test + void truncate() { + assertTrue(Utils.truncate("somelongstring", 10).truncated()); + assertEquals("somelongst", Utils.truncate("somelongstring", 10).value()); + assertFalse(Utils.truncate("somelongstring", 20).truncated()); + assertEquals("somelongstring", Utils.truncate("somelongstring", 20).value()); + } + + + @Test + void getVersion() { + assertEquals(7, Utils.extractVersion("1.7")); + assertEquals(8, Utils.extractVersion("1.8")); + assertEquals(9, Utils.extractVersion("1.9")); + assertEquals(10, Utils.extractVersion("10.1")); + assertEquals(11, Utils.extractVersion("11.2")); + assertEquals(12, Utils.extractVersion("12.4")); + assertEquals(13, Utils.extractVersion("13.5")); + assertEquals(14, Utils.extractVersion("14.5")); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/grpc/GrpcServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/grpc/GrpcServiceTest.java new file mode 100644 index 0000000..43dc10f --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/grpc/GrpcServiceTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import com.intergral.deep.agent.api.auth.IAuthProvider; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.logging.Logger; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.proto.poll.v1.PollRequest; +import com.intergral.deep.proto.poll.v1.PollResponse; +import com.intergral.deep.proto.tracepoint.v1.Snapshot; +import com.intergral.deep.proto.tracepoint.v1.SnapshotResponse; +import com.intergral.deep.tests.grpc.TestInterceptor; +import com.intergral.deep.tests.grpc.TestPollService; +import com.intergral.deep.tests.grpc.TestSnapshotService; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerInterceptors; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GrpcServiceTest { + + private Server server; + private CountDownLatch pollLatch; + private final AtomicReference pollRequest = new AtomicReference<>(); + private CountDownLatch snapshotLatch; + private final AtomicReference snapshotRequest = new AtomicReference<>(); + + private final AtomicReference header = new AtomicReference<>(); + private int port; + + @BeforeAll + static void beforeAll() { + System.setProperty("java.util.logging.config.file", ClassLoader.getSystemResource("logging.properties").getPath()); + Logger.configureLogging(Settings.build(Collections.singletonMap("logging.level", "DEBUG"))); + } + + @BeforeEach + void setUp() throws Exception { + + final TestInterceptor testInterceptor = new TestInterceptor("test_key"); + + pollLatch = new CountDownLatch(1); + snapshotLatch = new CountDownLatch(1); + final TestSnapshotService testSnapshotService = new TestSnapshotService((snapshot, responseObserver) -> { + final String headerVal = testInterceptor.contextKey().get(); + header.set(headerVal); + snapshotRequest.set(snapshot); + snapshotLatch.countDown(); + responseObserver.onNext(SnapshotResponse.newBuilder().build()); + responseObserver.onCompleted(); + }); + + final TestPollService testPollService = new TestPollService((request, responseObserver) -> { + final String headerVal = testInterceptor.contextKey().get(); + header.set(headerVal); + pollRequest.set(request); + pollLatch.countDown(); + responseObserver.onNext(PollResponse.newBuilder().setTsNanos(202020L).build()); + responseObserver.onCompleted(); + }); + + // find a free port + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + + server = ServerBuilder.forPort(port) + .addService(ServerInterceptors.intercept(testPollService.bindService(), testInterceptor)) + .addService(ServerInterceptors.intercept(testSnapshotService.bindService(), testInterceptor)).build(); + + server.start(); + } + + @AfterEach + void tearDown() throws IOException { + this.server.shutdownNow(); + } + + @Test + void serverCanConnect_poll() throws InterruptedException { + + final HashMap map = new HashMap<>(); + map.put(ISettings.KEY_SERVICE_URL, "localhost:" + port); + map.put(ISettings.KEY_SERVICE_SECURE, "false"); + map.put(ISettings.KEY_AUTH_PROVIDER, MockAuthProvider.class.getName()); + + final GrpcService grpcService = new GrpcService(Settings.build(map)); + + new Thread(grpcService::start).start(); + + final PollResponse pollResponse = grpcService.pollService().poll(PollRequest.newBuilder().setTsNanos(101010L).build()); + assertEquals(202020L, pollResponse.getTsNanos()); + pollLatch.await(5, TimeUnit.SECONDS); + + final PollRequest pollRequest = this.pollRequest.get(); + assertNotNull(pollRequest); + + assertEquals("test_header", header.get()); + } + + @Test + void serverCanConnect_snapshot() throws InterruptedException { + + final HashMap map = new HashMap<>(); + map.put(ISettings.KEY_SERVICE_URL, "localhost:" + port); + map.put(ISettings.KEY_SERVICE_SECURE, "false"); + map.put(ISettings.KEY_AUTH_PROVIDER, MockAuthProvider.class.getName()); + + final GrpcService grpcService = new GrpcService(Settings.build(map)); + + new Thread(grpcService::start).start(); + final CountDownLatch responseLatch = new CountDownLatch(1); + final AtomicReference responseAtomicReference = new AtomicReference<>(); + grpcService.snapshotService().send(Snapshot.newBuilder().build(), + new StreamObserver() { + @Override + public void onNext(final SnapshotResponse value) { + responseAtomicReference.set(value); + responseLatch.countDown(); + } + + @Override + public void onError(final Throwable t) { + t.printStackTrace(); + fail("Cannot connect snapshot service"); + } + + @Override + public void onCompleted() { + + } + }); + responseLatch.await(5, TimeUnit.SECONDS); + final SnapshotResponse snapshotResponse = responseAtomicReference.get(); + assertNotNull(snapshotResponse); + assertEquals("test_header", header.get()); + } + + + public static class MockAuthProvider implements IAuthProvider { + + @Override + public Map provide() { + return Collections.singletonMap("test_key", "test_header"); + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/plugins/PluginLoaderTest.java b/agent/src/test/java/com/intergral/deep/agent/plugins/PluginLoaderTest.java new file mode 100644 index 0000000..8c27177 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/plugins/PluginLoaderTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.plugins; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.agent.ReflectionUtils; +import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.plugin.JavaPlugin; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PluginLoaderTest { + + @Test + void canLoadNoPlugins() { + final List iPlugins = PluginLoader.loadPlugins(Settings.build(Collections.emptyMap()), ReflectionUtils.getReflection()); + assertEquals(1, iPlugins.size()); + assertEquals(iPlugins.get(0).name(), JavaPlugin.class.getName()); + } + + @Test + void canLoadBadPlugin() { + final HashMap agentArgs = new HashMap<>(); + agentArgs.put(ISettings.PLUGINS, BadPlugin.class.getName()); + final List iPlugins = PluginLoader.loadPlugins(Settings.build(agentArgs), ReflectionUtils.getReflection()); + assertEquals(0, iPlugins.size()); + } + + @Test + void canLoadGoodPlugin() { + final HashMap agentArgs = new HashMap<>(); + agentArgs.put(ISettings.PLUGINS, GoodPlugin.class.getName()); + final List iPlugins = PluginLoader.loadPlugins(Settings.build(agentArgs), ReflectionUtils.getReflection()); + assertEquals(1, iPlugins.size()); + assertEquals(iPlugins.get(0).name(), GoodPlugin.class.getName()); + } + + public static class BadPlugin { + + } + + public static class GoodPlugin implements IPlugin { + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + return null; + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/poll/DriftAwareThreadTest.java b/agent/src/test/java/com/intergral/deep/agent/poll/DriftAwareThreadTest.java new file mode 100644 index 0000000..c9d2eaa --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/poll/DriftAwareThreadTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.poll; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DriftAwareThreadTest { + + private DriftAwareThread task; + + + @BeforeEach + public void setUp() { + this.task = new DriftAwareThread("Test", null, 1000); + } + + + @Test + public void delay2() throws Exception { + class LWrap { + + long now; + } + + final LWrap lwrap = new LWrap(); + final CountDownLatch countDownLatch = new CountDownLatch(1); + final long currentTimeMillis = System.currentTimeMillis(); + final DriftAwareThread driftAwareThread = new DriftAwareThread("Test", new ITimerTask() { + @Override + public void run(final long now) { + assertTrue((now + 10000) >= currentTimeMillis); + lwrap.now = now; + countDownLatch.countDown(); + } + + + @Override + public long callback(final long duration, final long next) { + return next; + } + }, 5000); + driftAwareThread.start(10000); + countDownLatch.await(); + driftAwareThread.stopTask(); + assertTrue((lwrap.now + 10000) >= currentTimeMillis); + } + + + @Test + public void testCheckForEarlyWakeUp() throws Exception { + assertEquals(1000, task.checkForEarlyWake(0, 1000)); + assertEquals(999, task.checkForEarlyWake(1, 1000)); + assertEquals(100, task.checkForEarlyWake(900, 1000)); + assertEquals(1, task.checkForEarlyWake(999, 1000)); + assertEquals(-1, task.checkForEarlyWake(1000, 1000)); + assertEquals(-1, task.checkForEarlyWake(1001, 1000)); + assertEquals(-1, task.checkForEarlyWake(1100, 1000)); + assertEquals(-1, task.checkForEarlyWake(2000, 1000)); + + final long now = System.currentTimeMillis(); + + assertEquals(1000, task.checkForEarlyWake(now, now + 1000)); + + } + + + @Test + public void testWhatIsNextExecutionTime() { + assertEquals(2000, task.whatIsNextExecutionTime(1000, 1001)); + assertEquals(2000, task.whatIsNextExecutionTime(1000, 1499)); + assertEquals(3000, task.whatIsNextExecutionTime(1000, 1500)); + assertEquals(3000, task.whatIsNextExecutionTime(1000, 1501)); + + assertEquals(1000, task.whatIsNextExecutionTime(0, 0)); + assertEquals(1000, task.whatIsNextExecutionTime(0, 100)); + assertEquals(2000, task.whatIsNextExecutionTime(0, 1000)); + + assertEquals(2000, task.whatIsNextExecutionTime(1000, 1010)); + assertEquals(2100, task.whatIsNextExecutionTime(1100, 1)); + assertEquals(2110, task.whatIsNextExecutionTime(1110, 1)); + assertEquals(2111, task.whatIsNextExecutionTime(1111, 1)); + + assertEquals(4000, task.whatIsNextExecutionTime(1000, 3000)); + assertEquals(4000, task.whatIsNextExecutionTime(1000, 2999)); + + final long now = System.currentTimeMillis(); + + assertEquals(now + 1000, task.whatIsNextExecutionTime(now, now)); + assertEquals(now + 1000, task.whatIsNextExecutionTime(now, now - 1000)); + assertEquals(now, task.whatIsNextExecutionTime(now - 1000, now - 1000)); + assertEquals(now + 1000, task.whatIsNextExecutionTime(now - 1000, now)); + + assertEquals(now + 2000, task.whatIsNextExecutionTime(now, now + 1000)); + assertEquals(now + 2000, task.whatIsNextExecutionTime(now + 1000, now)); + assertEquals(now + 2000, task.whatIsNextExecutionTime(now + 1000, now + 1000)); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/poll/LongPollServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/poll/LongPollServiceTest.java new file mode 100644 index 0000000..c062fad --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/poll/LongPollServiceTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.poll; + +import static com.intergral.deep.agent.types.TracePointConfig.SINGLE_FRAME_TYPE; +import static com.intergral.deep.agent.types.TracePointConfig.STACK; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.grpc.GrpcService; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import com.intergral.deep.agent.tracepoint.inst.TracepointInstrumentationService; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.poll.v1.PollRequest; +import com.intergral.deep.proto.poll.v1.PollResponse; +import com.intergral.deep.proto.poll.v1.ResponseType; +import com.intergral.deep.proto.tracepoint.v1.TracePointConfig; +import com.intergral.deep.tests.grpc.TestPollService; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class LongPollServiceTest { + + + private Server server; + private LongPollService longPollService; + + private PollRequest request; + private PollResponse response; + private Throwable responseError; + + @BeforeEach + void setUp() throws IOException { + final TestPollService testPollService = new TestPollService((req, responseObserver) -> { + request = req; + if (responseError != null) { + responseObserver.onError(responseError); + } else { + responseObserver.onNext(response); + } + responseObserver.onCompleted(); + }); + + server = ServerBuilder.forPort(9999).addService(testPollService.bindService()).build(); + + server.start(); + + final HashMap agentArgs = new HashMap<>(); + agentArgs.put(ISettings.KEY_SERVICE_URL, "localhost:9999"); + agentArgs.put(ISettings.KEY_SERVICE_SECURE, "false"); + final Settings settings = Settings.build(agentArgs); + settings.setResource(Resource.create(Collections.singletonMap("test", "resource"))); + final GrpcService grpcService = new GrpcService(settings); + longPollService = new LongPollService(settings, grpcService); + } + + @AfterEach + void tearDown() { + server.shutdownNow(); + } + + @Test + void canPollServer() { + final TracepointConfigService configService = mock(TracepointConfigService.class); + + longPollService.setTracepointConfig(configService); + longPollService.run(100); + + verify(configService).noChange(0L); + } + + @Test + void callsNoChangeOnResponse() { + final TracepointConfigService configService = mock(TracepointConfigService.class); + longPollService.setTracepointConfig(configService); + + response = PollResponse.newBuilder().setResponseType(ResponseType.NO_CHANGE).build(); + + longPollService.run(100); + + verify(configService).noChange(0L); + } + + @Test + void callsUpdateOnResponse() { + final TracepointConfigService configService = mock(TracepointConfigService.class); + longPollService.setTracepointConfig(configService); + + response = PollResponse.newBuilder().setResponseType(ResponseType.UPDATE).build(); + + longPollService.run(100); + + verify(configService, never()).noChange(0L); + } + + @Test + void propagateHashOnNextCall() { + final TracepointInstrumentationService instrumentationService = mock(TracepointInstrumentationService.class); + + final TracepointConfigService configService = spy(new TracepointConfigService(instrumentationService)); + doCallRealMethod().when(configService).configUpdate(Mockito.anyLong(), Mockito.eq("123"), Mockito.anyCollection()); + longPollService.setTracepointConfig(configService); + + response = PollResponse.newBuilder().setResponseType(ResponseType.UPDATE).setCurrentHash("123").build(); + + longPollService.run(100); + + assertEquals("", request.getCurrentHash()); + assertEquals(100, request.getTsNanos()); + + response = PollResponse.newBuilder().setResponseType(ResponseType.UPDATE).setCurrentHash("321").build(); + + longPollService.run(101); + + assertEquals("123", request.getCurrentHash()); + assertEquals(101, request.getTsNanos()); + verify(instrumentationService, times(2)).processBreakpoints(Mockito.anyCollection()); + } + + @Test + void canHandleTracepointResponse() { + final TracepointInstrumentationService instrumentationService = mock(TracepointInstrumentationService.class); + + final TracepointConfigService configService = spy(new TracepointConfigService(instrumentationService)); + doCallRealMethod().when(configService).configUpdate(Mockito.anyLong(), Mockito.eq("123"), Mockito.anyCollection()); + longPollService.setTracepointConfig(configService); + + response = PollResponse.newBuilder().setResponseType(ResponseType.UPDATE).setCurrentHash("123").addResponse( + TracePointConfig.newBuilder().setPath("/some/file/path.py").setLineNumber(123).setID("tp-1") + .putAllArgs(Collections.singletonMap("key", "value")).addWatches("i watch").addTargeting( + KeyValue.newBuilder().setKey("key") + .setValue(AnyValue.newBuilder().setStringValue("some string").build()).build()).build()) + .build(); + + longPollService.run(100); + + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + verify(instrumentationService).processBreakpoints(captor.capture()); + + final Collection value = captor.getValue(); + + assertEquals(1, value.size()); + + final com.intergral.deep.agent.types.TracePointConfig next = value.iterator().next(); + + assertEquals("tp-1", next.getId()); + assertEquals("/some/file/path.py", next.getPath()); + assertEquals(123, next.getLineNo()); + assertArrayEquals(new String[]{"i watch"}, next.getWatches().toArray()); + assertEquals(1, next.getFireCount()); + assertNull(next.getCondition()); + assertEquals(SINGLE_FRAME_TYPE, next.getFrameType()); + assertEquals(STACK, next.getStackType()); + } + + @Test + void doesSendResourceOnRequest() { + final TracepointInstrumentationService instrumentationService = mock(TracepointInstrumentationService.class); + final TracepointConfigService configService = spy(new TracepointConfigService(instrumentationService)); + longPollService.setTracepointConfig(configService); + + longPollService.run(100); + + assertNotNull(request.getResource()); + assertEquals("test", request.getResource().getAttributes(0).getKey()); + assertEquals("resource", request.getResource().getAttributes(0).getValue().getStringValue()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/push/PushServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/push/PushServiceTest.java new file mode 100644 index 0000000..010a8a6 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/push/PushServiceTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.push; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.grpc.GrpcService; +import com.intergral.deep.agent.push.PushService.LoggingObserver; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.tracepoint.v1.Snapshot; +import com.intergral.deep.proto.tracepoint.v1.SnapshotServiceGrpc.SnapshotServiceStub; +import com.intergral.deep.test.MockEventSnapshot; +import java.util.Collections; +import java.util.HashMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class PushServiceTest { + + private PushService pushService; + private SnapshotServiceStub snapshotServiceStub; + private Settings settings; + + @BeforeEach + void setUp() { + final GrpcService grpcService = Mockito.mock(GrpcService.class); + snapshotServiceStub = Mockito.mock(SnapshotServiceStub.class); + when(grpcService.snapshotService()).thenReturn(snapshotServiceStub); + final HashMap map = new HashMap<>(); + + settings = Settings.build(map); + pushService = new PushService(settings, grpcService); + } + + @Test + void snapshotLogger() { + final LoggingObserver observer = new LoggingObserver("test id"); + assertDoesNotThrow(() -> observer.onError(new RuntimeException("test exception"))); + assertDoesNotThrow(() -> observer.onNext(null)); + assertDoesNotThrow(observer::onCompleted); + } + + @Test + void canPushSnapshot() { + final ISnapshotContext context = Mockito.mock(ISnapshotContext.class); + pushService.pushSnapshot(new MockEventSnapshot(), context); + + verify(snapshotServiceStub).send(any(), any()); + } + + @Test + void doesDecorate() { + settings.addPlugin(new TestDecorator()); + + final ISnapshotContext context = Mockito.mock(ISnapshotContext.class); + pushService.pushSnapshot(new MockEventSnapshot(), context); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Snapshot.class); + verify(snapshotServiceStub).send(argumentCaptor.capture(), any()); + + final Snapshot value = argumentCaptor.getValue(); + + assertNotNull(value); + + final KeyValue attributes = value.getAttributes(0); + assertEquals("decorated", attributes.getKey()); + assertEquals("value", attributes.getValue().getStringValue()); + } + + public static class TestDecorator implements IPlugin { + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + return Resource.create(Collections.singletonMap("decorated", "value")); + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/push/PushUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/push/PushUtilsTest.java new file mode 100644 index 0000000..a388292 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/push/PushUtilsTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.push; + +import static com.intergral.deep.tests.AssertUtils.assertContains; +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 com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.tracepoint.v1.Snapshot; +import com.intergral.deep.test.MockEventSnapshot; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PushUtilsTest { + + @Test + void canProcessSnapshot() { + final MockEventSnapshot eventSnapshot = new MockEventSnapshot(); + final Snapshot snapshot = PushUtils.convertToGrpc(eventSnapshot); + assertEquals(eventSnapshot.getID(), snapshot.getID().toStringUtf8()); + + assertEquals("tp-1", snapshot.getTracepoint().getID()); + assertEquals("/some/file/path.py", snapshot.getTracepoint().getPath()); + assertEquals(123, snapshot.getTracepoint().getLineNumber()); + } + + @Test + void canHandleFrames() { + final MockEventSnapshot mockEventSnapshot = new MockEventSnapshot().withFrames(); + final Snapshot snapshot = PushUtils.convertToGrpc(mockEventSnapshot); + + assertEquals("class_name_1", snapshot.getFrames(0).getClassName()); + assertEquals("class_name_2", snapshot.getFrames(1).getClassName()); + assertEquals("class_name_3", snapshot.getFrames(2).getClassName()); + assertEquals("class_name_4", snapshot.getFrames(3).getClassName()); + + assertTrue(snapshot.getFrames(0).getNativeFrame()); + assertTrue(snapshot.getFrames(0).getAppFrame()); + + assertFalse(snapshot.getFrames(3).getNativeFrame()); + assertFalse(snapshot.getFrames(3).getAppFrame()); + + assertEquals(3, snapshot.getFrames(3).getVariablesCount()); + + assertEquals("someLocalVar", snapshot.getFrames(3).getVariables(0).getName()); + assertEquals("private", snapshot.getFrames(3).getVariables(1).getModifiers(0)); + assertEquals("someOtherName", snapshot.getFrames(3).getVariables(2).getOriginalName()); + } + + @Test + void canConvertAllTypes() { + final MockEventSnapshot mockEventSnapshot = new MockEventSnapshot().withAttributes(); + final Snapshot snapshot = PushUtils.convertToGrpc(mockEventSnapshot); + + final List attributesList = new ArrayList<>(snapshot.getAttributesList()); + attributesList.remove(assertContains(attributesList, (item) -> item.getValue().getIntValue() == 123L)); + attributesList.remove(assertContains(attributesList, (item) -> item.getValue().getIntValue() == 123)); + attributesList.remove(assertContains(attributesList, (item) -> item.getValue().getDoubleValue() == 3.21F)); + attributesList.remove(assertContains(attributesList, (item) -> item.getValue().getDoubleValue() == 1.23D)); + attributesList.remove(assertContains(attributesList, (item) -> !item.getValue().getBoolValue())); + attributesList.remove(assertContains(attributesList, (item) -> "123l".equals(item.getValue().getStringValue()))); + + assertEquals(0, attributesList.size()); + } + + @Test + void canConvertWatches() { + final MockEventSnapshot mockEventSnapshot = new MockEventSnapshot().withWatches(); + final Snapshot snapshot = PushUtils.convertToGrpc(mockEventSnapshot); + + assertEquals("error", snapshot.getWatches(0).getExpression()); + assertEquals("this is an error", snapshot.getWatches(0).getErrorResult()); + assertEquals("good", snapshot.getWatches(1).getExpression()); + assertEquals("withName", snapshot.getWatches(1).getGoodResult().getName()); + } + + @Test + void canConvertVariables() { + final MockEventSnapshot mockEventSnapshot = new MockEventSnapshot().withVariables(); + final Snapshot snapshot = PushUtils.convertToGrpc(mockEventSnapshot); + + assertEquals("value_1", snapshot.getVarLookupOrThrow("1").getValue()); + assertFalse(snapshot.getVarLookupOrThrow("1").getTruncated()); + assertEquals("value_2", snapshot.getVarLookupOrThrow("2").getValue()); + assertTrue(snapshot.getVarLookupOrThrow("2").getTruncated()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/resource/ResourceDetectorTest.java b/agent/src/test/java/com/intergral/deep/agent/resource/ResourceDetectorTest.java new file mode 100644 index 0000000..4089e05 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/resource/ResourceDetectorTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.settings.Settings; +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +class ResourceDetectorTest { + + @Test + void loadsResource() { + final HashMap agentArgs = new HashMap<>(); + agentArgs.put(ResourceDetector.ENABLED_PROVIDERS_KEY, ""); + agentArgs.put(ResourceDetector.DISABLED_PROVIDERS_KEY, JavaResourceDetector.class.getName()); + final Settings settings = Settings.build(agentArgs); + final Resource resource = ResourceDetector.configureResource(settings, getClass().getClassLoader()); + + assertNotNull(resource); + assertEquals(0, resource.getAttributes().size()); + } + + @Test + void loadResourceFromSettings() { + final Settings settings = Settings.build(new HashMap<>()); + final Resource resource = ResourceDetector.configureResource(settings, getClass().getClassLoader()); + assertEquals(System.getProperty("java.version"), resource.getAttributes().get("java_version")); + } + + @Test + void loadResourceFromConfig() { + final HashMap agentArgs = new HashMap<>(); + agentArgs.put(ResourceDetector.ATTRIBUTE_PROPERTY, "key=value,other=thing"); + final Settings settings = Settings.build(agentArgs); + final Resource resource = ResourceDetector.configureResource(settings, getClass().getClassLoader()); + + assertEquals("value", resource.getAttributes().get("key")); + assertEquals("thing", resource.getAttributes().get("other")); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/resource/SpiUtilTest.java b/agent/src/test/java/com/intergral/deep/agent/resource/SpiUtilTest.java new file mode 100644 index 0000000..8d5062c --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/resource/SpiUtilTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.resource; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.intergral.deep.agent.api.spi.ResourceProvider; +import java.util.List; +import org.junit.jupiter.api.Test; + +class SpiUtilTest { + + @Test + void loadsOrderedSpi() { + ResourceProvider spi1 = mock(ResourceProvider.class); + ResourceProvider spi2 = mock(ResourceProvider.class); + ResourceProvider spi3 = mock(ResourceProvider.class); + + when(spi1.order()).thenReturn(2); + when(spi2.order()).thenReturn(0); + when(spi3.order()).thenReturn(1); + + SpiUtil.ServiceLoaderFinder mockFinder = mock(SpiUtil.ServiceLoaderFinder.class); + when(mockFinder.load(ResourceProvider.class, SpiUtil.class.getClassLoader())) + .thenReturn(asList(spi1, spi2, spi3)); + + List loadedSpi = SpiUtil.loadOrdered(ResourceProvider.class, getClass().getClassLoader(), mockFinder); + + assertEquals(3, loadedSpi.size()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java new file mode 100644 index 0000000..9ce7094 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.settings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.settings.Settings.InvalidConfigException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class SettingsTest { + + @Test + void canHandleTypes() { + final HashMap agentArgs = new HashMap<>(); + agentArgs.put("string", "string"); + agentArgs.put("number", "1.2"); + agentArgs.put("list", "one,two"); + agentArgs.put("map", "key=one,other=two"); + agentArgs.put("regex", "intergral(s)"); + agentArgs.put("url", "https://google.com"); + agentArgs.put("debug", "DEBUG"); + agentArgs.put("debug_2", "debug"); + agentArgs.put("level", "FINE"); + + final Settings settings = Settings.build(agentArgs); + + assertEquals("string", settings.getSettingAs("string", String.class)); + assertEquals(1, settings.getSettingAs("number", int.class)); + assertEquals(1L, settings.getSettingAs("number", long.class)); + assertEquals(1.2f, settings.getSettingAs("number", float.class)); + assertEquals(1.2d, settings.getSettingAs("number", double.class)); + + assertEquals("one", settings.getSettingAs("list", List.class).get(0)); + assertEquals("one", settings.getAsList("list").get(0)); + + assertEquals("two", settings.getSettingAs("map", Map.class).get("other")); + assertEquals("two", settings.getMap("map").get("other")); + + assertEquals("intergral(s)", settings.getSettingAs("regex", Pattern.class).pattern()); + assertEquals("https://google.com", settings.getSettingAs("url", URL.class).toString()); + assertEquals(Level.FINEST, settings.getSettingAs("debug", Level.class)); + assertEquals(Level.FINEST, settings.getSettingAs("debug_2", Level.class)); + assertEquals(Level.FINE, settings.getSettingAs("level", Level.class)); + } + + @Test + void canHandleURLs() { + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "://bad-url")); + + final InvalidConfigException invalidConfigException = assertThrows(InvalidConfigException.class, settings::getServiceHost); + assertEquals(MalformedURLException.class, invalidConfigException.getCause().getClass()); + assertThrows(InvalidConfigException.class, settings::getServicePort); + } + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "https://google.com")); + + assertEquals("google.com", settings.getServiceHost()); + assertEquals(-1, settings.getServicePort()); + } + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "http://google.com:80")); + + assertEquals("google.com", settings.getServiceHost()); + assertEquals(80, settings.getServicePort()); + } + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "google.com:443")); + + assertEquals("google.com", settings.getServiceHost()); + assertEquals(443, settings.getServicePort()); + } + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "google.com:80")); + + assertEquals("google.com", settings.getServiceHost()); + assertEquals(80, settings.getServicePort()); + } + { + final Settings settings = Settings.build(Collections.singletonMap(Settings.KEY_SERVICE_URL, "google.com")); + + assertThrows(InvalidConfigException.class, settings::getServiceHost); + assertThrows(InvalidConfigException.class, settings::getServicePort); + } + } + + @Test + void plugins() { + + final Settings settings = Settings.build(new HashMap<>()); + + final IPlugin plugin = new IPlugin() { + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + return null; + } + }; + settings.addPlugin(plugin); + final IllegalStateException illegalStateException = assertThrows(IllegalStateException.class, () -> settings.addPlugin(plugin)); + assertEquals("Cannot add duplicate named (com.intergral.deep.agent.settings.SettingsTest$1) plugin", + illegalStateException.getMessage()); + + assertEquals(1, settings.getPlugins().size()); + settings.removePlugin(plugin); + + assertEquals(0, settings.getPlugins().size()); + } + + @Test + void setActive() { + + final Settings settings = Settings.build(new HashMap<>()); + + assertTrue(settings.isActive()); + settings.setActive(false); + assertFalse(settings.isActive()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointConfigServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointConfigServiceTest.java new file mode 100644 index 0000000..16e0374 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointConfigServiceTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.agent.tracepoint.inst.TracepointInstrumentationService; +import com.intergral.deep.agent.types.TracePointConfig; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class TracepointConfigServiceTest { + + private TracepointInstrumentationService instrumentationService; + private TracepointConfigService tracepointConfigService; + + @BeforeEach + void setUp() { + instrumentationService = Mockito.mock(TracepointInstrumentationService.class); + tracepointConfigService = new TracepointConfigService(instrumentationService); + } + + @Test + void noChange() { + tracepointConfigService.configUpdate(1010110, "hash", Collections.emptyList()); + assertEquals(1010110, tracepointConfigService.lastUpdate()); + assertEquals("hash", tracepointConfigService.currentHash()); + Mockito.verify(instrumentationService, Mockito.times(1)).processBreakpoints(Mockito.anyCollection()); + + tracepointConfigService.noChange(202020); + assertEquals(202020, tracepointConfigService.lastUpdate()); + assertEquals("hash", tracepointConfigService.currentHash()); + Mockito.verify(instrumentationService, Mockito.times(1)).processBreakpoints(Mockito.anyCollection()); + } + + @Test + void customCallsUpdate() { + // add a custom tracepoint will call instrumentation update + final TracePointConfig tracePointConfig = tracepointConfigService.addCustom("/path", 123, Collections.emptyMap(), + Collections.emptyList()); + + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + + Mockito.verify(instrumentationService, Mockito.times(1)).processBreakpoints(captor.capture()); + + final Collection value = captor.getValue(); + assertEquals(1, value.size()); + final TracePointConfig next = value.iterator().next(); + + assertEquals(tracePointConfig.getId(), next.getId()); + + // remove a custom tracepoint will call instrumentation update + tracepointConfigService.removeCustom(tracePointConfig); + + Mockito.verify(instrumentationService, Mockito.times(2)).processBreakpoints(captor.capture()); + + final Collection captorValue = captor.getValue(); + assertEquals(0, captorValue.size()); + + // remove a tp that doesn't exist will not call instrumentation update + tracepointConfigService.removeCustom(tracePointConfig); + + Mockito.verify(instrumentationService, Mockito.times(2)).processBreakpoints(Mockito.anyCollection()); + } + + @Test + void canLoadTracepointConfigs() { + + final TracePointConfig tracePointConfig1 = tracepointConfigService.addCustom("/path", 123, Collections.emptyMap(), + Collections.emptyList()); + final TracePointConfig tracePointConfig2 = tracepointConfigService.addCustom("/path", 123, Collections.emptyMap(), + Collections.emptyList()); + final TracePointConfig tracePointConfig3 = tracepointConfigService.addCustom("/path", 123, Collections.emptyMap(), + Collections.emptyList()); + + { + final Collection tracePointConfigs = tracepointConfigService.loadTracepointConfigs( + Collections.singletonList(tracePointConfig1.getId())); + assertEquals(1, tracePointConfigs.size()); + assertEquals(tracePointConfig1.getId(), tracePointConfigs.iterator().next().getId()); + } + + { + final Collection tracePointConfigs = tracepointConfigService.loadTracepointConfigs( + Collections.singletonList(tracePointConfig2.getId())); + assertEquals(1, tracePointConfigs.size()); + assertEquals(tracePointConfig2.getId(), tracePointConfigs.iterator().next().getId()); + } + + { + final Collection tracePointConfigs = tracepointConfigService.loadTracepointConfigs( + Arrays.asList(tracePointConfig2.getId(), tracePointConfig3.getId())); + assertEquals(2, tracePointConfigs.size()); + assertArrayEquals(tracePointConfigs.stream().map(TracePointConfig::getId).toArray(), + new String[]{tracePointConfig2.getId(), tracePointConfig3.getId()}); + } + } + + @Test + void configUpdate() { + // TP config is passed to instrumentation + tracepointConfigService.configUpdate(10101, "hash", + Collections.singletonList(new TracePointConfig("some-id", "path", 123, Collections.emptyMap(), Collections.emptyList()))); + + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + + Mockito.verify(instrumentationService, Mockito.times(1)).processBreakpoints(captor.capture()); + + { + final Collection value = captor.getValue(); + assertEquals(1, value.size()); + assertEquals("some-id", value.iterator().next().getId()); + } + + // custom and config are passed to instrumentation + final TracePointConfig customTp = tracepointConfigService.addCustom("path", 123, Collections.emptyMap(), + Collections.emptyList()); + + Mockito.verify(instrumentationService, Mockito.times(2)).processBreakpoints(captor.capture()); + + { + final Collection tracePointConfigs = captor.getValue(); + assertEquals(2, tracePointConfigs.size()); + + assertArrayEquals(tracePointConfigs.stream().map(TracePointConfig::getId).toArray(), + new String[]{"some-id", customTp.getId()}); + } + + //custom remains after update + tracepointConfigService.configUpdate(10101, "hash", + Collections.singletonList(new TracePointConfig("some-id", "path", 123, Collections.emptyMap(), Collections.emptyList()))); + + Mockito.verify(instrumentationService, Mockito.times(3)).processBreakpoints(captor.capture()); + + { + final Collection tracePointConfigs = captor.getValue(); + assertEquals(2, tracePointConfigs.size()); + + assertArrayEquals(tracePointConfigs.stream().map(TracePointConfig::getId).toArray(), + new String[]{"some-id", customTp.getId()}); + } + // config remains after custom removed + tracepointConfigService.removeCustom(customTp); + + Mockito.verify(instrumentationService, Mockito.times(4)).processBreakpoints(captor.capture()); + + { + final Collection tracePointConfigs = captor.getValue(); + assertEquals(1, tracePointConfigs.size()); + + assertArrayEquals(tracePointConfigs.stream().map(TracePointConfig::getId).toArray(), + new String[]{"some-id"}); + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointUtilsTest.java new file mode 100644 index 0000000..db5bef7 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/TracepointUtilsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.test.MockTracepointConfig; +import org.junit.jupiter.api.Test; + +class TracepointUtilsTest { + + @Test + void estimatedClassRoot() { + assertEquals("com/intergral/class/name", + TracepointUtils.estimatedClassRoot(new MockTracepointConfig().withArg("class_name", "com.intergral.class.name"))); + assertEquals("some", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("some.java"))); + assertEquals("src/some", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("/src/some.java"))); + assertEquals("some", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("/src/some.java").withArg("src_root", "/src"))); + assertEquals("some", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("some.java").withArg("src_root", "src/main/java"))); + + assertEquals("cfm", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("some.cfm").withArg("src_root", "src/"))); + assertEquals("cfm", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("some.cfc").withArg("src_root", "src/"))); + + assertEquals("jsp", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("some.jsp").withArg("src_root", "src/"))); + + assertEquals("hello/world/HelloController", TracepointUtils.estimatedClassRoot( + new MockTracepointConfig("/src/main/java/hello/world/HelloController.java").withArg("src_root", "/src/main/java"))); + assertEquals("hello/world/HelloController", + TracepointUtils.estimatedClassRoot(new MockTracepointConfig("/src/main/java/hello/world/HelloController.java"))); + + assertEquals("docker/Dockerfile", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("/docker/Dockerfile"))); + assertEquals(".gitignore", TracepointUtils.estimatedClassRoot(new MockTracepointConfig("/.gitignore"))); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluatorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluatorTest.java new file mode 100644 index 0000000..667e219 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFEvaluatorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.cf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.tracepoint.cf.CFEvaluator.Loader; +import java.util.HashMap; +import java.util.Map; +import lucee.runtime.PageContextImpl; +import lucee.runtime.PageImpl; +import org.junit.jupiter.api.Test; + +class CFEvaluatorTest { + + @Test + void coverage() throws NoSuchMethodException { + final CFEvaluator evaluate = new CFEvaluator(this, this.getClass().getDeclaredMethod("evaluate", String.class)); + assertEquals("input", evaluate.evaluateExpression("input", new HashMap<>())); + assertNull(evaluate.evaluateExpression("error", new HashMap<>())); + } + + @Test + void loader() { + final Map hashMap = new HashMap<>(); + hashMap.put("this", new PageImpl.SomePageImpl()); + hashMap.put("param0", new PageContextImpl()); + final Loader loader = new Loader(hashMap); + final IEvaluator load = loader.load(); + assertNotNull(load); + } + + public Object evaluate(String input) { + if (input.equals("error")) { + throw new RuntimeException("test exception"); + } + return input; + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFUtilsTest.java new file mode 100644 index 0000000..ad93da6 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/cf/CFUtilsTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.cf; + +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import coldfusion.runtime.ArgumentCollection; +import coldfusion.runtime.TestScope; +import coldfusion.runtime.UDFMethod; +import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.types.TracePointConfig; +import com.intergral.deep.test.MockTracepointConfig; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import lucee.runtime.PageContextImpl; +import lucee.runtime.PageImpl; +import org.junit.jupiter.api.Test; + +class CFUtilsTest { + + @Test + void isCfClassFile() { + assertTrue(CFUtils.isCFFile("somefile.cfm")); + assertTrue(CFUtils.isCFFile("somefile.cfc")); + assertTrue(CFUtils.isCFFile("somefile.cfml")); + + assertFalse(CFUtils.isCFFile("somefile.cfml.java")); + assertFalse(CFUtils.isCFFile("somefile_cfml.java")); + assertFalse(CFUtils.isCFFile("somefile_cfc.java")); + assertFalse(CFUtils.isCFFile("somefile_cfm.java")); + } + + @Test + void isCfFile() { + assertTrue(CFUtils.isCFFile("something.cfc")); + assertTrue(CFUtils.isCFFile("something.cfm")); + assertTrue(CFUtils.isCFFile("something.cf")); + assertTrue(CFUtils.isCFFile("something.cfml")); + assertFalse(CFUtils.isCFFile("something.c")); + } + + + @Test + void findLuceeEval() throws Throwable { + final Map hashMap = new HashMap<>(); + hashMap.put("this", new PageImpl.SomePageImpl()); + hashMap.put("param0", new PageContextImpl()); + final IEvaluator cfEval = CFUtils.findCfEval(hashMap); + + assertNotNull(cfEval); + + final Object some_expression = cfEval.evaluateExpression("some Expression", hashMap); + assertEquals("some Expression", some_expression); + + } + + + @Test + void findCfEval() throws Throwable { + final Map hashMap = new HashMap<>(); + hashMap.put("this", new CFEvaluatorTarget()); + final IEvaluator cfEval = CFUtils.findCfEval(hashMap); + assertNotNull(cfEval); + + final Object test = cfEval.evaluateExpression("test", hashMap); + assertEquals(CFEvaluatorTarget.class.getName(), test.toString()); + } + + + @Test + void findCfEval2() throws Throwable { + final Map hashMap = new HashMap<>(); + hashMap.put("this", new CFEvaluatorTarget2()); + final IEvaluator cfEval = CFUtils.findCfEval(hashMap); + assertNotNull(cfEval); + + final Object test = cfEval.evaluateExpression("test", hashMap); + assertEquals(CFEvaluatorTarget2.class.getName(), test.toString()); + } + + + @Test + void findCfEval3() { + final Map hashMap = new HashMap<>(); + final IEvaluator cfEval = CFUtils.findCfEval(hashMap); + assertNull(cfEval); + } + + + @Test + void findCfEval4() { + final Map hashMap = new HashMap<>(); + hashMap.put("this", this); + final IEvaluator cfEval = CFUtils.findCfEval(hashMap); + assertNull(cfEval); + } + + + @Test + void findUDFName() { + assertNull(CFUtils.findUdfName(Collections.emptyMap(), "someClassName", 1)); + assertEquals("FINDFILES", CFUtils.findUdfName(Collections.emptyMap(), "cf123546123$funcFINDFILES", 1)); + final Map hashMap = new HashMap<>(); + hashMap.put("this", this); + assertNull(CFUtils.findUdfName(hashMap, "cf123546123$funcFINDFILES", 0)); + + hashMap.put("this", new CfUdfTest("findUDFName")); + assertEquals("findUDFName", CFUtils.findUdfName(hashMap, "cf123546123$funcFINDFILES", 0)); + } + + + @Test + void isScope() { + assertFalse(CFUtils.isScope(this)); + assertFalse(CFUtils.isScope("this")); + assertFalse(CFUtils.isScope(new HashMap<>())); + + assertTrue(CFUtils.isScope(new TestScope())); + assertTrue(CFUtils.isScope(new ArgumentCollection())); + } + + + @Test + void findPage() { + final Map map = new HashMap<>(); + assertNull(CFUtils.findPage(map)); + + map.put("this", this); + assertSame(this, CFUtils.findPage(map)); + + map.put("this", new CfUdfTest("findPage")); + + assertNull(CFUtils.findPage(map)); + + map.put("parentPage", this); + assertSame(this, CFUtils.findPage(map)); + } + + + @Test + void findPageContext() { + final Map map = new HashMap<>(); + assertNull(CFUtils.findPageContext(map)); + + final Object pageContext = new Object(); + map.put("this", new PageContextTest(pageContext)); + assertNotNull(CFUtils.findPageContext(map)); + assertSame(pageContext, CFUtils.findPageContext(map)); + } + + + @Test + void isCfClass() { + assertFalse(CFUtils.isCfClass("any/random/class")); + assertFalse(CFUtils.isCfClass("any.random.class")); + assertFalse(CFUtils.isCfClass("anyclass")); + + assertTrue(CFUtils.isCfClass("cf123546123$funcFINDFILES")); + assertTrue(CFUtils.isCfClass("tests.now_cfm$cf")); + } + + @Test + void guessSource() { + assertNull(CFUtils.guessSource("cf123546123$funcFINDFILES")); + assertEquals("tests/now.cfm", CFUtils.guessSource("tests.now_cfm$cf")); + } + + @Test + void loadCfTracepoints() { + final Set tracePointConfigs = CFUtils.loadCfTracepoints("some/file.cfm", + Collections.singletonMap("cfm", new MockTracepointConfig("some/file.cfm"))); + assertEquals(1, tracePointConfigs.size()); + } + + public static class CFEvaluatorTarget { + + public String Evaluate(String expr) { + return CFEvaluatorTarget.class.getName(); + } + } + + + public static class CFEvaluatorTarget2 { + + public String Evaluate(Object expr) { + return CFEvaluatorTarget2.class.getName(); + } + } + + + public static class CfUdfTest extends UDFMethod { + + public CfUdfTest(final String key) { + super(key); + } + } + + + public static class PageContextTest { + + private final Object pageContext; + + + public PageContextTest(final Object pageContext) { + this.pageContext = pageContext; + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorServiceTest.java new file mode 100644 index 0000000..d2a329b --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/EvaluatorServiceTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.evaluator; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.intergral.deep.agent.api.plugin.IEvaluator; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class EvaluatorServiceTest { + + @Test + void coverage() throws Throwable { + final IEvaluator evaluator; + try (MockedStatic nashorn = Mockito.mockStatic(NashornReflectEvaluator.class, "loadEvaluator")) { + nashorn.when(() -> NashornReflectEvaluator.loadEvaluator(Mockito.any())).thenReturn(null); + evaluator = EvaluatorService.createEvaluator(); + } + + assertTrue(evaluator.evaluate(null, null)); + assertThrows(RuntimeException.class, () -> evaluator.evaluateExpression(null, null)); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluatorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluatorTest.java index bf33717..b9f0572 100644 --- a/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluatorTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/evaluator/NashornReflectEvaluatorTest.java @@ -19,8 +19,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import com.intergral.deep.Person; import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.test.target.Person; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,15 +46,33 @@ void evalExpression() throws Throwable { assertEquals("bob", String.valueOf(o)); } + @Test + void parseExpression() { + assertEquals("deep_this", NashornReflectEvaluator.parseExpression("this")); + assertEquals("deep_this", NashornReflectEvaluator.parseExpression(" this")); + assertEquals("deep_this", NashornReflectEvaluator.parseExpression(" this ")); + assertEquals("deep_this", NashornReflectEvaluator.parseExpression("this ")); + + assertEquals("deep_this.some.path.to.value", NashornReflectEvaluator.parseExpression("this.some.path.to.value")); + assertEquals("deep_this.someFunction()", NashornReflectEvaluator.parseExpression("this.someFunction()")); + assertEquals("max(deep_this.someFunction(), 123)", NashornReflectEvaluator.parseExpression("max(this.someFunction(), 123)")); + assertEquals("101 - deep_this.someFunction()", NashornReflectEvaluator.parseExpression("101 - this.someFunction()")); + + assertEquals("101 - deep_this.someFunctionWithThisInTt()", + NashornReflectEvaluator.parseExpression("101 - this.someFunctionWithThisInTt()")); + // todo this is a known issue + // assertEquals("101 - deep_this.thisFunction()", NashornReflectEvaluator.parseExpression("101 - this.thisFunction()")); + } + /** * This test is more of a note for using nashorn and allows for testing features etc. */ @Test @Disabled void nashornRandomUsage() throws Exception { - final HashMap hashMap = new HashMap() {{ - put("name", "ben"); - }}; + final HashMap hashMap = new HashMap<>(); + hashMap.put("name", "ben"); + javax.script.ScriptEngineManager mgr = new javax.script.ScriptEngineManager(); final List engineFactories = mgr.getEngineFactories(); @@ -64,18 +82,18 @@ void nashornRandomUsage() throws Exception { javax.script.ScriptEngine engine = mgr.getEngineByName("JavaScript"); final javax.script.Bindings bindings = engine.createBindings(); -// bindings.put( "obj", hashMap ); -// bindings.put( "person", new Person( new Person( "mary" ), "bob" ) ); - bindings.putAll(new HashMap() {{ - put("obj", hashMap); - put("person", new Person(new Person("mary"), "qwe")); - put("qq", NashornReflectEvaluatorTest.this); - }}); + // bindings.put( "obj", hashMap ); + // bindings.put( "person", new Person( new Person( "mary" ), "bob" ) ); + final HashMap toMerge = new HashMap<>(); + toMerge.put("obj", hashMap); + toMerge.put("person", new Person(new Person("mary"), "qwe")); + toMerge.put("qq", NashornReflectEvaluatorTest.this); + bindings.putAll(toMerge); System.out.println(engine.eval("person", bindings)); System.out.println(engine.eval("person.name", bindings)); System.out.println(engine.eval("person.getParent()", bindings)); -// System.out.println( engine.eval( "System.exit()", bindings ) ); + // System.out.println( engine.eval( "System.exit()", bindings ) ); final Object eval = engine.eval("qq", bindings); System.out.println(eval); System.out.println(engine.eval("qq.name", bindings)); diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameCollectorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameCollectorTest.java new file mode 100644 index 0000000..e9eced9 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameCollectorTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.handler.FrameCollector.IExpressionResult; +import com.intergral.deep.agent.types.snapshot.Variable; +import com.intergral.deep.test.MockTracepointConfig; +import com.intergral.deep.test.target.ConditionTarget; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class FrameCollectorTest { + + private Settings settings = Mockito.mock(Settings.class); + private IEvaluator evaluator = Mockito.mock(IEvaluator.class); + private FrameCollector frameCollector; + + @BeforeEach + void setUp() { + Mockito.when(settings.getResource()).thenReturn(Resource.DEFAULT); + frameCollector = new FrameCollector(settings, evaluator, Collections.singletonMap("this", new ConditionTarget()), + Thread.currentThread().getStackTrace()); + frameCollector.configureSelf(Collections.singletonList(new MockTracepointConfig())); + } + + @Test + void evaluateWatchers() throws Throwable { + Mockito.when(evaluator.evaluateExpression(Mockito.anyString(), Mockito.anyMap())).thenReturn("some result"); + final IExpressionResult someExpression = frameCollector.evaluateWatchExpression("some expression"); + assertEquals("some expression", someExpression.result().expression()); + assertEquals(1, someExpression.variables().size()); + final Variable variable = someExpression.variables().get("1"); + assertEquals("some result", variable.getValString()); + } + + @Test + void evaluateWatchers_error() throws Throwable { + Mockito.when(evaluator.evaluateExpression(Mockito.anyString(), Mockito.anyMap())).thenThrow(new RuntimeException("Test exception")); + final IExpressionResult someExpression = frameCollector.evaluateWatchExpression("some expression"); + assertEquals("some expression", someExpression.result().expression()); + assertEquals(0, someExpression.variables().size()); + assertEquals("java.lang.RuntimeException: Test exception", someExpression.result().error()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessorTest.java new file mode 100644 index 0000000..1b66f9c --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/FrameProcessorTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +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.assertTrue; + +import com.intergral.deep.agent.Utils; +import com.intergral.deep.agent.api.plugin.EvaluationException; +import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.evaluator.EvaluatorService; +import com.intergral.deep.agent.types.TracePointConfig; +import com.intergral.deep.agent.types.snapshot.EventSnapshot; +import com.intergral.deep.agent.types.snapshot.WatchResult; +import com.intergral.deep.test.MockTracepointConfig; +import com.intergral.deep.test.target.ConditionTarget; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class FrameProcessorTest { + + private Settings settings = Mockito.mock(Settings.class); + private IEvaluator evaluator = EvaluatorService.createEvaluator(); + private Collection tracepoints = new ArrayList<>(); + private FrameProcessor frameProcessor; + + @BeforeEach + void setUp() { + Mockito.when(settings.getResource()).thenReturn(Resource.DEFAULT); + tracepoints.clear(); + frameProcessor = new FrameProcessor(settings, evaluator, Collections.singletonMap("this", new ConditionTarget()), tracepoints, + Utils.currentTimeNanos(), Thread.currentThread().getStackTrace()); + } + + @Test + void canCollect() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig(); + tracepoints.add(tracepointConfig); + + assertTrue(frameProcessor.canCollect()); + tracepointConfig.fired(Utils.currentTimeNanos()[0]); + + assertFalse(frameProcessor.canCollect()); + } + + @Test + void canCollect_withCondition() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig().withArg(TracePointConfig.CONDITION, "this.i == 101"); + tracepoints.add(tracepointConfig); + + assertFalse(frameProcessor.canCollect()); + tracepointConfig.withArg(TracePointConfig.CONDITION, "this.i == 100"); + + assertTrue(frameProcessor.canCollect()); + } + + @Test + void willCollect() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig(); + tracepoints.add(tracepointConfig); + assertTrue(frameProcessor.canCollect()); + frameProcessor.configureSelf(); + + final Collection collect = frameProcessor.collect(); + assertEquals(1, collect.size()); + } + + @Test + void willCollect_2() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig(); + tracepoints.add(tracepointConfig); + tracepoints.add(new MockTracepointConfig()); + assertTrue(frameProcessor.canCollect()); + frameProcessor.configureSelf(); + + final Collection collect = frameProcessor.collect(); + assertEquals(2, collect.size()); + } + + @Test + void willCollect_watches() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig().withWatches("this.i", "this.i - 10"); + tracepoints.add(tracepointConfig); + assertTrue(frameProcessor.canCollect()); + frameProcessor.configureSelf(); + + final Collection collect = frameProcessor.collect(); + assertEquals(1, collect.size()); + final EventSnapshot next = collect.iterator().next(); + final ArrayList watches = next.getWatches(); + assertEquals(2, watches.size()); + + final WatchResult iWatch; + final WatchResult i10Watch; + + // order is not determined + if (watches.get(0).expression().equals("this.i")) { + iWatch = watches.get(0); + i10Watch = watches.get(1); + } else { + iWatch = watches.get(1); + i10Watch = watches.get(0); + } + + assertEquals("this.i", iWatch.expression()); + assertNotNull(iWatch.goodResult()); + assertEquals("100", next.getVarLookup().get(iWatch.goodResult().getId()).getValString()); + + assertEquals("this.i - 10", i10Watch.expression()); + assertNotNull(i10Watch.goodResult()); + assertEquals("90.0", next.getVarLookup().get(i10Watch.goodResult().getId()).getValString()); + } + + @Test + void willCollect_watches_2() { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig().withWatches("this.i", "10 - this.i"); + tracepoints.add(tracepointConfig); + assertTrue(frameProcessor.canCollect()); + frameProcessor.configureSelf(); + + final Collection collect = frameProcessor.collect(); + assertEquals(1, collect.size()); + final EventSnapshot next = collect.iterator().next(); + final ArrayList watches = next.getWatches(); + assertEquals(2, watches.size()); + + final WatchResult iWatch; + final WatchResult i10Watch; + + // order is not determined + if (watches.get(0).expression().equals("this.i")) { + iWatch = watches.get(0); + i10Watch = watches.get(1); + } else { + iWatch = watches.get(1); + i10Watch = watches.get(0); + } + + assertEquals("this.i", iWatch.expression()); + assertNotNull(iWatch.goodResult()); + assertEquals("100", next.getVarLookup().get(iWatch.goodResult().getId()).getValString()); + + assertEquals("10 - this.i", i10Watch.expression()); + assertNotNull(i10Watch.goodResult()); + assertEquals("-90.0", next.getVarLookup().get(i10Watch.goodResult().getId()).getValString()); + } + + @Test + void willEvaluate() throws EvaluationException { + assertEquals("100", frameProcessor.evaluateExpression("this.i")); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessorTest.java new file mode 100644 index 0000000..88895f9 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/VariableProcessorTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.intergral.deep.agent.push.PushUtils; +import com.intergral.deep.agent.tracepoint.handler.VariableProcessor.VariableResponse; +import com.intergral.deep.agent.tracepoint.handler.bfs.Node; +import com.intergral.deep.agent.types.snapshot.VariableID; +import com.intergral.deep.proto.tracepoint.v1.Variable; +import com.intergral.deep.test.target.VariableTypeTarget; +import com.intergral.deep.tests.snapshot.SnapshotUtils; +import com.intergral.deep.tests.snapshot.SnapshotUtils.IVariableScan; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableProcessorTest { + + private VariableProcessor variableProcessor; + + @BeforeEach + void setUp() { + variableProcessor = new VariableProcessor() { + }; + variableProcessor.configureSelf(new ArrayList<>()); + + } + + @Test + void variableTypes() { + final List variableIDS = processVars(Collections.singletonMap("this", new VariableTypeTarget())); + assertNotNull(variableIDS); + + final Collection localVars = PushUtils.covertVariables(variableIDS); + final Map lookup = PushUtils.convertVarLookup(variableProcessor.varLookup); + final IVariableScan aThis = SnapshotUtils.findVarByName("this", localVars, lookup); + assertNotNull(aThis); + assertEquals("VariableTypeTarget{}", aThis.variable().getValue()); + + final IVariableScan VariableSuperTesti = SnapshotUtils.findVarByName("VariableSuperTest.i", aThis.variable().getChildrenList(), lookup); + assertTrue(VariableSuperTesti.found()); + assertEquals("9", VariableSuperTesti.variable().getValue()); + assertArrayEquals( + Arrays.stream(new String[]{"public", "static"}).sorted().toArray(), + Arrays.stream(VariableSuperTesti.variableId().getModifiersList().toArray(new String[0])).sorted().toArray()); + assertEquals("i", VariableSuperTesti.variableId().getOriginalName()); + + final IVariableScan VariableSuperSuperTesti = SnapshotUtils.findVarByName("VariableSuperSuperTest.i", + aThis.variable().getChildrenList(), lookup); + assertTrue(VariableSuperSuperTesti.found()); + assertEquals("11", VariableSuperSuperTesti.variable().getValue()); + assertArrayEquals( + Arrays.stream(new String[]{"public", "static"}).sorted().toArray(), + Arrays.stream(VariableSuperSuperTesti.variableId().getModifiersList().toArray(new String[0])).sorted().toArray()); + assertEquals("i", VariableSuperSuperTesti.variableId().getOriginalName()); + + final IVariableScan VariableSuperTestl = SnapshotUtils.findVarByName("VariableSuperTest.l", aThis.variable().getChildrenList(), lookup); + assertTrue(VariableSuperTestl.found()); + assertEquals("8", VariableSuperTestl.variable().getValue()); + assertArrayEquals(new String[]{"static"}, VariableSuperTestl.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan VariableSuperTestd = SnapshotUtils.findVarByName("VariableSuperTest.d", aThis.variable().getChildrenList(), lookup); + assertTrue(VariableSuperTestd.found()); + assertEquals("9.8", VariableSuperTestd.variable().getValue()); + assertArrayEquals(Arrays.stream(new String[]{"private", "static"}).sorted().toArray(), + VariableSuperTestd.variableId().getModifiersList().toArray(Arrays.stream(new String[0]).sorted().toArray())); + + final IVariableScan VariableSuperTestf = SnapshotUtils.findVarByName("VariableSuperTest.f", aThis.variable().getChildrenList(), lookup); + assertTrue(VariableSuperTestf.found()); + assertEquals("8.8", VariableSuperTestf.variable().getValue()); + assertArrayEquals(Arrays.stream(new String[]{"protected", "static"}).sorted().toArray(), + Arrays.stream(VariableSuperTestf.variableId().getModifiersList().toArray(new String[0])).sorted().toArray()); + + final IVariableScan i = SnapshotUtils.findVarByName("i", aThis.variable().getChildrenList(), lookup); + assertTrue(i.found()); + assertEquals("1", i.variable().getValue()); + assertArrayEquals( + Arrays.stream(new String[]{"public", "static"}).sorted().toArray(), + Arrays.stream(i.variableId().getModifiersList().toArray(new String[0])).sorted().toArray()); + + final IVariableScan l = SnapshotUtils.findVarByName("l", aThis.variable().getChildrenList(), lookup); + assertTrue(l.found()); + assertEquals("2", l.variable().getValue()); + assertArrayEquals(new String[]{"static"}, l.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan d = SnapshotUtils.findVarByName("d", aThis.variable().getChildrenList(), lookup); + assertTrue(d.found()); + assertEquals("1.2", d.variable().getValue()); + assertArrayEquals(Arrays.stream(new String[]{"private", "static"}).sorted().toArray(), + d.variableId().getModifiersList().toArray(Arrays.stream(new String[0]).sorted().toArray())); + + final IVariableScan f = SnapshotUtils.findVarByName("f", aThis.variable().getChildrenList(), lookup); + assertTrue(f.found()); + assertEquals("2.2", f.variable().getValue()); + assertArrayEquals(Arrays.stream(new String[]{"protected", "static"}).sorted().toArray(), + Arrays.stream(f.variableId().getModifiersList().toArray(new String[0])).sorted().toArray()); + + final IVariableScan str = SnapshotUtils.findVarByName("str", aThis.variable().getChildrenList(), lookup); + assertTrue(str.found()); + assertTrue(str.variable().getValue().startsWith("str with a very large value will be truncated, needs to be over 1024 by default")); + assertEquals(1024, str.variable().getValue().length()); + assertTrue(str.variable().hasTruncated()); + assertArrayEquals(new String[]{"public"}, str.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan nul = SnapshotUtils.findVarByName("nul", aThis.variable().getChildrenList(), lookup); + assertTrue(nul.found()); + assertEquals("null", nul.variable().getValue()); + assertArrayEquals(new String[]{}, nul.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan object = SnapshotUtils.findVarByName("object", aThis.variable().getChildrenList(), lookup); + assertTrue(object.found()); + assertEquals("java.lang.Object", object.variable().getValue().split("@")[0]); + assertArrayEquals(new String[]{"private", "final"}, object.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan arry = SnapshotUtils.findVarByName("arry", aThis.variable().getChildrenList(), lookup); + assertTrue(arry.found()); + assertEquals("Array of length: 2", arry.variable().getValue()); + assertArrayEquals(new String[]{"protected"}, arry.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan list = SnapshotUtils.findVarByName("list", aThis.variable().getChildrenList(), lookup); + assertTrue(list.found()); + assertEquals("ArrayList of size: 0", list.variable().getValue()); + assertArrayEquals(new String[]{"private", "volatile"}, list.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan set = SnapshotUtils.findVarByName("set", aThis.variable().getChildrenList(), lookup); + assertTrue(set.found()); + assertEquals("HashSet of size: 2", set.variable().getValue()); + assertArrayEquals(new String[]{"private", "transient"}, set.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan simplemap = SnapshotUtils.findVarByName("simplemap", aThis.variable().getChildrenList(), lookup); + assertTrue(simplemap.found()); + assertEquals("HashMap of size: 1", simplemap.variable().getValue()); + assertArrayEquals(new String[]{"private"}, simplemap.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan complexMap = SnapshotUtils.findVarByName("complexMap", aThis.variable().getChildrenList(), lookup); + assertTrue(complexMap.found()); + assertEquals("VariableTypeTarget$1 of size: 3", complexMap.variable().getValue()); + assertArrayEquals(new String[]{"private"}, complexMap.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan iter = SnapshotUtils.findVarByName("iter", aThis.variable().getChildrenList(), lookup); + assertTrue(iter.found()); + assertEquals("Iterator of type: HashMap$EntryIterator", iter.variable().getValue()); + assertArrayEquals(new String[]{"private"}, iter.variableId().getModifiersList().toArray(new String[0])); + + final IVariableScan someObj = SnapshotUtils.findVarByName("someObj", aThis.variable().getChildrenList(), lookup); + assertTrue(someObj.found()); + assertEquals("VariableTypeTarget{}", someObj.variable().getValue()); + assertArrayEquals(new String[]{"private"}, someObj.variableId().getModifiersList().toArray(new String[0])); + } + + + protected List processVars(final Map variables) { + final List frameVars = new ArrayList<>(); + + final Node.IParent frameParent = frameVars::add; + + final Set initialNodes = variables.entrySet() + .stream() + .map(stringObjectEntry -> new Node(new Node.NodeValue(stringObjectEntry.getKey(), + stringObjectEntry.getValue()), frameParent)).collect(Collectors.toSet()); + + Node.breadthFirstSearch(new Node(null, new HashSet<>(initialNodes), frameParent), + this::processNode); + + return frameVars; + } + + protected boolean processNode(final Node node) { + if (!variableProcessor.checkVarCount()) { + // we have exceeded the var count, so do not continue + return false; + } + + final Node.NodeValue value = node.getValue(); + if (value == null) { + // this node has no value, continue with children + return true; + } + + // process this node variable + final VariableResponse processResult = variableProcessor.processVariable(value); + final VariableID variableId = processResult.getVariableId(); + + // add the result to the parent - this maintains the hierarchy in the var look up + node.getParent().addChild(variableId); + + if (value.getValue() != null && processResult.processChildren()) { + final Set childNodes = variableProcessor.processChildNodes(variableId, value.getValue(), + node.depth()); + node.addChildren(childNodes); + } + return true; + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/bfs/NodeTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/bfs/NodeTest.java new file mode 100644 index 0000000..7cdb795 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/bfs/NodeTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler.bfs; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.intergral.deep.agent.tracepoint.handler.bfs.Node.IParent; +import com.intergral.deep.agent.types.snapshot.VariableID; +import org.junit.jupiter.api.Test; + +class NodeTest { + + @Test + void coverage() { + assertFalse(((IParent) child -> { + + }).isCollection()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/CFClassScannerTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/CFClassScannerTest.java new file mode 100644 index 0000000..b9f9a65 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/CFClassScannerTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CFClassScannerTest { + + private CFClassScanner cfClassScanner; + + @BeforeEach + void setUp() { + cfClassScanner = new CFClassScanner(new HashMap<>()); + } + + @Test + void getLocation() throws MalformedURLException { + final URL location = cfClassScanner.getLocation(CFClassScannerTest.class); + assertTrue(location.toString().endsWith("test-classes/")); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/InstUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/InstUtilsTest.java new file mode 100644 index 0000000..becba76 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/InstUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class InstUtilsTest { + + @Test + void internalClass() { + assertEquals("java/lang/String", InstUtils.internalClass(String.class)); + } + + + @Test + void externalClassName() { + assertEquals(getClass().getName(), InstUtils.externalClassName("com/intergral/deep/agent/tracepoint/inst/InstUtilsTest")); + assertEquals(getClass().getName(), InstUtils.externalClassName(getClass().getName())); + } + + + @Test + void fileName() { + assertEquals("InstUtilsTest", InstUtils.fileName("com/intergral/deep/agent/tracepoint/inst/InstUtilsTest")); + assertEquals("InstUtilsTest", InstUtils.fileName("InstUtilsTest")); + } + + + @Test + void innerClassName() { + assertEquals("com/intergral/deep/agent/tracepoint/inst/InstUtilsTest", InstUtils.internalClassStripInner(BadToStringType.class)); + } + + + @Test + void shortclassName() { + assertEquals("InstUtilsTest", InstUtils.shortClassName(getClass().getName())); + } + + + public static class BadToStringType { + + public String someString() { + return "someString"; + } + + + @Override + public String toString() { + throw new RuntimeException("bad class"); + } + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationServiceTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationServiceTest.java new file mode 100644 index 0000000..af42459 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationServiceTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.types.TracePointConfig; +import com.intergral.deep.test.MockTracepointConfig; +import com.intergral.deep.test.target.Person; +import com.intergral.deep.tests.inst.ByteClassLoader; +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class TracepointInstrumentationServiceTest { + + private Instrumentation instrumentation; + private Settings settings; + private TracepointInstrumentationService tracepointInstrumentationService; + + @BeforeEach + void setUp() throws MalformedURLException { + instrumentation = Mockito.mock(Instrumentation.class); + settings = Mockito.mock(Settings.class); + Mockito.when(settings.getSettingAs("jsp.packages", List.class)).thenReturn(Arrays.asList("org.apache.jsp", "jsp")); + Mockito.when(settings.getSettingAs("jsp.suffix", String.class)).thenReturn("_jsp"); + final URL cfurl = new URL("file:///src/main/cfml/testList.cfm"); + tracepointInstrumentationService = new TracepointInstrumentationService(instrumentation, settings) { + /** + * To let us test Adobe CF classes we need to load the protection domain, to get the code source url. + *

+ * We do this by intercepting the call to {@link TracepointInstrumentationService#reTransFormCfClasses(Map, Map)} + * and mocking the {@link CFClassScanner#getLocation(Class)} method. + * + * @param newCFMState the new CFM state + * @param previousCFMState the previous CFM state + * + * @return the modified {@link CFClassScanner} */ + @Override + protected CFClassScanner reTransFormCfClasses(final Map newCFMState, + final Map previousCFMState) { + final CFClassScanner iClassScanner = super.reTransFormCfClasses(newCFMState, previousCFMState); + return new CFClassScanner(iClassScanner.tracePointConfigMap) { + @Override + URL getLocation(final Class loadedClass) { + if (loadedClass.getName().equals("cftestList2ecfm1060358347")) { + return cfurl; + } + return super.getLocation(loadedClass); + } + }; + } + }; + } + + @Test + void initShouldRegister() { + final TracepointInstrumentationService init = TracepointInstrumentationService.init(instrumentation, settings); + assertNotNull(init); + Mockito.verify(instrumentation).addTransformer(Mockito.any(), Mockito.anyBoolean()); + } + + @Test + void processTracepointsShouldNotReTransform() throws UnmodifiableClassException { + // no matched class do not re-transform + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[]{}); + tracepointInstrumentationService.processBreakpoints(Collections.singletonList(new MockTracepointConfig())); + + Mockito.verify(instrumentation, Mockito.never()).retransformClasses(Mockito.any()); + } + + @Test + void matchedTracepointShouldReTranform() throws UnmodifiableClassException { + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[]{Person.class}); + Mockito.when(instrumentation.isModifiableClass(Person.class)).thenReturn(true); + tracepointInstrumentationService.processBreakpoints( + Collections.singletonList(new MockTracepointConfig("/com/intergral/deep/test/target/Person.java"))); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Class.class); + + Mockito.verify(instrumentation).retransformClasses(argumentCaptor.capture()); + + final Class value = argumentCaptor.getValue(); + assertEquals(value, Person.class); + } + + @Test + void removingTPShouldReTransform() throws UnmodifiableClassException { + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[]{Person.class}); + Mockito.when(instrumentation.isModifiableClass(Person.class)).thenReturn(true); + tracepointInstrumentationService.processBreakpoints( + Collections.singletonList(new MockTracepointConfig("/com/intergral/deep/test/target/Person.java"))); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Class.class); + + Mockito.verify(instrumentation, times(1)).retransformClasses(argumentCaptor.capture()); + + final Class value = argumentCaptor.getValue(); + assertEquals(value, Person.class); + + tracepointInstrumentationService.processBreakpoints(Collections.emptyList()); + + Mockito.verify(instrumentation, times(2)).retransformClasses(argumentCaptor.capture()); + + final Class value2 = argumentCaptor.getValue(); + assertEquals(value2, Person.class); + } + + @Test + void cfClassesTriggerTransform() throws IOException, ClassNotFoundException, UnmodifiableClassException { + final String cfClassName = "cftestList2ecfm1060358347"; + final ByteClassLoader byteClassLoader = ByteClassLoader.forFile(cfClassName); + final Class cfClass = byteClassLoader.loadClass(cfClassName); + + assertNotNull(cfClass); + + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[]{cfClass}); + Mockito.when(instrumentation.isModifiableClass(cfClass)).thenReturn(true); + + tracepointInstrumentationService.processBreakpoints(Collections.singletonList(new MockTracepointConfig("/src/main/cfml/testList.cfm"))); + + Mockito.verify(instrumentation, times(1)).retransformClasses(cfClass); + tracepointInstrumentationService.processBreakpoints(Collections.emptyList()); + + Mockito.verify(instrumentation, times(2)).retransformClasses(cfClass); + } + + @Test + void jspClassesTriggerTransform() throws IOException, ClassNotFoundException, UnmodifiableClassException { + final String jspClassName = "org/apache/jsp/tests/string_jsp"; + final ByteClassLoader byteClassLoader = ByteClassLoader.forFile(jspClassName); + final Class jspClass = byteClassLoader.loadClass(InstUtils.externalClassName(jspClassName)); + + assertNotNull(jspClass); + + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[]{jspClass}); + Mockito.when(instrumentation.isModifiableClass(jspClass)).thenReturn(true); + + tracepointInstrumentationService.processBreakpoints( + Collections.singletonList(new MockTracepointConfig("/src/main/webapp/tests/string.jsp"))); + + Mockito.verify(instrumentation, times(1)).retransformClasses(jspClass); + tracepointInstrumentationService.processBreakpoints(Collections.emptyList()); + Mockito.verify(instrumentation, times(2)).retransformClasses(jspClass); + } + + @Test + void getLocation() throws MalformedURLException { + final ProtectionDomain protectionDomain = Mockito.mock(ProtectionDomain.class); + final CodeSource codeSource = Mockito.mock(CodeSource.class); + Mockito.when(protectionDomain.getCodeSource()).thenReturn(codeSource); + Mockito.when(codeSource.getLocation()).thenReturn(new URL("http://google.com")); + final URL location = tracepointInstrumentationService.getLocation(protectionDomain); + assertEquals("http://google.com", location.toString()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundExceptionTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundExceptionTest.java new file mode 100644 index 0000000..c89b10c --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoNotFoundExceptionTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.asm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ClassInfoNotFoundExceptionTest { + + @Test + void coverage() { + assertEquals("some test", new ClassInfoNotFoundException("some test", "some type").getMessage()); + assertEquals("some type", new ClassInfoNotFoundException("some test", "some type").getType()); + assertEquals("some cause", + new ClassInfoNotFoundException("some test", "some type", new RuntimeException("some cause")).getCause().getMessage()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoTest.java new file mode 100644 index 0000000..ff3e818 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/ClassInfoTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.asm; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import lucee.loader.classloader.LuceeClassLoader; +import org.junit.jupiter.api.Test; +import railo.loader.classloader.RailoClassLoader; + +class ClassInfoTest { + + @Test + void isLuceeClassLoader() { + assertTrue(ClassInfo.isLuceeClassLoader(new LuceeClassLoader())); + assertFalse(ClassInfo.isRailoClassLoader(new LuceeClassLoader())); + } + + @Test + void isRailoClassLoader() { + assertFalse(ClassInfo.isLuceeClassLoader(new RailoClassLoader())); + assertTrue(ClassInfo.isRailoClassLoader(new RailoClassLoader())); + + } + + @Test + void isSafeClassLoader() { + assertFalse(ClassInfo.isSafeLoader(new RailoClassLoader())); + assertFalse(ClassInfo.isSafeLoader(new LuceeClassLoader())); + assertTrue(ClassInfo.isSafeLoader(new railo.commons.lang.PCLBlock())); + assertTrue(ClassInfo.isSafeLoader(new lucee.commons.lang.PCLBlock())); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java new file mode 100644 index 0000000..85f0bcd --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java @@ -0,0 +1,1440 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.asm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +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.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.logging.Logger; +import com.intergral.deep.agent.push.PushService; +import com.intergral.deep.agent.push.PushUtils; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import com.intergral.deep.agent.tracepoint.handler.Callback; +import com.intergral.deep.agent.tracepoint.inst.InstUtils; +import com.intergral.deep.agent.tracepoint.inst.TracepointInstrumentationService; +import com.intergral.deep.agent.types.TracePointConfig; +import com.intergral.deep.agent.types.snapshot.EventSnapshot; +import com.intergral.deep.agent.types.snapshot.StackFrame; +import com.intergral.deep.proto.tracepoint.v1.Snapshot; +import com.intergral.deep.proto.tracepoint.v1.Variable; +import com.intergral.deep.test.MockTracepointConfig; +import com.intergral.deep.test.target.BPTestTarget; +import com.intergral.deep.tests.inst.ByteClassLoader; +import com.intergral.deep.tests.snapshot.SnapshotUtils; +import com.intergral.deep.tests.snapshot.SnapshotUtils.IVariableScan; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.file.Paths; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import javax.servlet.DispatcherType; +import javax.servlet.Servlet; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspFactory; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import lucee.runtime.PageSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +/** + * We test the visitor by using the transformer to modify classes. + *

+ * This test can be used to play with visitor, it uses the {@link BPTestTarget} class to install tracepoints into + *

+ * To run this use the 'VisitorTest' saved config for idea, or add + * {@code -Ddeep.callback.class=com.intergral.deep.agent.tracepoint.handler.Callback} to the test runner args + *

+ * WARNING: The line numbers used in this test are important, they must match the line numbers on the test target classes!! + */ +class VisitorTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointConfigService tracepointConfigService = Mockito.mock(TracepointConfigService.class); + private final PushService pushService = Mockito.mock(PushService.class); + private final Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + private final String disPath = Paths.get(Paths.get(".").normalize().toAbsolutePath().getParent().toString(), "dispath").toString(); + + private final AtomicReference> tracepointRef = new AtomicReference<>(); + private final AtomicReference cfUrl = new AtomicReference<>(); + private TracepointInstrumentationService instrumentationService; + + @BeforeEach + void setUp() { + // set up logging to help with debugging tests + Mockito.when(settings.getSettingAs("logging.level", Level.class)).thenReturn(Level.FINEST); + Logger.configureLogging(settings); + + Mockito.when(settings.getResource()).thenReturn(Resource.DEFAULT); + + // for these tests we do not care about these (they are tested in the TracepointInstrumentationServiceTest) + // we just need them to pass, as we manage the instrumentation ourselves. + Mockito.when(instrumentation.getAllLoadedClasses()).thenReturn(new Class[0]); + Mockito.when(instrumentation.isModifiableClass(Mockito.any())).thenReturn(true); + + // these settings are needed for jsp transforms + Mockito.when(settings.getAsList("jsp.packages")).thenReturn(Arrays.asList("org.apache.jsp", "jsp")); + Mockito.when(settings.getSettingAs("jsp.packages", List.class)).thenReturn(Arrays.asList("org.apache.jsp", "jsp")); + Mockito.when(settings.getSettingAs("jsp.suffix", String.class)).thenReturn("_jsp"); + + Mockito.when(tracepointConfigService.loadTracepointConfigs(Mockito.any())).thenAnswer(invocationOnMock -> { + final Collection tracePointConfig = tracepointRef.get(); + if (tracePointConfig == null) { + return Collections.emptyList(); + } + return tracePointConfig; + }); + + instrumentationService = new TracepointInstrumentationService(instrumentation, settings) { + @Override + protected URL getLocation(final ProtectionDomain protectionDomain) { + final URL url = cfUrl.get(); + if (url == null) { + return super.getLocation(protectionDomain); + } + return url; + } + }; + + Callback.init(settings, tracepointConfigService, pushService); + } + + // test a line within the constructor + @Test + void constructor() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 21); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("my test", 4); + assertNotNull(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + assertTrue(scanResult.found()); + assertEquals("my test", scanResult.variable().getValue()); + + assertEquals("", snapshot.getFrames(0).getMethodName()); + } + + // test the last line of the constructor closing brace + @Test + void constructor_end_line() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 23); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("my test", 4); + assertNotNull(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // the parameter to the constructor should be available + assertTrue(scanResult.found()); + assertEquals("my test", scanResult.variable().getValue()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("my test", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namemy test", superName.variable().getValue()); + + assertEquals("", snapshot.getFrames(0).getMethodName()); + } + + // test first line in constructor (super call) + @Test + void constructor_start_line() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 20); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("my test", 4); + assertNotNull(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // the parameter to the constructor should be available + assertTrue(scanResult.found()); + assertEquals("my test", scanResult.variable().getValue()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' but it should not have a value as our TP is before this is set + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namemy test", superName.variable().getValue()); + + assertEquals("", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void setName() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 51); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("my test", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("setName", String.class); + setName.invoke(myTest, "something"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // the parameter to the constructor should be available + assertTrue(scanResult.found()); + assertEquals("something", scanResult.variable().getValue()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("my test", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namemy test", superName.variable().getValue()); + + assertEquals("setName", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void getName_null_return() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 44); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("getName"); + setName.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namenull", superName.variable().getValue()); + + assertEquals("getName", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void getName_non_null_return() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 46); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("getName"); + setName.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("getName", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void errorSomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 75); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("errorSomething", String.class); + final InvocationTargetException invocationTargetException = assertThrows(InvocationTargetException.class, + () -> setName.invoke(myTest, (Object) null)); + assertNull(invocationTargetException.getTargetException().getMessage()); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("errorSomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void throwSomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 87); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("throwSomething", String.class); + final InvocationTargetException invocationTargetException = assertThrows(InvocationTargetException.class, + () -> setName.invoke(myTest, (Object) null)); + assertNull(invocationTargetException.getTargetException().getMessage()); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("throwSomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void catchSomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 112); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("catchSomething", String.class); + setName.invoke(myTest, (Object) null); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("catchSomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void finallySomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 146); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method setName = aClass.getDeclaredMethod("finallySomething", String.class); + setName.invoke(myTest, (Object) null); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("finallySomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void conditionalThrow() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 163); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("conditionalThrow", int.class, int.class); + method.invoke(myTest, 1, 2); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.never()) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + assertThrows(InvocationTargetException.class, () -> method.invoke(myTest, 3, 2)); + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("conditionalThrow", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void breakSomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 174); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("breakSomething"); + method.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("breakSomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void continueSomething() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 187); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("continueSomething"); + method.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("some name", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namesome name", superName.variable().getValue()); + + assertEquals("continueSomething", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void superConstructor() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPSuperClass.java", 10); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String superName = "com/intergral/deep/test/target/BPSuperClass"; + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(superName); + classLoader.setBytes(name, ByteClassLoader.loadBytes(name)); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(superName); + final byte[] transformed = instrumentationService.transform(null, superName, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, + superName + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + // modify the super class with the tracepoint + final String clazzName = InstUtils.externalClassName(superName); + classLoader.setBytes(clazzName, transformed); + classLoader.setBytes(InstUtils.externalClassName(name), ByteClassLoader.loadBytes(name)); + + // now load the subclass + final Class superClass = classLoader.loadClass(clazzName); + final Class aClass = classLoader.loadClass(InstUtils.externalClassName(name)); + assertNotNull(aClass); + assertEquals(aClass.getSuperclass(), superClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance("some name", 4); + assertNotNull(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertTrue(scanResult.found()); + assertEquals("i am a namesome name", scanResult.variable().getValue()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be null + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + assertEquals("", snapshot.getFrames(0).getMethodName()); + } + + @Test + void multipleTps_oneLine() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 44); + final MockTracepointConfig tracepointConfig2 = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 44); + tracepointRef.set(Arrays.asList(tracepointConfig, tracepointConfig2)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("getName"); + method.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(2)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namenull", superName.variable().getValue()); + + assertEquals("getName", snapshot.getFrames(0).getMethodName()); + } + + @Test + void multipleTps_nextLine() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 42); + final MockTracepointConfig tracepointConfig2 = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 44); + tracepointRef.set(Arrays.asList(tracepointConfig, tracepointConfig2)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("getName"); + method.invoke(myTest); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(2)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namenull", superName.variable().getValue()); + + assertEquals("getName", snapshot.getFrames(0).getMethodName()); + } + + @Test + void someFunctionWithABody() throws Exception { + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 151); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("someFunctionWithABody", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + final IVariableScan scanResult = SnapshotUtils.findVarByName("name", snapshot); + + // there should not be a local var called name + assertFalse(scanResult.found()); + + // we should also find the 'this' object which should be the BPTargetClass + final IVariableScan thisScan = SnapshotUtils.findVarByName("this", snapshot); + assertTrue(thisScan.found()); + assertEquals(myTest.getClass().getName(), thisScan.variable().getType()); + + // on the 'this' object we should find the field 'name' which should be the value we set in the constructor + final Variable variable = thisScan.variable(); + final IVariableScan thisName = SnapshotUtils.findVarByName("name", variable.getChildrenList(), snapshot.getVarLookupMap()); + assertTrue(thisName.found()); + assertEquals("null", thisName.variable().getValue()); + + // there should however be a value for the 'super.name' field that is set to the known value + final IVariableScan superName = SnapshotUtils.findVarByName("BPSuperClass.name", variable.getChildrenList(), + snapshot.getVarLookupMap()); + assertTrue(superName.found()); + assertEquals("i am a namenull", superName.variable().getValue()); + + assertEquals("someFunctionWithABody", snapshot.getFrames(0).getMethodName()); + } + + + @Test + void cfVisitor() throws Exception { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig("/src/main/cfml/testFile.cfm", 3); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + // we need to process the cfm tracepoints + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final String name = "cftestFile2ecfm137384933"; + final ByteClassLoader byteClassLoader = ByteClassLoader.forFile(name); + final byte[] originalBytes = byteClassLoader.getBytes(name); + + final JspFactory jspFactory = Mockito.mock(JspFactory.class); + final PageContext pageContext = Mockito.mock(PageContext.class); + final JspWriter jspWriter = Mockito.mock(JspWriter.class); + Mockito.when(pageContext.getOut()).thenReturn(jspWriter); + + JspFactory.setDefaultFactory(jspFactory); + Mockito.when(jspFactory.getPageContext(Mockito.any(Servlet.class), Mockito.any(ServletRequest.class), Mockito.any( + ServletResponse.class), Mockito.eq(null), Mockito.anyBoolean(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(pageContext); + + // for adobe cf we need a location url to be set + cfUrl.set(new URL("file:///src/main/cfml/testFile.cfm")); + + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + // we cannot load the cf class in test case for some reason + // consistent issues with verifier + // byteClassLoader.setBytes(name, transformed); + // final List classes = Arrays.asList("java.lang.Object", "coldfusion.tagext.io.OutputTag", + // "coldfusion.runtime.NeoPageContext", + // "coldfusion.runtime.CfJspPage", "coldfusion.runtime.CFPage"); + // for (String s : classes) { + // final Class aClass = byteClassLoader.loadClass(s); + // assertNotNull(aClass); + // assertEquals(aClass.getName(), s); + // } + // final Class aClass = byteClassLoader.loadClass(name); + // + // final Constructor constructor = aClass.getConstructor(); + // final Object instance = constructor.newInstance(); + // final Method runPage = aClass.getDeclaredMethod("runPage"); + // runPage.setAccessible(true); + // runPage.invoke(instance); + // + // final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + // + // Mockito.verify(pushService, Mockito.times(1)) + // .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + } + + + @Test + void jspVisitorTest() throws Exception { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig("/src/main/webapp/tests/string.jsp", 9); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + // we need to process the jsp tracepoints + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final String name = "org/apache/jsp/tests/string_jsp"; + final ByteClassLoader byteClassLoader = ByteClassLoader.forFile(name); + final byte[] originalBytes = byteClassLoader.getBytes(name); + + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String className = InstUtils.externalClassName(name); + byteClassLoader.setBytes(className, transformed); + final Class aClass = byteClassLoader.loadClass(className); + + final JspFactory jspFactory = Mockito.mock(JspFactory.class); + final PageContext pageContext = Mockito.mock(PageContext.class); + final JspWriter jspWriter = Mockito.mock(JspWriter.class); + Mockito.when(pageContext.getOut()).thenReturn(jspWriter); + + JspFactory.setDefaultFactory(jspFactory); + Mockito.when(jspFactory.getPageContext(Mockito.any(Servlet.class), Mockito.any(ServletRequest.class), Mockito.any( + ServletResponse.class), Mockito.eq(null), Mockito.anyBoolean(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(pageContext); + + final Constructor constructor = aClass.getConstructor(); + final Object instance = constructor.newInstance(); + + final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class); + Mockito.when(servletRequest.getDispatcherType()).thenReturn(DispatcherType.ERROR); + final HttpServletResponse servletResponse = Mockito.mock(HttpServletResponse.class); + + final Method jspService = aClass.getDeclaredMethod("_jspService", HttpServletRequest.class, HttpServletResponse.class); + jspService.invoke(instance, servletRequest, servletResponse); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + assertEquals("_jspService", snapshot.getFrames(0).getMethodName()); + assertEquals("org.apache.jsp.tests.string_jsp", snapshot.getFrames(0).getClassName()); + assertEquals("string.jsp", snapshot.getFrames(0).getFileName()); + assertEquals(9, snapshot.getFrames(0).getLineNumber()); + assertEquals("string_jsp.java", snapshot.getFrames(0).getTranspiledFileName()); + assertEquals(127, snapshot.getFrames(0).getTranspiledLineNumber()); + } + + @Test + void luceeVisitorTest() throws Exception { + final MockTracepointConfig tracepointConfig = new MockTracepointConfig("/tests/testFile.cfm", 3); + tracepointRef.set(Collections.singletonList(tracepointConfig)); + // we need to process the jsp tracepoints + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + cfUrl.set(new URL("file:///tests/testFile.cfm")); + final String name = "testfile_cfm$cf"; + final ByteClassLoader byteClassLoader = ByteClassLoader.forFile(name); + final byte[] originalBytes = byteClassLoader.getBytes(name); + + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform test class."); + assertNotEquals(originalBytes.length, transformed.length); + + final String className = InstUtils.externalClassName(name); + byteClassLoader.setBytes(className, transformed); + final Class aClass = byteClassLoader.loadClass(className); + + final Constructor constructor = aClass.getConstructor(PageSource.class); + final Object instance = constructor.newInstance(new PageSource()); + final Method call = aClass.getMethod("call", lucee.runtime.PageContext.class); + call.invoke(instance, new lucee.runtime.PageContext()); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, Mockito.times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertNotNull(value); + // Cannot really verify variables as we are using fake classes to run this test + + final StackFrame stackFrame = value.getFrames().iterator().next(); + assertEquals("testFile.cfm", stackFrame.getFileName()); + assertEquals(3, stackFrame.getLineNumber()); + assertEquals("testfile_cfm$cf", stackFrame.getClassName()); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/JSPUtilsTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/JSPUtilsTest.java new file mode 100644 index 0000000..de48c26 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/JSPUtilsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.jsp; + +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.assertTrue; + +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SmapUtils; +import com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap.SourceMap; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; + +class JSPUtilsTest { + + @Test + void isJspClass() { + assertTrue(JSPUtils.isJspClass("_jsp", Settings.coerc("org.apache.jsp,jsp", List.class), + "org.apache.jsp.jdbc.views.companies_jsp")); + assertTrue(JSPUtils.isJspClass("_jsp", Settings.coerc("org.apache.jsp,jsp", List.class), + "org.apache.jsp.views.companies_jsp")); + assertTrue(JSPUtils.isJspClass("_jsp", Settings.coerc("org.apache.jsp,jsp", List.class), + "org.apache.jsp.companies_jsp")); + assertTrue(JSPUtils.isJspClass("_jsp", Settings.coerc("org.apache.jsp,jsp", List.class), + "jsp.companies_jsp")); + + assertFalse(JSPUtils.isJspClass("_jsp", Settings.coerc("org.apache.jsp,jsp", List.class), + "companies_jsp")); + } + + + @Test + void sourceMap() throws Exception { + final File file = new File("src/test/resources"); + final URL resourceUrl = file.toURI().toURL(); + final URL[] classUrls = {resourceUrl}; + final URLClassLoader ucl = new URLClassLoader(classUrls); + final Class c = ucl.loadClass("org.apache.jsp.jdbc.views.companies_jsp"); + + final SourceMap sourceMap = JSPUtils.getSourceMap(c); + + assertNotNull(sourceMap); + final List filenames = sourceMap.getFilenames(); + assertTrue(filenames.containsAll(Arrays.asList("companies.jsp", "header.jsp", "setup.jsp", "footer.jsp"))); + } + + + @Test + void sourceMap_bytes() throws Exception { + final InputStream resourceAsStream = getClass().getResourceAsStream("/org/apache/jsp/jdbc/views/companies_jsp.class"); + final byte[] bytes = new byte[resourceAsStream.available()]; + resourceAsStream.read(bytes); + + final SourceMap sourceMap = JSPUtils.getSourceMap(bytes); + + assertNotNull(sourceMap); + final List filenames = sourceMap.getFilenames(); + assertTrue(filenames.containsAll(Arrays.asList("companies.jsp", "header.jsp", "setup.jsp", "footer.jsp"))); + } + + + @Test + void sourceMap_bytes_src() throws Exception { + final InputStream resourceAsStream = getClass().getResourceAsStream("/org/apache/jsp/jdbc/views/companies_jsp.class"); + final byte[] bytes = new byte[resourceAsStream.available()]; + resourceAsStream.read(bytes); + + final String src = SmapUtils.scanSource(new ClassReader(bytes)); + + assertNotNull(src); + assertEquals("companies_jsp.java", src); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/FileSectionEntryTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/FileSectionEntryTest.java new file mode 100644 index 0000000..9b920e9 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/FileSectionEntryTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FileSectionEntryTest { + + + private FileSectionEntry fileSectionEntry; + + + @BeforeEach + void setUp() { + new FileSectionEntry(10, "source"); + fileSectionEntry = new FileSectionEntry(10, "source", "path"); + } + + + @Test + void getSourceName() { + assertEquals("source", fileSectionEntry.getSourceName()); + } + + + @Test + void getId() { + assertEquals(10, fileSectionEntry.getId()); + } + + + @Test + void getSourcePath() { + assertEquals("path", fileSectionEntry.getSourcePath()); + } + + + @Test + void toStringTest() { + assertEquals("FileSectionEntry#10:source:path", fileSectionEntry.toString()); + } +} diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/LineSectionEntryTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/LineSectionEntryTest.java new file mode 100644 index 0000000..2f50abb --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/LineSectionEntryTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LineSectionEntryTest { + + + private LineSectionEntry lineSectionEntry; + + + @BeforeEach + void setUp() { + lineSectionEntry = new LineSectionEntry(1, 101, 10, 202, 2); + } + + + @Test + void testToString() { + assertEquals("LineSectionEntry#101 1 10 202 2", lineSectionEntry.toString()); + } +} diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/SourceMapParserTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/SourceMapParserTest.java new file mode 100644 index 0000000..865a7a1 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/jsp/sourcemap/SourceMapParserTest.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.inst.jsp.sourcemap; + +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.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class SourceMapParserTest { + + public SourceMapParserTest() { + } + + + private String fileToString(final String filename) throws IOException { + final byte[] bytes; + try (InputStream resourceAsStream = getClass().getResourceAsStream(filename)) { + bytes = new byte[resourceAsStream.available()]; + resourceAsStream.read(bytes); + } + return new String(bytes); + } + + + @Test + public void testIndexJsp() throws Exception { + final SourceMapParser parser = new SourceMapParser(fileToString("/index_jsp.smap")); + final SourceMap smap = parser.parse(); + + //smap.dumpInformation(); + { + final List found = smap.map("header.jsp", 1); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(71, entry.getStart(), "Should start @ line 71"); + assertEquals(71, entry.getEnd(), "Should end @ line 71"); + } + + { + final List found = smap.map("header.jsp", 11); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(83, entry.getStart(), "Should start @ line 83"); + assertEquals(87, entry.getEnd(), "Should end @ line 87"); + } + + { + final List found = smap.map("footer.jsp", 4); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(138, entry.getStart(), "Should start @ line 138"); + assertEquals(140, entry.getEnd(), "Should end @ line 140"); + } + + { + final List found = smap.map("index.jsp", 4); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(92, entry.getStart(), "Should start @ line 92"); + assertEquals(92, entry.getEnd(), "Should end @ line 92"); + } + } + + + /* Test based on example table from JSR-045 document + Input Source Output Source + Line Begin Line End Line + 123 207 207 + 130 210 210 + 131 211 211 + 132 212 212 + 140 250 256 + 160 300 301 + 161 302 303 + 162 304 305*/ + @Test + public void testIndex2Jsp() throws Exception { + final SourceMapParser parser = new SourceMapParser(fileToString("/index2_jsp.smap")); + final SourceMap smap = parser.parse(); + + // smap.dumpInformation(); + { + final List found = smap.map("index2.jsp", 123); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(207, entry.getStart(), "Should start @ line 207"); + assertEquals(207, entry.getEnd(), "Should end @ line 207"); + } + + { + final List found = smap.map("index2.jsp", 140); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(250, entry.getStart(), "Should start @ line 250"); + assertEquals(256, entry.getEnd(), "Should end @ line 256"); + } + + { + final List found = smap.map("index2.jsp", 160); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(300, entry.getStart(), "Should start @ line 300"); + assertEquals(301, entry.getEnd(), "Should end @ line 301"); + } + + { + final List found = smap.map("index2.jsp", 162); + assertEquals(1, found.size(), "Should fine 1 line"); + + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(304, entry.getStart(), "Should start @ line 304"); + assertEquals(305, entry.getEnd(), "Should end @ line 305"); + } + } + + + @Test + public void testParseIncludeTimeJsp() throws Exception { + // Test with a page including the same jsp twice. + + final SourceMapParser parser = new SourceMapParser(fileToString("/include_time.smap")); + final SourceMap smap = parser.parse(); + + // smap.dumpInformation(); + { + final List found = smap.map("time.jsp", 1); + assertEquals(2, found.size(), "Should fine 2 line"); + { + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(90, entry.getStart(), "Should start @ line 90"); + assertEquals(91, entry.getEnd(), "Should end @ line 91"); + } + { + final SourceMapLineStartEnd entry = found.get(1); + assertEquals(99, entry.getStart(), "Should start @ line 99"); + assertEquals(100, entry.getEnd(), "Should end @ line 100"); + } + } + + { + final List found = smap.map("footer.jsp", 29); + assertEquals(1, found.size(), "Should fine 1 line"); + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(140, entry.getStart(), "Should start @ line 140"); + assertEquals(140, entry.getEnd(), "Should end @ line 140"); + } + } + + + @Test + public void testParseIncludeTime2Jsp() throws Exception { + final SourceMapParser parser = new SourceMapParser(fileToString("/include_time2.smap")); + final SourceMap smap = parser.parse(); + + //smap.dumpInformation(); + + final List found = smap.map("time.jsp", 1); + assertEquals(4, found.size(), "Should contain 4 entries"); + { + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(91, entry.getStart(), "Should start @ line 91"); + assertEquals(92, entry.getEnd(), "Should end @ line 92"); + } + { + final SourceMapLineStartEnd entry = found.get(1); + assertEquals(102, entry.getStart(), "Should start @ line 102"); + assertEquals(103, entry.getEnd(), "Should end @ line 103"); + } + { + final SourceMapLineStartEnd entry = found.get(2); + assertEquals(114, entry.getStart(), "Should start @ line 114"); + assertEquals(115, entry.getEnd(), "Should end @ line 115"); + } + { + final SourceMapLineStartEnd entry = found.get(3); + assertEquals(117, entry.getStart(), "Should start @ line 117"); + assertEquals(118, entry.getEnd(), "Should end @ line 119"); + } + } + + + @Test + public void testIncludeTime3Jsp() throws Exception { + final SourceMapParser parser = new SourceMapParser(fileToString("/include_time3.smap")); + final SourceMap smap = parser.parse(); + + final List found = smap.map("time.jsp", 1); + assertEquals(3, found.size(), "Should contain 3 entries"); + { + final SourceMapLineStartEnd entry = found.get(0); + assertEquals(91, entry.getStart(), "Should start @ line 91"); + assertEquals(92, entry.getEnd(), "Should end @ line 92"); + } + { + final SourceMapLineStartEnd entry = found.get(1); + assertEquals(102, entry.getStart(), "Should start @ line 102"); + assertEquals(103, entry.getEnd(), "Should end @ line 103"); + } + { + final SourceMapLineStartEnd entry = found.get(2); + assertEquals(113, entry.getStart(), "Should start @ line 113"); + assertEquals(116, entry.getEnd(), "Should end @ line 116"); + } + } + + + @Test + public void testIncludeTime3JspLookup() throws Exception { + final String s = fileToString("/include_time3.smap"); + final SourceMapParser parser = new SourceMapParser(s); + final SourceMap smap = parser.parse(); + + //smap.dumpInformation(); + // line 91 doesnt map back to line 1 as there 2 files which match to line 91 + // and the first include_time.jsp should take precident according to the spec. + // 1 include_time.jsp 4 -> 91 + // 2 time.jsp 1 -> 91 + final SourceMapLookup lookup = smap.lookup(92); + + assertEquals("time.jsp", lookup.getFilename(), "Should find time.jsp line 1"); + assertEquals(1, lookup.getLineNumber(), "Should find time.jsp line 1"); + } + + + @Test + public void testCompaniesJsp() throws Exception { + final URL resourceUrl = getClass().getResource("/org/apache/jsp/jdbc/views/companies_jsp.class"); + final URL[] classUrls = {resourceUrl}; + final URLClassLoader ucl = new URLClassLoader(classUrls); + final Class c = ucl.loadClass("org.apache.jsp.jdbc.views.companies_jsp"); + + final String sourceDebugExtension = SmapUtils.lookUp(c); + assertNotNull(sourceDebugExtension, "smap should not be null"); + assertFalse(sourceDebugExtension.isEmpty(), "smap should not be emtpy"); + + assertTrue(sourceDebugExtension.startsWith("SMAP"), "smap should start with SMAP"); + assertTrue(sourceDebugExtension.trim().endsWith("*E"), "smap should end with *E"); + + final SourceMapParser parser = new SourceMapParser(sourceDebugExtension); + final SourceMap smap = parser.parse(); + + //smap.dumpInformation(); + + final SourceMapLookup lookup = smap.lookup(458); + assertNull(lookup, "Should not find a line for 458"); + + assertEquals(71, smap.lookup(457).getLineNumber()); + } + + + @Test + public void testNoopJspBug() throws Exception { + // This code will cause a Null pointer exception FR-5116 + // as the SMAP is corrupt + final String s = fileToString("/noop.smap"); + final SourceMapParser parser = new SourceMapParser(s); + try { + final SourceMap smap = parser.parse(); + fail("Should not parse"); + } catch (IOException ioe) { + assertEquals("Cannot find a Stratum Section in SMAP", ioe.getMessage()); + } + } +} diff --git a/agent/src/test/java/com/intergral/deep/test/MockEventSnapshot.java b/agent/src/test/java/com/intergral/deep/test/MockEventSnapshot.java new file mode 100644 index 0000000..139a219 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockEventSnapshot.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.types.TracePointConfig; +import com.intergral.deep.agent.types.snapshot.EventSnapshot; +import com.intergral.deep.agent.types.snapshot.StackFrame; +import com.intergral.deep.agent.types.snapshot.Variable; +import com.intergral.deep.agent.types.snapshot.VariableID; +import com.intergral.deep.agent.types.snapshot.WatchResult; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +public class MockEventSnapshot extends EventSnapshot { + + public MockEventSnapshot() { + super(new TracePointConfig("tp-1", "/some/file/path.py", 123, new HashMap<>(), new ArrayList<>()), 1011L, Resource.DEFAULT, + new ArrayList<>(), new HashMap<>()); + } + + public MockEventSnapshot withFrames() { + this.getFrames().add(new StackFrame("file_name_1", 4321, "class_name_1", "methodName_1", true, true, new ArrayList<>(), + null, -1)); + this.getFrames().add(new StackFrame("file_name_2", 321, "class_name_2", "methodName_2", false, true, new ArrayList<>(), + null, -1)); + this.getFrames().add(new StackFrame("file_name_3", 21, "class_name_3", "methodName_3", true, false, new ArrayList<>(), + null, -1)); + + final VariableID someLocalVar = new VariableID("var-id-1", "someLocalVar", new HashSet<>(), null); + final HashSet modifiers = new HashSet<>(); + modifiers.add("private"); + final VariableID someLocalVar2 = new VariableID("var-id-2", "someLocalVar2", modifiers, null); + final VariableID someLocalVar3 = new VariableID("var-id-3", "someLocalVar3", new HashSet<>(), "someOtherName"); + final ArrayList frameVariables = new ArrayList<>(); + frameVariables.add(someLocalVar); + frameVariables.add(someLocalVar2); + frameVariables.add(someLocalVar3); + this.getFrames().add(new StackFrame("file_name_4", 1, "class_name_4", "methodName_4", false, false, frameVariables, + null, -1)); + return this; + } + + public MockEventSnapshot withAttributes() { + final HashMap attributes = new HashMap<>(); + attributes.put("long", 123L); + attributes.put("int", 123); + attributes.put("float", 3.21F); + attributes.put("double", 1.23D); + attributes.put("boolean", false); + attributes.put("string", "123l"); + mergeAttributes(Resource.create(attributes)); + return this; + } + + public MockEventSnapshot withWatches() { + getWatches().add(new WatchResult("error", "this is an error")); + getWatches().add(new WatchResult("good", new VariableID("some-var", "withName", new HashSet<>(), null))); + return this; + } + + public MockEventSnapshot withVariables() { + getVarLookup().put("1", new Variable("string", "value_1", "123124", false)); + getVarLookup().put("2", new Variable("string", "value_2", "123124", true)); + return this; + } +} diff --git a/agent/src/test/java/com/intergral/deep/test/MockTracepointConfig.java b/agent/src/test/java/com/intergral/deep/test/MockTracepointConfig.java new file mode 100644 index 0000000..5844623 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockTracepointConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.types.TracePointConfig; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +/** + * A mock tracepoint config to make it easier to create during testing. + */ +public class MockTracepointConfig extends TracePointConfig { + + public MockTracepointConfig() { + super("tp-id", "path", 123, new HashMap<>(), new ArrayList<>()); + } + + public MockTracepointConfig(final String path) { + super("tp-id", path, 123, new HashMap<>(), new ArrayList<>()); + } + + + public MockTracepointConfig(final String path, final int line) { + super("tp-id", path, line, new HashMap<>(), new ArrayList<>()); + } + + public MockTracepointConfig withArg(final String key, final String value) { + this.getArgs().put(key, value); + return this; + } + + public MockTracepointConfig withWatches(final String... watches) { + this.getWatches().addAll(Arrays.asList(watches)); + return this; + } +} diff --git a/agent/src/test/java/com/intergral/deep/test/target/BPSuperClass.java b/agent/src/test/java/com/intergral/deep/test/target/BPSuperClass.java new file mode 100644 index 0000000..a14118b --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/BPSuperClass.java @@ -0,0 +1,12 @@ +package com.intergral.deep.test.target; + +@SuppressWarnings({"FieldCanBeLocal", "unused"}) +public class BPSuperClass { + + private final String name; + private final int notOnSubClass = 11; + + public BPSuperClass(final String name) { + this.name = name; + } +} diff --git a/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java b/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java new file mode 100644 index 0000000..fcbd756 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java @@ -0,0 +1,194 @@ +package com.intergral.deep.test.target; + +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.util.Random; + +/** + * This is a target for many test breakpoints - be careful with line numbers, and do not format the code. + *

+ * The methods ending in _visited are what the code will be converted to (only includes the line ends) and is useful when using the asm + * plugin to see the code. + */ +// @formatter:off +@SuppressWarnings("ALL") public class BPTestTarget extends BPSuperClass +{ + private String name; + + + public BPTestTarget( final String name, int i ) + { + super("i am a name" + name ); + i += 1; + this.name = name; + } + + + public String getName_visited() + { + if( name == null ) + { + try{return null;}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + return name; + } + + public void setName_visited( final String name ) + { + try{this.name = name;}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + + public String getName() + { + if( name == null ) + { + return null; + } + return name; + } + + public void setName( final String name ) + { + this.name = name; + } + + + public String callSomeThing_visited( final String val ) + { + try{return getName() + val;}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + + + public String callSomeThing( final String val ) + { + return getName() + val; + } + + + public int errorSomething_visited( final String withargs ) + { + try{return withargs.length();}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + + + public int errorSomething( final String withargs ) + { + return withargs.length(); + } + + + public int throwSomething_visited( final String withargs ) + { + try{throw new RuntimeException(withargs);}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + + + public int throwSomething( final String withargs ) + { + throw new RuntimeException( withargs ); + } + + + public String catchSomething_visited( final String withargs ) + { + try + { + throw new RuntimeException( withargs ); + } + catch( RuntimeException re ) + { + try{return re.getMessage();}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + } + + + public String catchSomething( final String withargs ) + { + try + { + throw new RuntimeException( withargs ); + } + catch( RuntimeException re ) + { + return re.getMessage(); + } + } + + + public String finallySomething_visited( final String withargs ) + { + try + { + throw new RuntimeException( withargs ); + } + catch( RuntimeException re ) + { + return re.getMessage(); + } + finally + { + try{System.out.println("something in finally");}catch( Throwable t ){ Callback.callBackException( t ); throw t; }finally {Callback.callBackFinally(null,null);} + } + } + + + public String finallySomething( final String withargs ) + { + try + { + throw new RuntimeException( withargs ); + } + catch( RuntimeException re ) + { + return re.getMessage(); + } + finally + { + System.out.println( "something in finally" ); + } + } + + public String someFunctionWithABody(final String someArg){ + final String name = this.name; + final String newName = name + someArg; + final Random random = new Random(); + final int i = random.nextInt(); + return i + newName; + } + + + public void conditionalThrow( final int val, final int max ) throws Exception + { + if( val > max ) + { + throw new Exception( "Hit max executions " + val + " " + max ); + } + } + + + public void breakSomething() + { + for( int i = 0; i < 10; i++ ) + { + if( i == 5 ) + { + System.out.println( "do something" ); + break; + } + } + } + + + public void continueSomething() + { + for( int i = 0; i < 10; i++ ) + { + if( i == 5 ) + { + System.out.println( "do something" ); + continue; + } + System.out.println( "something else" ); + } + } +} +//@formatter:on diff --git a/agent/src/test/java/com/intergral/deep/test/target/ConditionTarget.java b/agent/src/test/java/com/intergral/deep/test/target/ConditionTarget.java new file mode 100644 index 0000000..b01b2ab --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/ConditionTarget.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test.target; + +public class ConditionTarget { + + public int i = 100; +} diff --git a/agent/src/test/java/com/intergral/deep/Person.java b/agent/src/test/java/com/intergral/deep/test/target/Person.java similarity index 96% rename from agent/src/test/java/com/intergral/deep/Person.java rename to agent/src/test/java/com/intergral/deep/test/target/Person.java index 5b3f1e4..536f1e4 100644 --- a/agent/src/test/java/com/intergral/deep/Person.java +++ b/agent/src/test/java/com/intergral/deep/test/target/Person.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.intergral.deep; +package com.intergral.deep.test.target; public class Person { diff --git a/agent/src/test/java/com/intergral/deep/test/target/VariableSuperSuperTest.java b/agent/src/test/java/com/intergral/deep/test/target/VariableSuperSuperTest.java new file mode 100644 index 0000000..01af4a7 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/VariableSuperSuperTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test.target; + +@SuppressWarnings("ALL") +public class VariableSuperSuperTest { + + public static int i = 11; +} diff --git a/agent/src/test/java/com/intergral/deep/test/target/VariableSuperTest.java b/agent/src/test/java/com/intergral/deep/test/target/VariableSuperTest.java new file mode 100644 index 0000000..e482ccf --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/VariableSuperTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test.target; + +@SuppressWarnings("ALL") +public class VariableSuperTest extends VariableSuperSuperTest { + + public static int i = 9; + protected static float f = 8.8f; + static long l = 8L; + private static double d = 9.8d; +} diff --git a/agent/src/test/java/com/intergral/deep/test/target/VariableTypeTarget.java b/agent/src/test/java/com/intergral/deep/test/target/VariableTypeTarget.java new file mode 100644 index 0000000..c99697e --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/target/VariableTypeTarget.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test.target; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@SuppressWarnings("ALL") +public class VariableTypeTarget extends VariableSuperTest { + + public static int i = 1; + protected static float f = 2.2f; + static long l = 2L; + private static double d = 1.2d; + private final Object object = new Object(); + public String str = + "str with a very large value will be truncated, needs to be over 1024 by default.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................." + + "str with a very large value will be truncated, needs to be over 1024 by default.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................." + + "str with a very large value will be truncated, needs to be over 1024 by default.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................."; + protected String[] arry = new String[]{"some", "thing"}; + Object nul = null; + private volatile List list = new ArrayList<>(); + private transient Set set = new HashSet<>(Arrays.asList("one", "two")); + + private Map simplemap = new HashMap<>(Collections.singletonMap("key", "value")); + private Map complexMap = new HashMap() {{ + put("key", "other"); + put("1", 1); + put("list", list); + }}; + + private Iterator iter = complexMap.entrySet().iterator(); + + private VariableTypeTarget someObj = this; + + public VariableTypeTarget() { + // this is here to ensure we have a line to install TP on for tests + nul = null; + } + + @Override + public String toString() { + return "VariableTypeTarget{}"; + } +} diff --git a/agent/src/test/java/lucee/commons/color/ConstantsDouble.java b/agent/src/test/java/lucee/commons/color/ConstantsDouble.java new file mode 100644 index 0000000..e1d34bb --- /dev/null +++ b/agent/src/test/java/lucee/commons/color/ConstantsDouble.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.commons.color; + +@SuppressWarnings("ALL") +public class ConstantsDouble { + + public static Double _100 = Double.valueOf(100.0d); +} diff --git a/agent/src/test/java/lucee/commons/lang/PCLBlock.java b/agent/src/test/java/lucee/commons/lang/PCLBlock.java new file mode 100644 index 0000000..334b107 --- /dev/null +++ b/agent/src/test/java/lucee/commons/lang/PCLBlock.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.commons.lang; + +@SuppressWarnings("ALL") +public class PCLBlock extends ClassLoader { + +} diff --git a/agent/src/test/java/lucee/loader/classloader/LuceeClassLoader.java b/agent/src/test/java/lucee/loader/classloader/LuceeClassLoader.java new file mode 100644 index 0000000..2037419 --- /dev/null +++ b/agent/src/test/java/lucee/loader/classloader/LuceeClassLoader.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.loader.classloader; + +@SuppressWarnings("ALL") +public class LuceeClassLoader extends ClassLoader { + +} diff --git a/agent/src/test/java/lucee/package-info.java b/agent/src/test/java/lucee/package-info.java new file mode 100644 index 0000000..d44ba6a --- /dev/null +++ b/agent/src/test/java/lucee/package-info.java @@ -0,0 +1,4 @@ +/** + * These packages are here to allow for using lucee classes in visitor tests + */ +package lucee; diff --git a/agent/src/test/java/lucee/runtime/PageContext.java b/agent/src/test/java/lucee/runtime/PageContext.java new file mode 100644 index 0000000..15c728a --- /dev/null +++ b/agent/src/test/java/lucee/runtime/PageContext.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime; + +import lucee.runtime.type.Collection.Key; +import lucee.runtime.type.scope.Undefined; + +@SuppressWarnings("ALL") +public class PageContext { + + private Object server = new Scope("server"); + private Object variables = new Scope("variables"); + private Object local = new Scope("local"); + private Object cookie = new Scope("cookie"); + private Object session = new Scope("session"); + private Object application = new Scope("application"); + private Object cgiR = new Scope("cgiR"); + private Object request = new Scope("request"); + private Object _form = new Scope("_form"); + private Object _url = new Scope("_url"); + private Object client = new Scope("client"); + private Object threads = new Scope("threads"); + + public void write(String str) { + + } + + public Undefined us() { + return new Undefined() { + @Override + public Object get(final Key k) { + return new Object(); + } + + @Override + public Object set(final Key ket, final Object obj) { + return obj; + } + }; + } + + public void outputStart() { + } + + public void outputEnd() { + } + + public static class Scope { + + private final String threads; + + + public Scope(final String threads) { + this.threads = threads; + } + + public boolean isInitalized() { + return false; + } + } +} diff --git a/agent/src/test/java/lucee/runtime/PageContextImpl.java b/agent/src/test/java/lucee/runtime/PageContextImpl.java new file mode 100644 index 0000000..6f90fbe --- /dev/null +++ b/agent/src/test/java/lucee/runtime/PageContextImpl.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime; + +@SuppressWarnings("ALL") +public class PageContextImpl { + + public String evaluate(final String expression) { + return expression; + } +} diff --git a/agent/src/test/java/lucee/runtime/PageImpl.java b/agent/src/test/java/lucee/runtime/PageImpl.java new file mode 100644 index 0000000..c555464 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/PageImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime; + +import lucee.runtime.type.UDFProperties; + +@SuppressWarnings("ALL") +public class PageImpl { + + protected UDFProperties[] udfs; + + protected void setPageSource(PageSource source) { + } + + public static class SomePageImpl extends PageImpl { + + } +} diff --git a/agent/src/test/java/lucee/runtime/PageSource.java b/agent/src/test/java/lucee/runtime/PageSource.java new file mode 100644 index 0000000..a600523 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/PageSource.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime; + +@SuppressWarnings("ALL") +public class PageSource { + +} diff --git a/agent/src/test/java/lucee/runtime/component/ImportDefintion.java b/agent/src/test/java/lucee/runtime/component/ImportDefintion.java new file mode 100644 index 0000000..44753a1 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/component/ImportDefintion.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.component; + +@SuppressWarnings("ALL") +public class ImportDefintion { + +} diff --git a/agent/src/test/java/lucee/runtime/exp/PageException.java b/agent/src/test/java/lucee/runtime/exp/PageException.java new file mode 100644 index 0000000..636b43c --- /dev/null +++ b/agent/src/test/java/lucee/runtime/exp/PageException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.exp; + +@SuppressWarnings("ALL") +public class PageException extends Exception { + +} diff --git a/agent/src/test/java/lucee/runtime/interpreter/VariableInterpreter.java b/agent/src/test/java/lucee/runtime/interpreter/VariableInterpreter.java new file mode 100644 index 0000000..864def5 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/interpreter/VariableInterpreter.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.interpreter; + +import lucee.runtime.type.ref.VariableReference; + +@SuppressWarnings("ALL") +public class VariableInterpreter { + + public static VariableReference getVariableReference(lucee.runtime.PageContext ctx, String name) { + return new VariableReference(); + } +} diff --git a/agent/src/test/java/lucee/runtime/op/Caster.java b/agent/src/test/java/lucee/runtime/op/Caster.java new file mode 100644 index 0000000..6b8805e --- /dev/null +++ b/agent/src/test/java/lucee/runtime/op/Caster.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.op; + +@SuppressWarnings("ALL") +public class Caster { + + public static Double toDouble(double d) { + return d; + } + + public static double toDoubleValue(Double d) { + return d; + } + + public static String toString(Object obj) { + return ""; + } +} diff --git a/agent/src/test/java/lucee/runtime/op/Operator.java b/agent/src/test/java/lucee/runtime/op/Operator.java new file mode 100644 index 0000000..10a87e1 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/op/Operator.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.op; + +@SuppressWarnings("ALL") +public class Operator { + + public static Double modulusRef(Object obj1, Object obj2) { + return 2.0d; + } + + public static int compare(double d1, double d2) { + return 0; + } +} diff --git a/agent/src/test/java/lucee/runtime/scope/MockLuceeScope.java b/agent/src/test/java/lucee/runtime/scope/MockLuceeScope.java new file mode 100644 index 0000000..ffda8d4 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/scope/MockLuceeScope.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.scope; + +import java.util.HashMap; + +@SuppressWarnings("ALL") +public class MockLuceeScope extends HashMap { + +} diff --git a/agent/src/test/java/lucee/runtime/type/Collection.java b/agent/src/test/java/lucee/runtime/type/Collection.java new file mode 100644 index 0000000..c8dd4d5 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/Collection.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type; + +@SuppressWarnings("ALL") +public class Collection { + + public static class Key { + + } +} diff --git a/agent/src/test/java/lucee/runtime/type/UDF.java b/agent/src/test/java/lucee/runtime/type/UDF.java new file mode 100644 index 0000000..5b064c1 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/UDF.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type; + +@SuppressWarnings("ALL") +public class UDF { + +} diff --git a/agent/src/test/java/lucee/runtime/type/UDFProperties.java b/agent/src/test/java/lucee/runtime/type/UDFProperties.java new file mode 100644 index 0000000..f58bebb --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/UDFProperties.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type; + +@SuppressWarnings("ALL") +public class UDFProperties { + +} diff --git a/agent/src/test/java/lucee/runtime/type/ref/VariableReference.java b/agent/src/test/java/lucee/runtime/type/ref/VariableReference.java new file mode 100644 index 0000000..375fe64 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/ref/VariableReference.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type.ref; + +@SuppressWarnings("ALL") +public class VariableReference { + + public void set(double d) { + } +} diff --git a/agent/src/test/java/lucee/runtime/type/scope/Undefined.java b/agent/src/test/java/lucee/runtime/type/scope/Undefined.java new file mode 100644 index 0000000..3be40bf --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/scope/Undefined.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type.scope; + +import lucee.runtime.type.Collection.Key; + +@SuppressWarnings("ALL") +public interface Undefined { + + Object get(Key k); + + Object set(Key ket, Object obj); +} diff --git a/agent/src/test/java/lucee/runtime/type/util/KeyConstants.java b/agent/src/test/java/lucee/runtime/type/util/KeyConstants.java new file mode 100644 index 0000000..4fa2cd5 --- /dev/null +++ b/agent/src/test/java/lucee/runtime/type/util/KeyConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package lucee.runtime.type.util; + +import lucee.runtime.type.Collection.Key; + +@SuppressWarnings("ALL") +public class KeyConstants { + + public static final Key _I = new Key(); +} diff --git a/agent/src/test/java/railo/commons/lang/PCLBlock.java b/agent/src/test/java/railo/commons/lang/PCLBlock.java new file mode 100644 index 0000000..487ff5c --- /dev/null +++ b/agent/src/test/java/railo/commons/lang/PCLBlock.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package railo.commons.lang; + +@SuppressWarnings("ALL") +public class PCLBlock extends ClassLoader { + +} diff --git a/agent/src/test/java/railo/loader/classloader/RailoClassLoader.java b/agent/src/test/java/railo/loader/classloader/RailoClassLoader.java new file mode 100644 index 0000000..7f3913a --- /dev/null +++ b/agent/src/test/java/railo/loader/classloader/RailoClassLoader.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package railo.loader.classloader; + +@SuppressWarnings("ALL") +public class RailoClassLoader extends ClassLoader { + +} diff --git a/agent/src/test/resources/2_stratum.smap b/agent/src/test/resources/2_stratum.smap new file mode 100644 index 0000000..e18f012 --- /dev/null +++ b/agent/src/test/resources/2_stratum.smap @@ -0,0 +1,20 @@ +SMAP +Hi.java +Java +*S Foo +*F +1 Hi.foo +2 Incl.foo +*L +1#1,1:1,1 +2#1,4:6,2 +1#2,2:2,2 +*S Bar +*F +1 Hi.bar +2 Incl.bar +*L +1#1:1 +1#2,4:2 +3#1,8:6 +*E \ No newline at end of file diff --git a/agent/src/test/resources/cftestFile2ecfm137384933.class b/agent/src/test/resources/cftestFile2ecfm137384933.class new file mode 100644 index 0000000..4b1fde8 Binary files /dev/null and b/agent/src/test/resources/cftestFile2ecfm137384933.class differ diff --git a/agent/src/test/resources/cftestList2ecfm1060358347.class b/agent/src/test/resources/cftestList2ecfm1060358347.class new file mode 100644 index 0000000..977b8f2 Binary files /dev/null and b/agent/src/test/resources/cftestList2ecfm1060358347.class differ diff --git a/agent/src/test/resources/include_time.smap b/agent/src/test/resources/include_time.smap new file mode 100644 index 0000000..c8cffca --- /dev/null +++ b/agent/src/test/resources/include_time.smap @@ -0,0 +1,35 @@ +SMAP +include_005ftime_jsp.java +JSP +*S JSP +*F ++ 0 header.jsp +jsp/../header.jsp ++ 1 include_time.jsp +jsp/include_time.jsp ++ 2 time.jsp +jsp/time.jsp ++ 3 footer.jsp +jsp/../footer.jsp +*L +1,5:70 +6,4:76 +9,2:80 +11:82,5 +12,2:87 +1#1,2:88 +1#2:90,2 +3#1,2:92 +6,2:95 +7,2:97 +1#2:99,2 +9#1,2:101 +11,2:103,2 +13:107 +1#3,3:108 +4:111,3 +5,4:114 +9,17:119 +25,5:136 +14#1:140 +*E diff --git a/agent/src/test/resources/include_time2.smap b/agent/src/test/resources/include_time2.smap new file mode 100644 index 0000000..5483877 --- /dev/null +++ b/agent/src/test/resources/include_time2.smap @@ -0,0 +1,39 @@ +SMAP +include_005ftime_jsp.java +JSP +*S JSP +*F ++ 0 header.jsp +smap/../header.jsp ++ 1 include_time.jsp +smap/include_time.jsp ++ 2 time.jsp +smap/includes/time.jsp ++ 3 footer.jsp +smap/../footer.jsp +*L +1,5:70 +6,4:76 +9,2:80 +11:82,5 +12,2:87 +1#1,4:88 +1#2:91,2 +4#1,2:93 +7,2:96 +8,5:98 +1#2:102,2 +12#1,4:104 +15,2:107,2 +17,4:111 +1#2:114,2 +20#1:116 +1#2:117,2 +21#1,2:119 +1#3,3:121 +4:124,3 +5,4:127 +9,17:132 +25,5:149 +23#1:153 +*E diff --git a/agent/src/test/resources/include_time3.smap b/agent/src/test/resources/include_time3.smap new file mode 100644 index 0000000..748a597 --- /dev/null +++ b/agent/src/test/resources/include_time3.smap @@ -0,0 +1,37 @@ +SMAP +include_005ftime_jsp.java +JSP +*S JSP +*F ++ 0 header.jsp +smap/../header.jsp ++ 1 include_time.jsp +smap/include_time.jsp ++ 2 time.jsp +smap/includes/time.jsp ++ 3 footer.jsp +smap/../footer.jsp +*L +1,5:70 +6,4:76 +9,2:80 +11:82,5 +12,2:87 +1#1,4:88 +1#2:91,2 +4#1,2:93 +7,2:96 +8,5:98 +1#2:102,2 +12#1,4:104 +15:107,3 +16,4:110 +1#2:113,4 +19#1,2:117 +1#3,3:119 +4:122,3 +5,4:125 +9,17:130 +25,5:147 +21#1:151 +*E diff --git a/agent/src/test/resources/index2_jsp.smap b/agent/src/test/resources/index2_jsp.smap new file mode 100644 index 0000000..7eaad8f --- /dev/null +++ b/agent/src/test/resources/index2_jsp.smap @@ -0,0 +1,13 @@ +SMAP +index2_jsp.java +JSP +*S JSP +*F ++ 0 index2.jsp +index2.jsp +*L +123:207 +130,3:210 +140:250,7 +160,3:300,2 +*E \ No newline at end of file diff --git a/agent/src/test/resources/index_jsp.smap b/agent/src/test/resources/index_jsp.smap new file mode 100644 index 0000000..0f2ed37 --- /dev/null +++ b/agent/src/test/resources/index_jsp.smap @@ -0,0 +1,27 @@ +SMAP +index_jsp.java +JSP +*S JSP +*F ++ 0 header.jsp +header.jsp ++ 1 index.jsp +index.jsp ++ 2 footer.jsp +footer.jsp +*L +1,5:71 +6,4:77 +9,2:81 +11:83,5 +12,2:88 +1#1,9:89 +10,33:99 +42,4:132 +1#2,3:135 +4:138,3 +5,4:141 +9,17:146 +25,5:163 +45#1:167,2 +*E diff --git a/agent/src/test/resources/logging.properties b/agent/src/test/resources/logging.properties new file mode 100644 index 0000000..8d1a982 --- /dev/null +++ b/agent/src/test/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (C) 2023 Intergral GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +.level=FINEST +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST diff --git a/agent/src/test/resources/noop.smap b/agent/src/test/resources/noop.smap new file mode 100644 index 0000000..46c6d75 --- /dev/null +++ b/agent/src/test/resources/noop.smap @@ -0,0 +1,4 @@ +SMAP +noop_jsp.java +JSP +null*E \ No newline at end of file diff --git a/agent/src/test/resources/org/apache/jsp/jdbc/views/companies_jsp.class b/agent/src/test/resources/org/apache/jsp/jdbc/views/companies_jsp.class new file mode 100644 index 0000000..22c5962 Binary files /dev/null and b/agent/src/test/resources/org/apache/jsp/jdbc/views/companies_jsp.class differ diff --git a/agent/src/test/resources/org/apache/jsp/tests/string_jsp.class b/agent/src/test/resources/org/apache/jsp/tests/string_jsp.class new file mode 100755 index 0000000..47736f9 Binary files /dev/null and b/agent/src/test/resources/org/apache/jsp/tests/string_jsp.class differ diff --git a/agent/src/test/resources/testfile_cfm$cf.class b/agent/src/test/resources/testfile_cfm$cf.class new file mode 100644 index 0000000..ea1eb7d Binary files /dev/null and b/agent/src/test/resources/testfile_cfm$cf.class differ diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d63945c..71fbc26 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -20,7 +20,36 @@ "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" "https://checkstyle.org/dtds/suppressions_1_2.dtd"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/checkstyle.xml b/checkstyle.xml index e5e6bc0..2945096 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -344,7 +344,6 @@ - @@ -404,4 +403,8 @@ + + + + \ No newline at end of file diff --git a/deep/src/main/java/com/intergral/deep/Deep.java b/deep/src/main/java/com/intergral/deep/Deep.java index c07361f..5fb8a14 100644 --- a/deep/src/main/java/com/intergral/deep/Deep.java +++ b/deep/src/main/java/com/intergral/deep/Deep.java @@ -45,12 +45,21 @@ public class Deep { /** - * This is a shortcut for {@code Deep.config().start()} + * This is a shortcut for {@code Deep.config().start()}. */ public static void start() { Deep.config().start(); } + /** + * This allows deep to be started with the parsed config builder. + * + * @param builder the config to use + */ + public void start(final DeepConfigBuilder builder) { + builder.start(); + } + /** * This will create an instance of DEEP allowing access to the APIs from inside deep agent. * @@ -76,7 +85,7 @@ public static DeepConfigBuilder config() { } /** - * This allows deep to be started with the parsed config + * This allows deep to be started with the parsed config. * * @param config as a string */ @@ -84,14 +93,6 @@ void startWithConfig(final String config, final String jarPath) { getInstance().startDeep(config, jarPath); } - /** - * This allows deep to be started with the parsed config builder - * - * @param builder the config to use - */ - public void start(final DeepConfigBuilder builder) { - builder.start(); - } private void startDeep(final String config, final String jarPath) { try { @@ -112,7 +113,7 @@ private void loadAgent(final String config, final String jarPath) throws Throwab /** - * Get the process id for the current process + * Get the process id for the current process. * * @return the process id */ @@ -123,7 +124,7 @@ String getPid() { /** - * Load the loader for NerdVision + * Load the loader for NerdVision. * * @return the loader to use * @throws Throwable if we cannot load the loader @@ -218,8 +219,8 @@ public T api() { *

* This uses T as the type {@link com.intergral.deep.agent.api.reflection.IReflection} is not loaded so this class cannot use it. * - * @return the {@link com.intergral.deep.agent.api.reflection.IReflection} service * @param this should be {@link com.intergral.deep.agent.api.reflection.IReflection} + * @return the {@link com.intergral.deep.agent.api.reflection.IReflection} service */ public T reflection() { loadAPI(); diff --git a/deep/src/main/java/com/intergral/deep/DEEPAPI.java b/deep/src/main/java/com/intergral/deep/DeepAPI.java similarity index 94% rename from deep/src/main/java/com/intergral/deep/DEEPAPI.java rename to deep/src/main/java/com/intergral/deep/DeepAPI.java index f827771..f79b730 100644 --- a/deep/src/main/java/com/intergral/deep/DEEPAPI.java +++ b/deep/src/main/java/com/intergral/deep/DeepAPI.java @@ -24,10 +24,10 @@ * This type provides helper methods to get the api and other exposed APIs from deep. This type MUST not be used until after the agent is * loaded or there will be Exceptions thrown. */ -public class DEEPAPI { +public class DeepAPI { /** - * Get the reflection API + * Get the reflection API. * * @return a {@link IReflection} instance for the java version we are running */ @@ -36,7 +36,7 @@ public static IReflection reflection() { } /** - * Get the Deep api + * Get the Deep api. * * @return a {@link IDeep} instance */ diff --git a/deep/src/main/java/com/intergral/deep/DeepConfigBuilder.java b/deep/src/main/java/com/intergral/deep/DeepConfigBuilder.java index 1abe153..4dd7ac6 100644 --- a/deep/src/main/java/com/intergral/deep/DeepConfigBuilder.java +++ b/deep/src/main/java/com/intergral/deep/DeepConfigBuilder.java @@ -20,13 +20,16 @@ import java.util.HashMap; import java.util.Map; +/** + * Builder to create deep config. + */ public class DeepConfigBuilder { private final Map config = new HashMap<>(); private String jarPath; /** - * Start Deep using this config + * Start Deep using this config. */ public void start() { Deep.getInstance().startWithConfig(this.build(), this.jarPath); @@ -96,7 +99,8 @@ public DeepConfigBuilder setValue(final String key, final double value) { } /** - * Converts this object into a string that can be used by the attachment process + * Converts this object into a string that can be used by the attachment process. + * * @return a string for this config */ private String build() { @@ -104,7 +108,7 @@ private String build() { } /** - * Convert the config to a string + * Convert the config to a string. * * @param config the config to parse * @return the config as a string diff --git a/deep/src/main/java/com/intergral/deep/DeepLoader.java b/deep/src/main/java/com/intergral/deep/DeepLoader.java index 9b07e4c..730bc86 100644 --- a/deep/src/main/java/com/intergral/deep/DeepLoader.java +++ b/deep/src/main/java/com/intergral/deep/DeepLoader.java @@ -24,6 +24,9 @@ import java.io.InputStream; import net.bytebuddy.agent.ByteBuddyAgent; +/** + * Custom loader to attach deep to the running process. + */ public class DeepLoader implements IDeepLoader { @Override @@ -54,7 +57,7 @@ private File getToolsJar() { /** - * Load the agent jar to a file + * Load the agent jar to a file. * * @return the {@link File} object for the agent */ @@ -75,7 +78,7 @@ private File getAgentJar(final String jarPath) { /** - * Log the agent file as a stream + * Log the agent file as a stream. * * @return the stream to use, or {@code null} */ @@ -85,7 +88,7 @@ private InputStream getAgentJarStream() { /** - * Extract a stream to a temp file and return the absolute file path + * Extract a stream to a temp file and return the absolute file path. * * @param inputStream the stream to extract * @return the absolute file path to the extracted library diff --git a/deep/src/main/java/com/intergral/deep/ShippedToolsJarProvider.java b/deep/src/main/java/com/intergral/deep/ShippedToolsJarProvider.java index 8fa8554..be4fcd9 100644 --- a/deep/src/main/java/com/intergral/deep/ShippedToolsJarProvider.java +++ b/deep/src/main/java/com/intergral/deep/ShippedToolsJarProvider.java @@ -33,7 +33,7 @@ public class ShippedToolsJarProvider implements ByteBuddyAgent.AttachmentProvide /** - * Create a new provider + * Create a new provider. * * @param tools the file object for the tools jar to use */ diff --git a/examples/.gitignore b/examples/.gitignore index 5ff6309..af665ab 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -35,4 +35,4 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/examples/agent-load/src/main/java/com/intergral/deep/examples/Main.java b/examples/agent-load/src/main/java/com/intergral/deep/examples/Main.java index 8c933bb..e024e86 100644 --- a/examples/agent-load/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/agent-load/src/main/java/com/intergral/deep/examples/Main.java @@ -18,7 +18,7 @@ package com.intergral.deep.examples; -import com.intergral.deep.DEEPAPI; +import com.intergral.deep.DeepAPI; import com.intergral.deep.agent.api.resource.Resource; import java.util.Collections; @@ -33,23 +33,29 @@ */ public class Main { + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ public static void main(String[] args) throws Throwable { // Use the API to show the version of deep that is running // If the Deep agent is not loaded then you will get an exception on this line // java.lang.IllegalStateException: Must start Deep first! - System.out.println(DEEPAPI.api().getVersion()); + System.out.println(DeepAPI.api().getVersion()); // Use the API to register a plugin // This plugin will attach the attribute 'example' to the created snapshot // you should also see the log line 'custom plugin' when you run this example - DEEPAPI.api().registerPlugin((settings, snapshot) -> { + DeepAPI.api().registerPlugin((settings, snapshot) -> { System.out.println("custom plugin"); return Resource.create(Collections.singletonMap("example", "agent_load")); }); // USe the API to create a tracepoint that will fire forever - DEEPAPI.api() + DeepAPI.api() .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, Collections.singletonMap("fire_count", "-1"), Collections.emptyList()); diff --git a/examples/docker/coldfusion/.gitignore b/examples/docker/coldfusion/.gitignore deleted file mode 100644 index bd3f22b..0000000 --- a/examples/docker/coldfusion/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -jvm.config -testFile.cfm \ No newline at end of file diff --git a/examples/docker/coldfusion/jvm.config b/examples/docker/coldfusion/jvm.config new file mode 100644 index 0000000..442c3ff --- /dev/null +++ b/examples/docker/coldfusion/jvm.config @@ -0,0 +1,29 @@ +# +# VM configuration +# +# Where to find JVM, if {java.home}/jre exists then that JVM is used +# if not then it must be the path to the JRE itself + +java.home=/opt/coldfusion/jre + +# +# If no java.home is specified a VM is located by looking in these places in this +# order: +# +# 1) ../runtime/jre +# 2) registry (windows only) +# 3) JAVA_HOME env var plus jre (ie $JAVA_HOME/jre) +# 4) java.exe in path +# + +# Arguments to VM + +java.args=-javaagent:/opt/deep/deep.jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -server --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.util.cldr=ALL-UNNAMED --add-opens=java.base/sun.util.locale.provider=ALL-UNNAMED -Xms256m -Xmx1024m -XX:MaxMetaspaceSize=192m -XX:+UseParallelGC -Djdk.attach.allowAttachSelf=true -Dcoldfusion.home={application.home} -Djava.security.egd=/dev/urandom -Djava.awt.headless=true -Duser.language=en -Dcoldfusion.rootDir={application.home} -Dcom.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize=true -Djava.security.policy={application.home}/lib/coldfusion.policy -Djava.security.auth.policy={application.home}/lib/neo_jaas.policy -Dcoldfusion.classPath={application.home}/lib/updates,{application.home}/lib,{application.home}/lib/axis2,{application.home}/gateway/lib/,{application.home}/wwwroot/WEB-INF/cfform/jars,{application.home}/wwwroot/WEB-INF/flex/jars,{application.home}/lib/oosdk/lib,{application.home}/lib/oosdk/classes -Dcoldfusion.libPath={application.home}/lib -Dorg.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true -Dcoldfusion.jsafe.defaultalgo=FIPS186Random -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.JavaUtilLog -Djava.util.logging.config.file={application.home}/lib/logging.properties -Djava.locale.providers=COMPAT,SPI -Dsun.font.layoutengine=icu + +# Comma separated list of shared library path +java.library.path={application.home}/lib/international + +# Comma separated list of shared library path for non-windows +java.nixlibrary.path={application.home}/lib + +java.class.path={application.home}/lib/oosdk/lib,{application.home}/lib/oosdk/classes diff --git a/examples/docker/coldfusion/testFile.cfm b/examples/docker/coldfusion/testFile.cfm new file mode 100644 index 0000000..a4a683d --- /dev/null +++ b/examples/docker/coldfusion/testFile.cfm @@ -0,0 +1,3 @@ + + +#i# diff --git a/examples/docker/lucee/Dockerfile b/examples/docker/lucee/Dockerfile new file mode 100644 index 0000000..5ccdd6c --- /dev/null +++ b/examples/docker/lucee/Dockerfile @@ -0,0 +1,3 @@ +FROM lucee/lucee:5.4 + +COPY testFile.cfm /var/www/testFile.cfm \ No newline at end of file diff --git a/examples/docker/lucee/testFile.cfm b/examples/docker/lucee/testFile.cfm new file mode 100644 index 0000000..a4a683d --- /dev/null +++ b/examples/docker/lucee/testFile.cfm @@ -0,0 +1,3 @@ + + +#i# diff --git a/examples/dynamic-load/src/main/java/com/intergral/deep/examples/Main.java b/examples/dynamic-load/src/main/java/com/intergral/deep/examples/Main.java index a336fec..41ed7bd 100644 --- a/examples/dynamic-load/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/dynamic-load/src/main/java/com/intergral/deep/examples/Main.java @@ -18,8 +18,8 @@ package com.intergral.deep.examples; -import com.intergral.deep.DEEPAPI; import com.intergral.deep.Deep; +import com.intergral.deep.DeepAPI; import com.intergral.deep.agent.api.IDeep; import com.intergral.deep.agent.api.reflection.IReflection; import com.intergral.deep.agent.api.resource.Resource; @@ -39,6 +39,12 @@ */ public class Main { + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ public static void main(String[] args) throws Throwable { // this is only needed in this example as we are using a local built module // if using the dependency from maven you do not need to set the path @@ -53,7 +59,7 @@ public static void main(String[] args) throws Throwable { Deep.config() .setJarPath(jarPath.toAbsolutePath().toString()) .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") - .setValue("service.secure", false) + .setValue(ISettings.KEY_SERVICE_SECURE, false) .start(); // different ways to get the API instance @@ -61,20 +67,21 @@ public static void main(String[] args) throws Throwable { System.out.println(instance.api().getVersion()); System.out.println(instance.reflection()); - System.out.println(DEEPAPI.api().getVersion()); - System.out.println(DEEPAPI.reflection()); + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); // Use the API to register a plugin // This plugin will attach the attribute 'example' to the created snapshot // you should also see the log line 'custom plugin' when you run this example - DEEPAPI.api().registerPlugin((settings, snapshot) -> { + DeepAPI.api().registerPlugin((settings, snapshot) -> { System.out.println("custom plugin"); return Resource.create(Collections.singletonMap("example", "dynamic_load")); }); // USe the API to create a tracepoint that will fire forever - DEEPAPI.api() - .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, Collections.singletonMap("fire_count", "-1"), Collections.emptyList()); + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + Collections.singletonMap("fire_count", "-1"), Collections.emptyList()); final SimpleTest ts = new SimpleTest("This is a test", 2); for (; ; ) { diff --git a/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/CFPlugin.java b/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/CFPlugin.java index 40d6bb4..5925a64 100644 --- a/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/CFPlugin.java +++ b/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/CFPlugin.java @@ -17,12 +17,17 @@ package com.intergral.deep.plugins.cf; -import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.resource.Resource; import com.intergral.deep.agent.api.settings.ISettings; import java.util.HashMap; +/** + * This plugin is activated when we are running on an adobe CF server. + *

+ * This plugin will attach the cf version and the cf app name to the captured snapshots. + */ public class CFPlugin implements IPlugin { @Override diff --git a/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/Utils.java b/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/Utils.java index 7759d4d..64c30cf 100644 --- a/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/Utils.java +++ b/plugins/cf-plugin/src/main/java/com/intergral/deep/plugins/cf/Utils.java @@ -17,12 +17,30 @@ package com.intergral.deep.plugins.cf; -public class Utils { +/** + * A small collection of utils used to capture the adobe coldfusion version number. + */ +public final class Utils { + + private Utils() { + } + /** + * Are we running on a CF server. + *

+ * By looking at the java start up command we can tell if this is a CF server. + * + * @return {@code true} if we are on a coldfusion server. + */ public static boolean isCFServer() { return System.getProperty("sun.java.command").contains("coldfusion"); } + /** + * Try to load the coldfusion version number. + * + * @return the major version of adobe coldfusion. + */ public static String loadCFVersion() { try { return String.valueOf(Thread.currentThread() diff --git a/plugins/java-plugin/src/main/java/com/intergral/deep/plugin/JavaPlugin.java b/plugins/java-plugin/src/main/java/com/intergral/deep/plugin/JavaPlugin.java index 0d3e5e7..8f8c049 100644 --- a/plugins/java-plugin/src/main/java/com/intergral/deep/plugin/JavaPlugin.java +++ b/plugins/java-plugin/src/main/java/com/intergral/deep/plugin/JavaPlugin.java @@ -17,13 +17,16 @@ package com.intergral.deep.plugin; -import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.plugin.IPlugin; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; import com.intergral.deep.agent.api.resource.Resource; import com.intergral.deep.agent.api.settings.ISettings; import java.util.HashMap; import java.util.Map; +/** + * This plugin captures the thread name of the thread the snapshot was cpatured on. + */ public class JavaPlugin implements IPlugin { private final Map basic = new HashMap<>(); diff --git a/pom.xml b/pom.xml index 42da979..7bde4ef 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,7 @@ sign + ${env.GPG_PASSPHRASE} @@ -285,11 +286,13 @@ checkstyle.xml + LICENSE.txt true true true true false + warning @@ -343,6 +346,90 @@ + + coverage + + false + + + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + + prepare-agent + + + + + report + test + + report + report-aggregate + + + + + **/java/com/intergral/deep/** + + **/*InsnPrinter.* + + **/com/intergral/deep/agent/Agent.* + + + + + default-check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0 + + + BRANCH + COVEREDRATIO + 0 + + + CLASS + MISSEDCOUNT + 0 + + + METHOD + MISSEDCOUNT + 0 + + + + + + + **/java/com/intergral/deep/** + + **/*InsnPrinter.* + + **/com/intergral/deep/agent/Agent.* + + + + + + + + @@ -434,6 +521,11 @@ junit-jupiter 5.9.3 + + org.mockito + mockito-core + 5.4.0 + @@ -443,6 +535,11 @@ junit-jupiter test + + org.mockito + mockito-core + test + diff --git a/reflect-api/src/main/java/com/intergral/deep/reflect/ReflectionImpl.java b/reflect-api/src/main/java/com/intergral/deep/reflect/ReflectionImpl.java index b9a882f..288e8e2 100644 --- a/reflect-api/src/main/java/com/intergral/deep/reflect/ReflectionImpl.java +++ b/reflect-api/src/main/java/com/intergral/deep/reflect/ReflectionImpl.java @@ -17,18 +17,24 @@ package com.intergral.deep.reflect; +import com.intergral.deep.agent.api.DeepRuntimeException; import com.intergral.deep.agent.api.reflection.IReflection; import com.intergral.deep.agent.api.utils.ArrayIterator; import com.intergral.deep.agent.api.utils.CompoundIterator; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.Set; import java.util.stream.Collectors; +/** + * The version of reflection used before modules where added. + */ public class ReflectionImpl implements IReflection { @Override @@ -50,7 +56,17 @@ public boolean setAccessible(final Class clazz, final Method method) { } catch (final Exception e) { return false; } + } + + @Override + public boolean setAccessible(final Class clazz, final Constructor constructor) { + try { + constructor.setAccessible(true); + return true; + } catch (final Exception e) { + return false; + } } @@ -66,6 +82,7 @@ public T callMethod(final Object target, final String methodName, final Obje if (method != null) { try { setAccessible(target.getClass(), method); + //noinspection unchecked return (T) method.invoke(target, args); } catch (IllegalAccessException | InvocationTargetException e) { return null; @@ -94,7 +111,7 @@ public Method findMethod(final Class originalClazz, final String methodName, return null; } - + @Override public Field getField(final Object obj, final String fieldName) { Class clazz = obj.getClass(); while (clazz != null) { @@ -141,7 +158,6 @@ public Field next() { } final Field[] fields = clazz.getFields(); final Field[] declaredFields = clazz.getDeclaredFields(); - //noinspection unchecked return new CompoundIterator<>(new ArrayIterator<>(fields), new ArrayIterator<>(declaredFields)); } @@ -162,7 +178,30 @@ public T callField(final Object target, final Field field) { @Override public Set getModifiers(final Field field) { final int modifiers = field.getModifiers(); + if (modifiers == 0) { + return Collections.emptySet(); + } final String string = Modifier.toString(modifiers); return Arrays.stream(string.split(" ")).collect(Collectors.toSet()); } + + @Override + public Constructor findConstructor(final Class clazz, final Class... args) { + try { + return clazz.getConstructor(args); + } catch (NoSuchMethodException e) { + return null; + } + } + + @Override + public T callConstructor(final Constructor constructor, final Object... args) { + try { + setAccessible(constructor.getDeclaringClass(), constructor); + //noinspection unchecked + return (T) constructor.newInstance(args); + } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) { + throw new DeepRuntimeException("Cannot call constructor: " + constructor, e); + } + } } diff --git a/reflect-java-9/src/main/java/com/intergral/deep/reflect/Java9ReflectionImpl.java b/reflect-java-9/src/main/java/com/intergral/deep/reflect/Java9ReflectionImpl.java index bba84a1..577da4a 100644 --- a/reflect-java-9/src/main/java/com/intergral/deep/reflect/Java9ReflectionImpl.java +++ b/reflect-java-9/src/main/java/com/intergral/deep/reflect/Java9ReflectionImpl.java @@ -17,9 +17,13 @@ package com.intergral.deep.reflect; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +/** + * The version of reflection that deals with modules. + */ public class Java9ReflectionImpl extends ReflectionImpl { @Override @@ -48,6 +52,19 @@ public boolean setAccessible(final Class clazz, final Method method) { } + @Override + public boolean setAccessible(final Class clazz, final Constructor constructor) { + try { + openModule(clazz); + + constructor.setAccessible(true); + return true; + } catch (final Exception e) { + return false; + } + } + + private void openModule(final Class clazz) { final Module m = clazz.getModule(); if (m.isNamed()) { diff --git a/test-utils/pom.xml b/test-utils/pom.xml index 98c63ec..0f963a4 100644 --- a/test-utils/pom.xml +++ b/test-utils/pom.xml @@ -52,6 +52,10 @@ grpc-protobuf provided - + + org.junit.jupiter + junit-jupiter + compile + \ No newline at end of file diff --git a/test-utils/src/main/java/com/intergral/deep/tests/AssertUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/AssertUtils.java new file mode 100644 index 0000000..83dfa19 --- /dev/null +++ b/test-utils/src/main/java/com/intergral/deep/tests/AssertUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.tests; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Collection; + +public class AssertUtils { + + /** + * Assert that a collection contains an item that matches the function. + * + * @param list the collection to scan + * @param compareFunction the function to run + * @param the type of items in the collection + * @return the result of the compare + */ + public static int assertContains(final Collection list, final ICompareFunction compareFunction) { + int index = -1; + for (final T listItem : list) { + index++; + if (compareFunction.compare(listItem)) { + return index; + } + } + fail(String.format("Cannot find %s in list %s", compareFunction, list)); + return -1; + } + + public interface ICompareFunction { + + boolean compare(T item); + } +} diff --git a/test-utils/src/main/java/com/intergral/deep/tests/grpc/TestInterceptor.java b/test-utils/src/main/java/com/intergral/deep/tests/grpc/TestInterceptor.java new file mode 100644 index 0000000..e398e6b --- /dev/null +++ b/test-utils/src/main/java/com/intergral/deep/tests/grpc/TestInterceptor.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.tests.grpc; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +public class TestInterceptor implements ServerInterceptor { + + private final String key; + private Context.Key contextKey; + + public TestInterceptor(final String key) { + this.key = key; + } + + public Context.Key contextKey() { + if (contextKey == null) { + contextKey = Context.key(key); + } + return contextKey; + } + + @Override + public Listener interceptCall(final ServerCall call, final Metadata headers, + final ServerCallHandler next) { + final Key grpcKey = Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + final String val = headers.get(grpcKey); + final Context context = Context.current().withValue(contextKey(), val); + return Contexts.interceptCall(context, call, headers, next); + } +} diff --git a/test-utils/src/main/java/com/intergral/deep/tests/inst/ByteClassLoader.java b/test-utils/src/main/java/com/intergral/deep/tests/inst/ByteClassLoader.java new file mode 100644 index 0000000..0e7322a --- /dev/null +++ b/test-utils/src/main/java/com/intergral/deep/tests/inst/ByteClassLoader.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.tests.inst; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class ByteClassLoader extends ClassLoader { + + private final Map bytes = new HashMap<>(); + + public static byte[] loadBytes(final String name) throws IOException { + final byte[] bytes; + try (InputStream resourceAsStream = ByteClassLoader.class.getResourceAsStream("/" + name + ".class")) { + bytes = new byte[resourceAsStream.available()]; + resourceAsStream.read(bytes); + } + return bytes; + } + + public static ByteClassLoader forFile(final String name) throws IOException { + final byte[] loadedBytes = loadBytes(name); + final ByteClassLoader byteClassLoader = new ByteClassLoader(); + byteClassLoader.setBytes(name, loadedBytes); + return byteClassLoader; + } + + public void setBytes(final String name, final byte[] bytes) { + this.bytes.put(name, bytes); + } + + public byte[] getBytes(final String name) { + return this.bytes.get(name); + } + + @Override + public Class loadClass(final String name) throws ClassNotFoundException { + final byte[] bytes = this.bytes.get(name); + if (bytes != null) { + return defineClass(name, bytes, 0, bytes.length); + } + return super.loadClass(name); + } + + @Override + protected Class findClass(final String name) throws ClassNotFoundException { + final byte[] bytes = this.bytes.get(name); + if (bytes != null) { + return defineClass(name, bytes, 0, bytes.length); + } + return super.findClass(name); + } +} diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index e95da64..266d801 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,14 +17,29 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; -import java.util.List; +import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.Optional; -public class SnapshotUtils { +public final class SnapshotUtils { + + private SnapshotUtils() { + } + + /** + * Scan a snapshot for a variable with the given name. + * + * @param name the variable name + * @param snapshot the snapshot + * @return a {@link IVariableScan} + */ + public static IVariableScan findVarByName(final String name, final Snapshot snapshot) { + return findVarByName(name, snapshot.getFrames(0).getVariablesList(), snapshot.getVarLookupMap()); + } /** * Scan a snapshot for a variable with a given name. @@ -35,7 +50,7 @@ public class SnapshotUtils { * @return a {@link IVariableScan} */ public static IVariableScan findVarByName(final String name, - final List localVars, + final Collection localVars, final Map lookup) { final Optional first = localVars.stream() .filter(variableID -> Objects.equals(variableID.getName(), name))