Skip to content

Commit

Permalink
Merge pull request #121 from cqse/agent-library
Browse files Browse the repository at this point in the history
Agent library
  • Loading branch information
DreierF authored Mar 24, 2020
2 parents b2bdbc7 + 3dd0bef commit f0cd2dd
Show file tree
Hide file tree
Showing 53 changed files with 1,437 additions and 244 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
**/test-tmp/**

*.swp
report-generator/test-coverage-*.xml
1 change: 1 addition & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
We use [semantic versioning][semver]

# Next Release
- [feature] add TIA client library for integrating TIA in your custom test framework
- [fix] Delete empty coverage directories

# 15.4.0
Expand Down
69 changes: 54 additions & 15 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ echo `git rev-parse --abbrev-ref HEAD`:`git --no-pager log -n1 --format="%ct000"
- `validate-ssl` (optional): by default the agent will accept any SSL certificate. This enables a fast setup of the agent
even in the face of broken or self-signed certificates. If you need to validate certificates, set this option to `true`.
You might need to make your self-signed certificates available to the agent via a keystore. See
[the Teamscale userguide's section on that topic][ts-userguide] for how to do that.
[the Teamscale userguide's section on that topic][ts-userguide-keystore] for how to do that.
- `azure-url`: a HTTPS URL to an azure file storage. Must be in the following format:
https://\<account\>.file.core.windows.net/\<share\>/(\<path\>)</pre>. The \<path\> is optional; note, that in the case
that the given
Expand Down Expand Up @@ -181,6 +181,12 @@ finished via a REST API. The corresponding server listens at the specified port.

- `http-server-port` (required): the port at which the agent should start an HTTP server that listens for test events
(Recommended port is 8123)
- `class-dir` (required when `coverage-via-http` or `teamscale-testwise-upload` is `true`):
the path under which all class files of the profiled
application are stored. May be a directory or a Jar/War/Ear/... file. Separate multiple paths with a semicolon.
(For details see path format section above)

#### REST API

The agent's REST API has the following endpoints:
- `[GET] /test` Returns the testPath of the current test. The result will be empty when the test already finished or was
Expand All @@ -200,14 +206,39 @@ The agent's REST API has the following endpoints:
}
```

- `[POST] /test/start/{testPath}` Signals to the agent that the test with the given testPath is about to start.
- `[POST] /test/end/{testPath}` Signals to the agent that the test with the given testPath has just finished.
- `[POST] /testrun/start` If you configured a connection to Teamscale via the `teamscale-` options, this will fetch
impacted tests from Teamscale and return them in the response body. The format is the same as returned by Teamscale
itself. You may optionally provide a list of all available test cases in the body of the request. These will also be used to
generate the test-wise coverage report in `[POST] /testrun/end`. The format of the request body is:

```json
[
{
"clusterId": "<ID of the cluster the test belongs to>",
"uniformPath": "<Unique name of the test case>",
"sourcePath": "<Optional: Path to the source of the test>",
"content": "<Optional: Value to detect changes to the test, e.g. hash code, revision, ...>"
}
]
```

Additionally, you may pass the following optional URL query parameters:

- `include-non-impacted`: If this is `true`, will not perform test-selection, only test-prioritization.
- `baseline`: UNIX timestamp in milliseconds to indicate the time since which changes should be considered.
If not given, the time since the last uploaded test-wise coverage report is used (i.e. the last time you
ran the TIA).

- `[POST] /testrun/end` If you configured a connection to Teamscale via the `teamscale-` options and enabled
`teamscale-testwise-upload`, this will upload a test-wise coverage report to Teamscale.
- `[POST] /test/start/{uniformPath}` Signals to the agent that the test with the given uniformPath is about to start.
- `[POST] /test/end/{uniformPath}` Signals to the agent that the test with the given uniformPath has just finished.
The body of the request may optionally contain the test execution result in json format:

```json
{
"result": "ERROR",
"message": "<stacktrace>|<ignore reason>"
"result": "ERROR",
"message": "<stacktrace>|<ignore reason>"
}
```

Expand All @@ -220,13 +251,16 @@ The agent's REST API has the following endpoints:

(`uniformPath` and `duration` is set automatically)

The `testPath` parameter is a hierarchically structured identifier of the test and must be url encoded.
The `uniformPath` parameter is a hierarchically structured identifier of the test and must be url encoded.
E.g. `com/example/MyTest/testSomething` -> `http://localhost:8123/test/start/com%2Fexample%2FMyTest%2FtestSomething`.

- `coverage-via-http` (optional): if set to true the coverage collected during a test is generated in process and
returned as response to the the `[POST] /test/end/...` request. Be aware that this option may slow down the startup
of the system under test and result in a larger memory footprint. If set to false the coverage is stored in a binary
`*.exec` file within the `out` directory. (Default is false)
#### Test-wise coverage modes

You can run the test-wise agent in three different modes:

- `coverage-via-http`: if set to true the coverage collected during a test is generated in-process and
returned as response to the `[POST] /test/end/...` request. Be aware that this option may slow down the startup
of the system under test and result in a larger memory footprint.

The response format looks like this:
```json
Expand All @@ -252,10 +286,15 @@ E.g. `com/example/MyTest/testSomething` -> `http://localhost:8123/test/start/com
}
```
(`duration` and `result` are included when a test execution result is given in the request body)

- `class-dir` (required when `coverage-via-http` is `true`): the path under which all class files of the profiled
application are stored. May be a directory or a Jar/War/Ear/... file. Separate multiple paths with a semicolon.
(For details see path format section above)

- `teamscale-testwise-upload`: if `true`, the agent will buffer all test-wise coverage and test execution data in-memory
and upload the test-wise report to Teamscale when the `POST /testrun/end` REST endpoint is called.
Be aware that this option may slow down the startup
of the system under test and result in a larger memory footprint.

- Neither of the above: The coverage is stored in a binary `*.exec` file within the `out` directory.
This is most useful when running tests in a CI/CD pipeline where the build tooling can later batch-convert all
`*.exec` files and upload a test-wise coverage report to Teamscale.

## Additional steps for WebSphere

Expand Down Expand Up @@ -616,6 +655,6 @@ Enable debug logging in the logging config. Warning: this may create a lot of lo
[glassfish-domainxml]: https://docs.oracle.com/cd/E19798-01/821-1753/abhar/index.html
[glassfish-escaping]: https://stackoverflow.com/questions/24699202/how-to-add-a-jvm-option-to-glassfish-4-0
[git-properties-spring]: https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-git-info
[ts-userguide]: https://www.cqse.eu/download/teamscale/userguide.pdf
[ts-userguide-keystore]: https://docs.teamscale.com/howto/configuring-https/#creating-a-keystore
[teamscale]: https://teamscale.com
[signal-trapping]: http://veithen.io/2014/11/16/sigterm-propagation.html
6 changes: 5 additions & 1 deletion agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,18 @@ dependencies {

implementation 'com.squareup.moshi:moshi:1.8.0'

testImplementation 'junit:junit:4.12'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation 'org.assertj:assertj-core:3.8.0'
testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.mockito:mockito-junit-jupiter:2.19.0'
testImplementation project(':tia-client')
testImplementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
}

test {
testLogging.exceptionFormat "full"
useJUnitPlatform()
}

mainClassName = 'com.teamscale.jacoco.agent.Main'
Expand Down
18 changes: 8 additions & 10 deletions agent/src/main/java/com/teamscale/jacoco/agent/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.conqat.lib.commons.filesystem.FileSystemUtils;
import spark.Request;
import spark.Response;
import spark.Service;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
Expand All @@ -24,8 +25,6 @@
import java.util.Optional;

import static com.teamscale.jacoco.agent.util.LoggingUtils.wrap;
import static spark.Spark.get;
import static spark.Spark.post;

/**
* A wrapper around the JaCoCo Java agent that automatically triggers a dump and XML conversion based on a time
Expand Down Expand Up @@ -68,13 +67,12 @@ public Agent(AgentOptions options,
}

@Override
protected void initServerEndpoints() {
get("/partition", (request, response) ->
protected void initServerEndpoints(Service spark) {
spark.get("/partition", (request, response) ->
Optional.ofNullable(options.getTeamscaleServerOptions().partition).orElse(""));

post("/dump", this::handleDump);
post("/reset", this::handleReset);
post("/partition/" + PARTITION_PARAMETER, this::handleSetPartition);
spark.post("/dump", this::handleDump);
spark.post("/reset", this::handleReset);
spark.post("/partition/" + PARTITION_PARAMETER, this::handleSetPartition);
}

/** Handles dumping a XML coverage report for coverage collected until now. */
Expand Down Expand Up @@ -123,7 +121,7 @@ protected void prepareShutdown() {
try {
com.teamscale.jacoco.agent.util.FileSystemUtils.deleteDirectoryIfEmpty(options.getOutputDirectory());
} catch (IOException e) {
logger.info("Could not delete emtpy output directory {}. " +
logger.info("Could not delete empty output directory {}. " +
"This directory was created inside the configured output directory to be able to " +
"distinguish between different runs of the profiled JVM. You may delete it manually.",
options.getOutputDirectory(), e);
Expand Down Expand Up @@ -158,7 +156,7 @@ private void dumpReportUnsafe() {
long currentTime = System.currentTimeMillis();
Path outputPath = options.getOutputDirectory().resolve("jacoco-" + currentTime + ".xml");

try (Benchmark benchmark = new Benchmark("Generating the XML report")) {
try (Benchmark ignored = new Benchmark("Generating the XML report")) {
FileSystemUtils.ensureParentDirectoryExists(outputPath.toFile());
coverageFile = generator.convert(dump, outputPath);
} catch (IOException e) {
Expand Down
16 changes: 9 additions & 7 deletions agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
import com.teamscale.jacoco.agent.util.LoggingUtils;
import org.jacoco.agent.rt.RT;
import org.slf4j.Logger;
import spark.Service;

import java.lang.instrument.Instrumentation;

import static spark.Spark.port;
import static spark.Spark.stop;

/**
* Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the {@link
* JacocoRuntimeController}.
Expand All @@ -32,6 +30,8 @@ public abstract class AgentBase {

private static LoggingUtils.LoggingResources loggingResources;

private final Service spark = Service.ignite();

/** Constructor. */
public AgentBase(AgentOptions options) throws IllegalStateException {
this.options = options;
Expand All @@ -53,13 +53,15 @@ public AgentBase(AgentOptions options) throws IllegalStateException {
*/
private void initServer() {
logger.info("Listening for test events on port {}.", options.getHttpServerPort());
port(options.getHttpServerPort());
spark.port(options.getHttpServerPort());

initServerEndpoints();
initServerEndpoints(spark);
// this is needed during our tests which will try to access the API directly after creating an agent
spark.awaitInitialization();
}

/** Adds the endpoints that are available in the implemented mode. */
protected abstract void initServerEndpoints();
protected abstract void initServerEndpoints(Service spark);

/** Called by the actual premain method once the agent is isolated from the rest of the application. */
public static void premain(String options, Instrumentation instrumentation) throws Exception {
Expand Down Expand Up @@ -97,7 +99,7 @@ public static void premain(String options, Instrumentation instrumentation) thro
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (options.getHttpServerPort() != null) {
stop();
spark.stop();
}
prepareShutdown();
logger.info("CQSE JaCoCo agent successfully shut down.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ private void searchJarFile(File jarFile) {
*/
public static CommitDescriptor getCommitFromGitProperties(
File jarFile) throws IOException, InvalidGitPropertiesException {
try (JarInputStream jarStream = new JarInputStream(new BashFileSkippingInputStream(new FileInputStream(jarFile)))) {
try (JarInputStream jarStream = new JarInputStream(
new BashFileSkippingInputStream(new FileInputStream(jarFile)))) {
return getCommitFromGitProperties(jarStream, jarFile);
} catch (IOException e) {
throw new IOException("Reading jar " + jarFile.getAbsolutePath() + " for obtaining commit " +
Expand Down
Loading

0 comments on commit f0cd2dd

Please sign in to comment.