ISSUE #15381 - Fix execution summary timeout (#15378)

* feat: added dynamic method to build search aggregation from string

* fix: moved summary computation to ES

* fix: added+updated tests

* style: ran java linting
This commit is contained in:
Teddy 2024-03-01 16:57:34 +01:00 committed by GitHub
parent 8960c159eb
commit 7424b2f430
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 529 additions and 68 deletions

View File

@ -335,6 +335,12 @@
<version>2.40</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<!-- JSON-P: Java API for JSON Processing (JSR 374) -->
<dependency>
<groupId>javax.json</groupId>

View File

@ -409,13 +409,15 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
updateResultSummaries(testCase, isDeleted, resultSummaries, resultSummary);
// Update test case result summary attribute for the test suite
TestSuiteRepository testSuiteRepository =
(TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE);
TestSuite original =
TestSuiteRepository.copyTestSuite(
testSuite); // we'll need the original state to update the test suite
testSuite.setTestCaseResultSummary(resultSummaries);
daoCollection
.testSuiteDAO()
.update(
testSuite.getId(),
testSuite.getFullyQualifiedName(),
JsonUtils.pojoToJson(testSuite));
EntityRepository<TestSuite>.EntityUpdater testSuiteUpdater =
testSuiteRepository.getUpdater(original, testSuite, Operation.PUT);
testSuiteUpdater.update();
}
}
@ -652,11 +654,16 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
testSuite.setSummary(null); // we don't want to store the summary in the database
List<ResultSummary> resultSummaries = testSuite.getTestCaseResultSummary();
resultSummaries.removeIf(summary -> summary.getTestCaseName().equals(testCaseFqn));
TestSuiteRepository testSuiteRepository =
(TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE);
TestSuite original =
TestSuiteRepository.copyTestSuite(
testSuite); // we'll need the original state to update the test suite
testSuite.setTestCaseResultSummary(resultSummaries);
daoCollection
.testSuiteDAO()
.update(
testSuite.getId(), testSuite.getFullyQualifiedName(), JsonUtils.pojoToJson(testSuite));
EntityRepository<TestSuite>.EntityUpdater testSuiteUpdater =
testSuiteRepository.getUpdater(original, testSuite, Operation.PUT);
testSuiteUpdater.update();
}
@Override

View File

@ -9,10 +9,14 @@ import static org.openmetadata.service.Entity.TEST_CASE;
import static org.openmetadata.service.Entity.TEST_SUITE;
import static org.openmetadata.service.util.FullyQualifiedName.quoteName;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.ws.rs.core.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction;
@ -23,7 +27,6 @@ import org.openmetadata.schema.tests.type.TestCaseStatus;
import org.openmetadata.schema.tests.type.TestSummary;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.EventType;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.dqtests.TestSuiteResource;
@ -55,7 +58,7 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
fields.contains("pipelines") ? getIngestionPipelines(entity) : entity.getPipelines());
entity.setSummary(
fields.contains("summary") ? getTestCasesExecutionSummary(entity) : entity.getSummary());
entity.withTests(fields.contains("tests") ? getTestCases(entity) : entity.getTests());
entity.withTests(fields.contains(UPDATE_FIELDS) ? getTestCases(entity) : entity.getTests());
}
@Override
@ -71,7 +74,7 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
public void clearFields(TestSuite entity, EntityUtil.Fields fields) {
entity.setPipelines(fields.contains("pipelines") ? entity.getPipelines() : null);
entity.setSummary(fields.contains("summary") ? entity.getSummary() : null);
entity.withTests(fields.contains("tests") ? entity.getTests() : null);
entity.withTests(fields.contains(UPDATE_FIELDS) ? entity.getTests() : null);
}
private TestSummary buildTestSummary(Map<String, Integer> testCaseSummary) {
@ -117,31 +120,81 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
return buildTestSummary(testCaseSummary);
}
private TestSummary getTestCasesExecutionSummary(List<TestSuite> entities) {
if (entities.isEmpty()) return new TestSummary();
Map<String, Integer> testsSummary = new HashMap<>();
for (TestSuite testSuite : entities) {
Map<String, Integer> testSummary = getResultSummary(testSuite);
for (Map.Entry<String, Integer> entry : testSummary.entrySet()) {
testsSummary.put(
entry.getKey(), testsSummary.getOrDefault(entry.getKey(), 0) + entry.getValue());
private TestSummary getTestCasesExecutionSummary(JsonObject aggregation) {
// Initialize the test summary with 0 values
TestSummary testSummary =
new TestSummary().withAborted(0).withFailed(0).withSuccess(0).withQueued(0).withTotal(0);
JsonObject summary = aggregation.getJsonObject("nested#testCaseResultSummary");
testSummary.setTotal(summary.getJsonNumber("doc_count").intValue());
JsonObject statusCount = summary.getJsonObject("sterms#status_counts");
JsonArray buckets = statusCount.getJsonArray("buckets");
for (JsonValue bucket : buckets) {
String key = ((JsonObject) bucket).getString("key");
Integer count = ((JsonObject) bucket).getJsonNumber("doc_count").intValue();
switch (key) {
case "Success":
testSummary.setSuccess(count);
break;
case "Failed":
testSummary.setFailed(count);
break;
case "Aborted":
testSummary.setAborted(count);
break;
case "Queued":
testSummary.setQueued(count);
break;
}
testSuite.getTestCaseResultSummary().size();
}
return buildTestSummary(testsSummary);
return testSummary;
}
public TestSummary getTestSummary(UUID testSuiteId) {
public TestSummary getTestSummary(UUID testSuiteId) throws IOException {
String aggregationQuery =
"""
{
"aggregations": {
"test_case_results": {
"nested": {
"path": "testCaseResultSummary"
},
"aggs": {
"status_counts": {
"terms": {
"field": "testCaseResultSummary.status"
}
}
}
}
}
}
""";
JsonObject aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
TestSummary testSummary;
if (testSuiteId == null) {
ListFilter filter = new ListFilter();
filter.addQueryParam("testSuiteType", "executable");
List<TestSuite> testSuites = listAll(EntityUtil.Fields.EMPTY_FIELDS, filter);
testSummary = getTestCasesExecutionSummary(testSuites);
JsonObject testCaseResultSummary =
searchRepository.aggregate(null, TEST_SUITE, aggregationJson);
testSummary = getTestCasesExecutionSummary(testCaseResultSummary);
} else {
String query =
"""
{
"query": {
"bool": {
"must": {
"term": {"id": "%s"}
}
}
}
}
"""
.formatted(testSuiteId);
// don't want to get it from the cache as test results summary may be stale
TestSuite testSuite = Entity.getEntity(TEST_SUITE, testSuiteId, "", Include.ALL, false);
testSummary = getTestCasesExecutionSummary(testSuite);
JsonObject testCaseResultSummary =
searchRepository.aggregate(query, TEST_SUITE, aggregationJson);
testSummary = getTestCasesExecutionSummary(testCaseResultSummary);
}
return testSummary;
}
@ -211,6 +264,26 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
return new RestUtil.DeleteResponse<>(updated, changeType);
}
public static TestSuite copyTestSuite(TestSuite testSuite) {
return new TestSuite()
.withConnection(testSuite.getConnection())
.withDescription(testSuite.getDescription())
.withChangeDescription(testSuite.getChangeDescription())
.withDeleted(testSuite.getDeleted())
.withDisplayName(testSuite.getDisplayName())
.withFullyQualifiedName(testSuite.getFullyQualifiedName())
.withHref(testSuite.getHref())
.withId(testSuite.getId())
.withName(testSuite.getName())
.withExecutable(testSuite.getExecutable())
.withExecutableEntityReference(testSuite.getExecutableEntityReference())
.withServiceType(testSuite.getServiceType())
.withOwner(testSuite.getOwner())
.withUpdatedBy(testSuite.getUpdatedBy())
.withUpdatedAt(testSuite.getUpdatedAt())
.withVersion(testSuite.getVersion());
}
public class TestSuiteUpdater extends EntityUpdater {
public TestSuiteUpdater(TestSuite original, TestSuite updated, Operation operation) {
super(original, updated, operation);
@ -221,7 +294,13 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
public void entitySpecificUpdate() {
List<EntityReference> origTests = listOrEmpty(original.getTests());
List<EntityReference> updatedTests = listOrEmpty(updated.getTests());
recordChange("tests", origTests, updatedTests);
List<ResultSummary> origTestCaseResultSummary =
listOrEmpty(original.getTestCaseResultSummary());
List<ResultSummary> updatedTestCaseResultSummary =
listOrEmpty(updated.getTestCaseResultSummary());
recordChange(UPDATE_FIELDS, origTests, updatedTests);
recordChange(
"testCaseResultSummary", origTestCaseResultSummary, updatedTestCaseResultSummary);
}
}
}

View File

@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.json.JsonPatch;
@ -321,7 +322,8 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
description = "get summary for a specific test suite",
schema = @Schema(type = "String", format = "uuid"))
@QueryParam("testSuiteId")
UUID testSuiteId) {
UUID testSuiteId)
throws IOException {
ResourceContext<?> resourceContext = getResourceContext();
OperationContext operationContext =
new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS);

View File

@ -8,6 +8,7 @@ import java.text.ParseException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.tuple.Pair;
@ -87,6 +88,8 @@ public interface SearchClient {
Response aggregate(String index, String fieldName, String value, String query) throws IOException;
JsonObject aggregate(String query, String index, JsonObject aggregationJson) throws IOException;
Response suggest(SearchRequest request) throws IOException;
void createEntity(String indexName, String docId, String doc);

View File

@ -669,6 +669,11 @@ public class SearchRepository {
return searchClient.aggregate(index, fieldName, value, query);
}
public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
throws IOException {
return searchClient.aggregate(query, index, aggregationJson);
}
public Response suggest(SearchRequest request) throws IOException {
return searchClient.suggest(request);
}

View File

@ -105,6 +105,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@ -704,6 +705,82 @@ public class ElasticSearchClient implements SearchClient {
return Response.status(OK).entity(response).build();
}
/*
Build dynamic aggregation from elasticsearch JSON like aggregation query.
See TestSuiteResourceTest for example usage (ln. 506) for tested aggregation query.
@param aggregations - JsonObject containing the aggregation query
*/
public static List<AggregationBuilder> buildAggregation(JsonObject aggregations) {
List<AggregationBuilder> aggregationBuilders = new ArrayList<>();
for (String key : aggregations.keySet()) {
JsonObject aggregation = aggregations.getJsonObject(key);
for (String aggregationType : aggregation.keySet()) {
switch (aggregationType) {
case "terms":
JsonObject termAggregation = aggregation.getJsonObject(aggregationType);
TermsAggregationBuilder termsAggregationBuilder =
AggregationBuilders.terms(key).field(termAggregation.getString("field"));
aggregationBuilders.add(termsAggregationBuilder);
break;
case "nested":
JsonObject nestedAggregation = aggregation.getJsonObject("nested");
AggregationBuilder nestedAggregationBuilder =
AggregationBuilders.nested(
nestedAggregation.getString("path"), nestedAggregation.getString("path"));
JsonObject nestedAggregations = aggregation.getJsonObject("aggs");
List<AggregationBuilder> nestedAggregationBuilders =
buildAggregation(nestedAggregations);
for (AggregationBuilder nestedAggregationBuilder1 : nestedAggregationBuilders) {
nestedAggregationBuilder.subAggregation(nestedAggregationBuilder1);
}
aggregationBuilders.add(nestedAggregationBuilder);
break;
default:
break;
}
}
}
return aggregationBuilders;
}
@Override
public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
throws IOException {
JsonObject aggregations = aggregationJson.getJsonObject("aggregations");
if (aggregations == null) {
return null;
}
List<AggregationBuilder> aggregationBuilder = buildAggregation(aggregations);
es.org.elasticsearch.action.search.SearchRequest searchRequest =
new es.org.elasticsearch.action.search.SearchRequest(
Entity.getSearchRepository().getIndexOrAliasName(index));
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
if (query != null) {
XContentParser queryParser =
XContentType.JSON
.xContent()
.createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, query);
QueryBuilder parsedQuery = SearchSourceBuilder.fromXContent(queryParser).query();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(parsedQuery);
searchSourceBuilder.query(boolQueryBuilder);
}
searchSourceBuilder.size(0).timeout(new TimeValue(30, TimeUnit.SECONDS));
for (AggregationBuilder aggregation : aggregationBuilder) {
searchSourceBuilder.aggregation(aggregation);
}
searchRequest.source(searchSourceBuilder);
String response = client.search(searchRequest, RequestOptions.DEFAULT).toString();
JsonObject jsonResponse = JsonUtils.readJson(response).asJsonObject();
return jsonResponse.getJsonObject("aggregations");
}
private static ScriptScoreFunctionBuilder boostScore() {
return ScoreFunctionBuilders.scriptFunction(
"double score = _score;"

View File

@ -37,6 +37,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@ -724,6 +725,82 @@ public class OpenSearchClient implements SearchClient {
return Response.status(OK).entity(response).build();
}
/*
Build dynamic aggregation from elasticsearch JSON like aggregation query.
See TestSuiteResourceTest for example usage (ln. 506) for tested aggregation query.
@param aggregations - JsonObject containing the aggregation query
*/
public static List<AggregationBuilder> buildAggregation(JsonObject aggregations) {
List<AggregationBuilder> aggregationBuilders = new ArrayList<>();
for (String key : aggregations.keySet()) {
JsonObject aggregation = aggregations.getJsonObject(key);
for (String aggregationType : aggregation.keySet()) {
switch (aggregationType) {
case "terms":
JsonObject termAggregation = aggregation.getJsonObject(aggregationType);
TermsAggregationBuilder termsAggregationBuilder =
AggregationBuilders.terms(key).field(termAggregation.getString("field"));
aggregationBuilders.add(termsAggregationBuilder);
break;
case "nested":
JsonObject nestedAggregation = aggregation.getJsonObject("nested");
AggregationBuilder nestedAggregationBuilder =
AggregationBuilders.nested(
nestedAggregation.getString("path"), nestedAggregation.getString("path"));
JsonObject nestedAggregations = aggregation.getJsonObject("aggs");
List<AggregationBuilder> nestedAggregationBuilders =
buildAggregation(nestedAggregations);
for (AggregationBuilder nestedAggregationBuilder1 : nestedAggregationBuilders) {
nestedAggregationBuilder.subAggregation(nestedAggregationBuilder1);
}
aggregationBuilders.add(nestedAggregationBuilder);
break;
default:
break;
}
}
}
return aggregationBuilders;
}
@Override
public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
throws IOException {
JsonObject aggregations = aggregationJson.getJsonObject("aggregations");
if (aggregations == null) {
return null;
}
List<AggregationBuilder> aggregationBuilder = buildAggregation(aggregations);
os.org.opensearch.action.search.SearchRequest searchRequest =
new os.org.opensearch.action.search.SearchRequest(
Entity.getSearchRepository().getIndexOrAliasName(index));
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
if (query != null) {
XContentParser queryParser =
XContentType.JSON
.xContent()
.createParser(X_CONTENT_REGISTRY, LoggingDeprecationHandler.INSTANCE, query);
QueryBuilder parsedQuery = SearchSourceBuilder.fromXContent(queryParser).query();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(parsedQuery);
searchSourceBuilder.query(boolQueryBuilder);
}
searchSourceBuilder.size(0).timeout(new TimeValue(30, TimeUnit.SECONDS));
for (AggregationBuilder aggregation : aggregationBuilder) {
searchSourceBuilder.aggregation(aggregation);
}
searchRequest.source(searchSourceBuilder);
String response = client.search(searchRequest, RequestOptions.DEFAULT).toString();
JsonObject jsonResponse = JsonUtils.readJson(response).asJsonObject();
return jsonResponse.getJsonObject("aggregations");
}
public void updateSearch(UpdateRequest updateRequest) {
if (updateRequest != null) {
updateRequest.docAsUpsert(true);

View File

@ -84,6 +84,30 @@
"entityType": {
"type": "keyword"
},
"testCaseResultSummary": {
"type": "nested",
"properties": {
"status": {
"type": "keyword"
},
"testCaseName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
},
"ngram": {
"type": "text",
"analyzer": "om_ngram"
}
}
},
"timestamp": {
"type": "long"
}
}
},
"suggest": {
"type": "completion",
"contexts": [

View File

@ -82,6 +82,27 @@
"entityType": {
"type": "keyword"
},
"testCaseResultSummary": {
"type": "nested",
"properties": {
"status": {
"type": "keyword"
},
"testCaseName": {
"type": "text",
"analyzer": "om_analyzer_jp",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "long"
}
}
},
"suggest": {
"type": "completion",
"contexts": [

View File

@ -67,6 +67,28 @@
"entityType": {
"type": "keyword"
},
"testCaseResultSummary": {
"type": "nested",
"properties": {
"status": {
"type": "keyword"
},
"testCaseName": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "long"
}
}
},
"fqnParts": {
"type": "keyword"
},

View File

@ -381,17 +381,14 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
ADMIN_AUTH_HEADERS);
verifyTestCaseResults(testCaseResults, testCase1ResultList, 4);
TestSummary testSummary = getTestSummary(ADMIN_AUTH_HEADERS, null);
assertNotEquals(0, testSummary.getFailed());
assertNotEquals(0, testSummary.getSuccess());
assertNotEquals(0, testSummary.getTotal());
assertEquals(0, testSummary.getAborted());
String randomUUID = UUID.randomUUID().toString();
assertResponseContains(
() -> getTestSummary(ADMIN_AUTH_HEADERS, randomUUID),
NOT_FOUND,
"testSuite instance for " + randomUUID + " not found");
TestSummary testSummary;
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, null);
assertNotEquals(0, testSummary.getFailed());
assertNotEquals(0, testSummary.getSuccess());
assertNotEquals(0, testSummary.getTotal());
assertEquals(0, testSummary.getAborted());
}
// Test that we can get the test summary for a logical test suite and that
// adding a logical test suite does not change the total number of tests
@ -403,25 +400,30 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
testCaseIds.add(testCase1.getId());
testSuiteResourceTest.addTestCasesToLogicalTestSuite(logicalTestSuite, testCaseIds);
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, testSummary.getTotal());
assertEquals(1, testSummary.getFailed());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, testSummary.getTotal());
assertEquals(1, testSummary.getFailed());
}
// add a new test case to the logical test suite to validate if the
// summary is updated correctly
testCaseIds.clear();
testCaseIds.add(testCase.getId());
testSuiteResourceTest.addTestCasesToLogicalTestSuite(logicalTestSuite, testCaseIds);
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, testSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, testSummary.getTotal());
}
// remove test case from logical test suite and validate
// the summary is updated as expected
deleteLogicalTestCase(logicalTestSuite, testCase.getId());
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, testSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
testSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, testSummary.getTotal());
}
}
@Test
@ -458,37 +460,52 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
testCaseIds.add(testCase1.getId());
testSuiteResourceTest.addTestCasesToLogicalTestSuite(logicalTestSuite, testCaseIds);
// test we get the right summary for the executable test suite
TestSummary executableTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, testCase.getTestSuite().getId().toString());
TestSuite testSuite =
testSuiteResourceTest.getEntity(testCase.getTestSuite().getId(), "*", ADMIN_AUTH_HEADERS);
assertEquals(testSuite.getTests().size(), executableTestSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
// test we get the right summary for the executable test suite
TestSummary executableTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, testCase.getTestSuite().getId().toString());
assertEquals(testSuite.getTests().size(), executableTestSummary.getTotal());
}
// test we get the right summary for the logical test suite
TestSummary logicalTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, logicalTestSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
TestSummary logicalTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(1, logicalTestSummary.getTotal());
}
testCaseIds.clear();
testCaseIds.add(testCase.getId());
testSuiteResourceTest.addTestCasesToLogicalTestSuite(logicalTestSuite, testCaseIds);
logicalTestSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, logicalTestSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
TestSummary logicalTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, logicalTestSummary.getTotal());
}
deleteEntity(testCase1.getId(), ADMIN_AUTH_HEADERS);
executableTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, testCase.getTestSuite().getId().toString());
testSuite =
testSuiteResourceTest.getEntity(testCase.getTestSuite().getId(), "*", ADMIN_AUTH_HEADERS);
assertEquals(testSuite.getTests().size(), executableTestSummary.getTotal());
logicalTestSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, logicalTestSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
TestSummary executableTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, testCase.getTestSuite().getId().toString());
assertEquals(testSuite.getTests().size(), executableTestSummary.getTotal());
TestSummary logicalTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
assertEquals(2, logicalTestSummary.getTotal());
}
// check the deletion of the test case from the executable test suite
// cascaded to the logical test suite
deleteLogicalTestCase(logicalTestSuite, testCase.getId());
logicalTestSummary = getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
// check the deletion of the test case from the logical test suite is reflected in the summary
assertEquals(1, logicalTestSummary.getTotal());
if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
TestSummary logicalTestSummary =
getTestSummary(ADMIN_AUTH_HEADERS, logicalTestSuite.getId().toString());
// check the deletion of the test case from the logical test suite is reflected in the summary
assertEquals(1, logicalTestSummary.getTotal());
}
}
@Test

View File

@ -2,6 +2,7 @@ package org.openmetadata.service.resources.dqtests;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
@ -11,6 +12,8 @@ import static org.openmetadata.service.util.TestUtils.assertListNull;
import static org.openmetadata.service.util.TestUtils.assertResponse;
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
import es.org.elasticsearch.search.aggregations.AggregationBuilder;
import es.org.elasticsearch.search.aggregations.AggregationBuilders;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
@ -19,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.json.JsonObject;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import org.apache.http.client.HttpResponseException;
@ -41,6 +45,7 @@ import org.openmetadata.schema.type.Include;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.databases.TableResourceTest;
import org.openmetadata.service.search.elasticsearch.ElasticSearchClient;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.ResultList;
import org.openmetadata.service.util.TestUtils;
@ -58,6 +63,7 @@ public class TestSuiteResourceTest extends EntityResourceTest<TestSuite, CreateT
TestSuiteResource.TestSuiteList.class,
"dataQuality/testSuites",
TestSuiteResource.FIELDS);
supportsSearchIndex = true;
}
public void setupTestSuites(TestInfo test) throws IOException {
@ -496,6 +502,121 @@ public class TestSuiteResourceTest extends EntityResourceTest<TestSuite, CreateT
TestUtils.getEntityNameLengthError(entityClass));
}
@Test
void buildElasticsearchAggregationFromJson(TestInfo test) {
JsonObject aggregationJson;
List<AggregationBuilder> actual;
List<AggregationBuilder> expected = new ArrayList<>();
String aggregationQuery;
// Test aggregation with nested aggregation
aggregationQuery =
"""
{
"aggregations": {
"test_case_results": {
"nested": {
"path": "testCaseResultSummary"
},
"aggs": {
"status_counts": {
"terms": {
"field": "testCaseResultSummary.status"
}
}
}
}
}
}
""";
expected.add(
AggregationBuilders.nested("testCaseResultSummary", "testCaseResultSummary")
.subAggregation(
AggregationBuilders.terms("status_counts").field("testCaseResultSummary.status")));
aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
assertThat(actual).hasSameElementsAs(expected);
// Test aggregation with multiple aggregations
aggregationQuery =
"""
{
"aggregations": {
"my-first-agg-name": {
"terms": {
"field": "my-field"
}
},
"my-second-agg-name": {
"terms": {
"field": "my-other-field"
}
}
}
}
""";
aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
expected.clear();
expected.addAll(
List.of(
AggregationBuilders.terms("my-second-agg-name").field("my-other-field"),
AggregationBuilders.terms("my-first-agg-name").field("my-field")));
actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
assertThat(actual).hasSameElementsAs(expected);
// Test aggregation with multiple aggregations including a nested one which has itself multiple
// aggregations
aggregationQuery =
"""
{
"aggregations": {
"my-first-agg-name": {
"terms": {
"field": "my-field"
}
},
"test_case_results": {
"nested": {
"path": "testCaseResultSummary"
},
"aggs": {
"status_counts": {
"terms": {
"field": "testCaseResultSummary.status"
}
},
"other_status_counts": {
"terms": {
"field": "testCaseResultSummary.status"
}
}
}
}
}
}
""";
aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
expected.clear();
expected.addAll(
List.of(
AggregationBuilders.nested("testCaseResultSummary", "testCaseResultSummary")
.subAggregation(
AggregationBuilders.terms("status_counts")
.field("testCaseResultSummary.status"))
.subAggregation(
AggregationBuilders.terms("other_status_counts")
.field("testCaseResultSummary.status")),
AggregationBuilders.terms("my-first-agg-name").field("my-field")));
actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
assertThat(actual).hasSameElementsAs(expected);
}
@Test
void delete_LogicalTestSuite_200(TestInfo test) throws IOException {
TestCaseResourceTest testCaseResourceTest = new TestCaseResourceTest();