Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import scala.collection.Seq

lazy val root = (project in file("."))
.enablePlugins(GatlingPlugin)
.settings(
Expand Down
15 changes: 4 additions & 11 deletions src/test/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,7 @@ perftest {
#requestPercentageFailureThreshold = 1
}

# if true, saves NDJSON to a txt file
# Each "no-of-json-lines" generates an NDJSON payload containing all 6 ISA account types.
# For example:
# no-of-json-lines = 1 → 6 JSON objects(6 NDJSON lines)
# no-of-json-lines = N → 6 x N JSON objects (N NDJSON lines)
# With the existing payload "no-of-json-lines = 5832" creates a 10MB ndjson file
# "no-of-json-lines = 364" creates a 1MB ndjson file
# set 'save-to-file = true' only when tests are running locally
saveMonthlyReturnLocally {
no-of-json-lines = 5832
}
# numOfIsaAccountSets = 1 → 4 JSON objects(4 NDJSON lines)
monthlyReturnsTestPayload {
numOfIsaAccountSets = 4000
}
41 changes: 19 additions & 22 deletions src/test/resources/journeys.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,34 @@
# Configure here your journeys. A journey is a sequence of requests at a certain load.

journeys {
monthly-returns-submission-journey = {
description = "Monthly returns submission journey"
load = 10
parts = [
monthly-returns-submission-journey
]
}

monthly-returns-declaration-journey = {
description = "monthly returns declaration journey"
load = 50
submit-monthly-returns = {
description = "Submit Monthly Returns Journey"
load = 2
parts = [
monthly-returns-declaration-journey
submit-monthly-returns
]
}

monthly-reconciliation-report-summary-journey = {
description = "Monthly reconciliation report summary journey"
load = 50
},
nps-report-summary-callback = {
description = "NPS Report Summary Callback Journey"
load = 2
parts = [
monthly-reconciliation-report-summary-journey
nps-report-summary-callback
]
}
},
nps-retrieve-monthly-summary-and-report = {
description = "NPS Retrieve Monthly Summary and Report Journey"
load = 5
parts = [
nps-retrieve-monthly-summary-and-report
]
}
}


# Default behaviour is to run all journeys. If that is not what you need you can specify the list of journeys to run
journeysToRun = [
monthly-returns-submission-journey,
monthly-returns-declaration-journey,
monthly-reconciliation-report-summary-journey
submit-monthly-returns,
nps-report-summary-callback
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we running 'nps-retrieve-monthly-summary-and-report' later ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep put todos in simulation and created https://jira.tools.tax.service.gov.uk/browse/DFI-1720 here to fully implement the get report reconciliation & test support integration

]

# You can specify the same list of journeys via environment variables:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,30 @@ package uk.gov.hmrc.perftests.disareturns
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.http.request.builder.HttpRequestBuilder
import uk.gov.hmrc.perftests.disareturns.Util.MockMonthlyReturnData.validNdjsonTestData
import uk.gov.hmrc.perftests.disareturns.Util.NdjsonPayloadGenerator
import uk.gov.hmrc.perftests.disareturns.constant.AppConfig.{disaReturnsHost, disaReturnsRoute}
import uk.gov.hmrc.perftests.disareturns.constant.Headers.headerWithClientIdAndBearerToken

import java.nio.file.{Files, Paths, StandardOpenOption}

object MonthlyReturnsSubmissionRequests {

private val submissionPayloadFilePath: String = {
val path = "target/monthly-return-payload.ndjson"
val ndjson = NdjsonPayloadGenerator.generateNdjsonPayload()
val filePath = Paths.get(path)
Files.write(filePath, ndjson.getBytes("UTF-8"),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
path
}

val submitMonthlyReport: HttpRequestBuilder =
http("Submit monthly report")
.post(s"$disaReturnsHost$disaReturnsRoute#{isaManagerReference}/#{taxYear}/#{month}")
.headers(headerWithClientIdAndBearerToken)
.body(StringBody(validNdjsonTestData()))
.body(RawFileBody(submissionPayloadFilePath)).asJson
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What RawFileBody does:

Open NDJSON file → stream bytes → write to buffer → send

appose to what we was happening previously with StringBody

Load entire file → build giant String → send

This was consuming large amounts of gatlings memory resulting in OOM expections

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we configure this? Robs comment

.check(status.is(204))

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,86 +16,108 @@

package uk.gov.hmrc.perftests.disareturns

import io.gatling.core.Predef.feed
import io.gatling.core.Predef._
import io.gatling.core.structure.ChainBuilder
import uk.gov.hmrc.performance.simulation.PerformanceTestRunner
import uk.gov.hmrc.perftests.disareturns.MonthlyReconciliationReportRequests.{getReportingResultsSummary, submitReturnSummaryCallback}
import uk.gov.hmrc.perftests.disareturns.MonthlyReturnLoginRequest.getBearerToken
import uk.gov.hmrc.perftests.disareturns.MonthlyReturnsDeclarationRequest.submitDeclaration
import uk.gov.hmrc.perftests.disareturns.MonthlyReturnsSubmissionRequests.submitMonthlyReport
import uk.gov.hmrc.perftests.disareturns.Util.RandomDataGenerator.{generateRandomISAReference, getMonth, getTaxYear}
import uk.gov.hmrc.perftests.disareturns.models.TestDataSetupResult
import uk.gov.hmrc.perftests.disareturns.TestOnlyRequests.openObligationStatus
import uk.gov.hmrc.perftests.disareturns.Util.DirectMemoryLogger
import uk.gov.hmrc.perftests.disareturns.Util.RandomDataGenerator.{getMonth, getTaxYear}
import uk.gov.hmrc.perftests.disareturns.models.{Applications, IsaManagers}
import uk.gov.hmrc.perftests.disareturns.testSetup.BaseRequests

import scala.concurrent.Await
import scala.concurrent.duration.DurationInt

class MonthlyReturnsSubmissionSimulation extends PerformanceTestRunner with BaseRequests {

var setupData: TestDataSetupResult = _
var setupIsaManagers: IsaManagers = _
var setupIsaApplications: Applications = _

before {
setupData = Await.result(testDataSetup(), 30.seconds)
setupIsaManagers = Await.result(setupTestData(), 30.seconds)
setupIsaApplications = Await.result(setupApplications(), 30.seconds)

val scheduler = java.util.concurrent.Executors.newSingleThreadScheduledExecutor()

scheduler.scheduleAtFixedRate(
new Runnable {
override def run(): Unit = DirectMemoryLogger.log()
},
0,
5,
java.util.concurrent.TimeUnit.SECONDS
)
}

after {
testDataCleanUp(setupData)
testDataCleanUp(setupIsaApplications)
}

val bearerTokenFeeder: ChainBuilder = feed(Iterator.continually(Map("bearerToken" -> setupData.bearerToken)))

val clientIdFeeder: ChainBuilder = feed(
Iterator.continually(Map("clientId" -> setupData.clientIds(scala.util.Random.nextInt(setupData.clientIds.size))))
)

def generateReportInformationForTheSubmission(): Iterator[Map[String, String]] =
Iterator.continually(
Map(
"isaManagerReference" -> generateRandomISAReference(1, 500),
"taxYear" -> getTaxYear,
"month" -> getMonth
)
val appFeeder: ChainBuilder =
feed(
Iterator
.continually(setupIsaApplications.applications)
.flatten
.map { im =>
Map(
"clientId" -> im.clientId,
"applicationId" -> im.applicationId)
}
)

def generateReportInformationForTheDeclaration(): Iterator[Map[String, String]] =
Iterator.continually(
Map(
"isaManagerReference" -> generateRandomISAReference(501, 999),
"taxYear" -> getTaxYear,
"month" -> getMonth
)
val isaManagerFeeder: ChainBuilder =
feed(
Iterator
.continually(setupIsaManagers.isaManager)
.flatten
.map { im =>
Map(
"isaManagerReference" -> im.zRef,
"bearerToken" -> im.bearerToken,
"taxYear" -> getTaxYear,
"month" -> getMonth
)
}
)

setup(
"monthly-returns-submission-journey",
"Monthly returns submission journey"
) withActions (feed(
generateReportInformationForTheSubmission()
).actionBuilders ++ clientIdFeeder.actionBuilders: _*) withRequests (
getBearerToken,
submitMonthlyReport
"submit-monthly-returns",
"Submit Monthly Returns"
).withActions(
isaManagerFeeder.actionBuilders ++ appFeeder.actionBuilders: _*
).withRequests(
openObligationStatus,
submitMonthlyReport,
submitDeclaration
)

setup(
"monthly-returns-declaration-journey",
"Monthly returns declaration journey"
) withActions (feed(
generateReportInformationForTheDeclaration()
).actionBuilders ++ clientIdFeeder.actionBuilders: _*) withRequests (
getBearerToken,
submitDeclaration
"nps-report-summary-callback",
"NPS Report Summary Callback"
).withActions(
isaManagerFeeder.actionBuilders ++ appFeeder.actionBuilders: _*
).withRequests(
submitReturnSummaryCallback
)

//TODO: Ticket raised - DFI-1720
// - to implement request to test support API to setup reconciliation report
// - to implement request to get reconciliation report
// - remove NPS callback as this is tested separately
setup(
"monthly-reconciliation-report-summary-journey",
"Monthly reconciliation report summary journey"
) withActions (feed(
generateReportInformationForTheSubmission()
).actionBuilders: _*) withRequests (
getBearerToken,
"nps-retrieve-monthly-summary-and-report",
"NPS Retrieve Monthly Summary and Report"
).withActions(
isaManagerFeeder.actionBuilders ++ appFeeder.actionBuilders: _*
).withRequests(
submitReturnSummaryCallback,
getReportingResultsSummary
)


runSimulation()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* 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.
*/

package uk.gov.hmrc.perftests.disareturns

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.http.request.builder.HttpRequestBuilder
import uk.gov.hmrc.perftests.disareturns.constant.AppConfig.{disaReturnsStubHost, openObligationStatusPath}
import uk.gov.hmrc.perftests.disareturns.constant.Headers.headerWithClientIdAndBearerToken

object TestOnlyRequests {
val openObligationStatus: HttpRequestBuilder =
http("Open Obligation Status")
.post(s"$disaReturnsStubHost$openObligationStatusPath#{isaManagerReference}")
.headers(headerWithClientIdAndBearerToken)
.body(StringBody(""))
.check(status.is(200))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026 HM Revenue & Customs
*
* 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.
*/

package uk.gov.hmrc.perftests.disareturns.Util

import java.lang.management.{BufferPoolMXBean, ManagementFactory}
import scala.jdk.CollectionConverters._

object DirectMemoryLogger {

def log(): Unit = {
val bufferPools: Seq[BufferPoolMXBean] =
ManagementFactory.getPlatformMXBeans(classOf[BufferPoolMXBean]).asScala.toSeq

bufferPools.foreach { pool =>
if (pool.getName == "direct") {
val usedMb = pool.getMemoryUsed / 1024 / 1024
val capMb = pool.getTotalCapacity / 1024 / 1024
val count = pool.getCount

println(
s"[DIRECT MEMORY] Used: ${usedMb}MB | Capacity: ${capMb}MB | Buffers: $count"
)
}
}
}
}
Loading