Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added history file downloader #7

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
build
target
testerra-report
/src/test/resources/local.properties
/test-report/
77 changes: 65 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ Maven:

## Documentation

### TeamCity configuration
### Improvement of build status in TeamCity

#### TeamCity configuration

Please ensure that you have `Failure Conditions > Common Failure Conditions > at least one test failed` deactivated on your TeamCity
build configuration,
because TeamCity Connector will announce the build status on report generation based on test execution statistics.

### Gradle / Maven configuration
#### Gradle / Maven configuration

When using TeamCity Connector you have to ensure that Gradle/Maven will ignore test failures.

Expand Down Expand Up @@ -95,7 +97,7 @@ Maven:
</build>
````

### Impacts on TeamCity
#### Impacts on TeamCity

*Changes in TeamCity*

Expand Down Expand Up @@ -124,6 +126,57 @@ The following tables shows some more examples how the result could be.
| ![](doc/teamcity_connector_result_failure_corr.png) | The Failure corridor was matched, the status is OK although a test failed.|
| ![](doc/teamcity_connector_result_exp_failed.png) | A test was marked as expected failed, all other tests passed. The restult of the test run is still passed.|

### Support of Testrun history of Testerra Report

TeamCity connector can load the testrun history information of the latest build to get the report history in TeamCity-based Testerra executions. See more details in https://docs.testerra.io/.

To get the latest testrun history, the selected TeamCity build job:
- is not running
- was not canceled
- should be of the same build configuration ;-)
- must match the [branch specification](#selecting-the-branch)

#### Properties

Specify the following properties in `test.properties` to control the history file download for Testerra Report:

| Property | Description |
|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `tt.teamcity.history.download.active` | Activate the history file download, default: `false` |
| `tt.teamcity.url` | URL of your TeamCity server |
| `tt.teamcity.rest.token` | TeamCity Access token needed for REST API |
| `tt.teamcity.buildTypeId` | BuildType ID for the current Build configuration |
| `tt.teamcity.build.branch` | Specify the branch of the build job from which the history file has to be downloaded, default: `all`. See [below](#selecting-the-branch) for more details. |
| `tt.teamcity.build.count` | Number of last build jobs for getting the latest history file., default: `3` |

#### TeamCity configuration

It is recommended that the REST token is stored in a Configuration Parameter in your Build Configuration.

![teamcity_connector_resttoken_parameter.png](doc/teamcity_connector_resttoken_parameter.png)

All the other properties can be setup as 'Additional Maven command line parameters':

![teamcity_connector_history_parameter.png](doc/teamcity_connector_history_parameter.png)

#### Selecting the branch

You can specify the Git branch of the build job which is used to download the latest history file.

Add the property `tt.teamcity.build.branch` to your setup:

| Value | Description |
|---------------|--------------------------------------------------------------------------------------------------------------------|
| `all` | TeamCity connector takes the last build job independent of the used branch. This is default. |
| `default` | Only build jobs of the default branch are used for download. The default branch is configured in your VCS setting. |
| `<any other>` | Any other value is used as a branch name, eg. `development`, `fix/a-bug` |

#### Set the number of latest build jobs

With `tt.teamcity.build.count` you can set the number of latest builds, where TeamCity connector will search of the latest history file.

This could help to prevent interrupted history reports in case of broken jobs or incomplete report artifacts.

---

## Publication
Expand All @@ -132,15 +185,15 @@ This module is deployed and published to Maven Central. All JAR files are signed

The following properties have to be set via command line or ``~/.gradle/gradle.properties``

| Property | Description |
| ----------------------------- | --------------------------------------------------- |
| `moduleVersion` | Version of deployed module, default is `1-SNAPSHOT` |
| `deployUrl` | Maven repository URL |
| `deployUsername` | Maven repository username |
| `deployPassword` | Maven repository password |
| `signing.keyId` | GPG private key ID (short form) |
| `signing.password` | GPG private key password |
| `signing.secretKeyRingFile` | Path to GPG private key |
| Property | Description |
|-----------------------------|-----------------------------------------------------|
| `moduleVersion` | Version of deployed module, default is `1-SNAPSHOT` |
| `deployUrl` | Maven repository URL |
| `deployUsername` | Maven repository username |
| `deployPassword` | Maven repository password |
| `signing.keyId` | GPG private key ID (short form) |
| `signing.password` | GPG private key password |
| `signing.secretKeyRingFile` | Path to GPG private key |

If all properties are set, call the following to build, deploy and release this module:
````shell
Expand Down
6 changes: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ ext {
// Minimum required Testerra version
testerraCompileVersion = '2.0'
// Unit tests use the latest Testerra version
testerraTestVersion = '[2,3-SNAPSHOT)'
// testerraTestVersion = '[2,3-SNAPSHOT)'
testerraTestVersion = '2-hist-SNAPSHOT'
moduleVersion = '2-SNAPSHOT'
if (System.properties.containsKey('moduleVersion')) {
moduleVersion = System.getProperty('moduleVersion')
Expand All @@ -30,7 +31,8 @@ java {
apply from: rootProject.file('publish.gradle')

dependencies {
compileOnly 'io.testerra:driver-ui-desktop:' + testerraCompileVersion
compileOnly 'io.testerra:driver-ui:' + testerraCompileVersion
implementation 'com.jayway.jsonpath:json-path:2.9.0'

testImplementation 'io.testerra:driver-ui-desktop:' + testerraTestVersion
testImplementation 'io.testerra:report-ng:' + testerraTestVersion
Expand Down
Binary file added doc/teamcity_connector_history_parameter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/teamcity_connector_resttoken_parameter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Testerra
*
* (C) 2024, Martin Großmann, Deutsche Telekom MMS GmbH, Deutsche Telekom AG
*
* Deutsche Telekom AG and all other contributors /
* copyright owners license this file to you 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.
*
*/
package eu.tsystems.mms.tic.testerra.plugins.teamcity.history;

import eu.tsystems.mms.tic.testerra.plugins.teamcity.restapi.TeamCityRestClient;
import eu.tsystems.mms.tic.testframework.common.IProperties;
import eu.tsystems.mms.tic.testframework.common.Testerra;
import eu.tsystems.mms.tic.testframework.logging.Loggable;
import eu.tsystems.mms.tic.testframework.report.Report;
import eu.tsystems.mms.tic.testframework.utils.FileDownloader;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;

public class TeamCityHistoryDownloader implements Loggable {

private static final String REPORT_MODEL_DIRECTORY = "report-ng/model/";
private static final String HISTORY_FILENAME = "history";

public enum Properties implements IProperties {
TEAMCITY_HISTORY_DOWNLOAD("tt.teamcity.history.download.active", false),
TEAMCITY_URL("tt.teamcity.url", ""),
TEAMCITY_REST_TOKEN("tt.teamcity.rest.token", ""),
TEAMCITY_BUILD_TYPE_ID("tt.teamcity.buildTypeId", ""),

// Define the type or name of the branch from which the last history file should download
// all = all branches
// default = only default branch
// <any other> = value is used as a branch name
TEAMCITY_BUILD_BRANCH("tt.teamcity.build.branch", "all"),

// If the artifacts of a build job do not contain the history file, the history downloader will check the next 'n' build jobs
TEAMCITY_LAST_BUILD_COUNT("tt.teamcity.build.count", "3"),
;

private final String property;
private final Object defaultValue;

Properties(String property, Object defaultValue) {
this.property = property;
this.defaultValue = defaultValue;
}

@Override
public String toString() {
return property;
}

@Override
public Object getDefault() {
return defaultValue;
}
}

/**
* This workflow get the history file of the latest finished build job
* and move it to the final report directory
*/
public void downloadHistoryFileToReport() {
if (!Properties.TEAMCITY_HISTORY_DOWNLOAD.asBool()) {
return;
}

log().info("Trying to download the Report History file of the last TeamCity build...");

try {
final String historyFilePath = this.getHistoryFilePath();
if (historyFilePath == null) {
return;
}
Path historyFile = this.downloadHistoryFile(historyFilePath);
Report report = Testerra.getInjector().getInstance(Report.class);
Path finalModelDirectory = report.getFinalReportDirectory().resolve(REPORT_MODEL_DIRECTORY);
Path finalHistoryFile = finalModelDirectory.resolve(HISTORY_FILENAME);
if (Files.exists(finalHistoryFile) && !Files.isDirectory(finalHistoryFile)) {
Files.delete(finalHistoryFile);
}
Files.createDirectories(finalModelDirectory);
Files.move(historyFile, finalHistoryFile, StandardCopyOption.REPLACE_EXISTING);
log().info("History file moved to {}", finalHistoryFile.toAbsolutePath());
} catch (Exception e) {
log().warn("Cannot download history file to report directory: {}: {}", e.getClass(), e.getMessage());
}
}

private String getHistoryFilePath() {
final String teamCityUrl = Properties.TEAMCITY_URL.asString();
final String restToken = Properties.TEAMCITY_REST_TOKEN.asString();
final String buildTypeId = Properties.TEAMCITY_BUILD_TYPE_ID.asString();
final String branchType = Properties.TEAMCITY_BUILD_BRANCH.asString();
final String count = Properties.TEAMCITY_LAST_BUILD_COUNT.asString();

TeamCityRestClient client = new TeamCityRestClient(teamCityUrl, restToken);
TeamCityReadHistoryHelper helper = new TeamCityReadHistoryHelper(client);
List<Integer> latestBuildIds = helper.findLatestBuildIds(buildTypeId, branchType, count);
return helper.getHistoryFilePath(latestBuildIds);
}

private Path downloadHistoryFile(final String path) throws IOException {
final String restToken = Properties.TEAMCITY_REST_TOKEN.asString();
final String teamCityUrl = Properties.TEAMCITY_URL.asString();
FileDownloader downloader = new FileDownloader();
downloader.setConnectionConfigurator(connection -> {
connection.setRequestProperty("Authorization", "Bearer " + restToken);
});
Path tempFile = Files.createTempFile(null, "_" + HISTORY_FILENAME);
return downloader.download(teamCityUrl + path, tempFile.getFileName().toString()).toPath();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Testerra
*
* (C) 2025, Martin Großmann, Deutsche Telekom MMS GmbH, Deutsche Telekom AG
*
* Deutsche Telekom AG and all other contributors /
* copyright owners license this file to you 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.
*
*/
package eu.tsystems.mms.tic.testerra.plugins.teamcity.history;

import eu.tsystems.mms.tic.testerra.plugins.teamcity.restapi.TeamCityRestClient;
import eu.tsystems.mms.tic.testerra.plugins.teamcity.restapi.TeamCityRestResponseParser;
import eu.tsystems.mms.tic.testframework.logging.Loggable;
import org.apache.commons.lang3.StringUtils;

import java.net.http.HttpResponse;
import java.util.List;

public class TeamCityReadHistoryHelper implements Loggable {

private TeamCityRestClient client;

public TeamCityReadHistoryHelper(TeamCityRestClient client) {
this.client = client;
}

public List<Integer> findLatestBuildIds(final String buildTypeId, final String branchType, final String count) {
HttpResponse<String> response = client.readBuildJobs(buildTypeId, branchType, count);
if(response == null) {
return List.of();
}

String foundJobs = TeamCityRestResponseParser.getCountOfBuilds(response.body());
if (StringUtils.isBlank(foundJobs) || "0".equals(foundJobs)) {
log().warn("Cannot find a build id of buildtype {} and branch locator ({})", buildTypeId, branchType);
return List.of();
}

return TeamCityRestResponseParser.getBuildIdList(response.body());
}

public String getHistoryFilePath(final List<Integer> buildIds) {
for (Integer buildId : buildIds) {
HttpResponse<String> response = client.readArtifacts(buildId);

if (response == null) {
break;
}
final String path = TeamCityRestResponseParser.readPathOfHistory(response.body());
if (StringUtils.isNotBlank(path) && !"[]".equals(path)) {
return path;
}
}
log().warn("Cannot find history file of build ids {}", buildIds);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.common.eventbus.EventBus;
import com.google.inject.AbstractModule;
import eu.tsystems.mms.tic.testerra.plugins.teamcity.history.TeamCityHistoryDownloader;
import eu.tsystems.mms.tic.testerra.plugins.teamcity.listener.TeamCityEventListener;
import eu.tsystems.mms.tic.testerra.plugins.teamcity.worker.TeamCityStatusReportWorker;
import eu.tsystems.mms.tic.testframework.common.Testerra;
Expand All @@ -35,10 +36,11 @@ public class TeamCityHook extends AbstractModule implements ModuleHook {

@Override
public void init() {

EventBus eventBus = Testerra.getEventBus();
eventBus.register(new TeamCityEventListener());
eventBus.register(new TeamCityStatusReportWorker());

new TeamCityHistoryDownloader().downloadHistoryFileToReport();
}

@Override
Expand Down
Loading