From a51865837c7260f0eabf998f4546ba8b08b5d866 Mon Sep 17 00:00:00 2001 From: Michal Wyrzykowski Date: Fri, 25 Oct 2019 12:38:52 +0200 Subject: [PATCH 1/5] WIP Support HttpClient based scenarios When JPT detects a regression and the regression is on a backend side, then it's handy to test only the backend side, to avoid the browser's noise. I had this problem twice and two persons asked me about the feature. As there was no dedicated API, I've been creating a custom scenario, and translate web driver to HttpClient inside of a custom action. This approach has two disadvantages: - It requires expert-level knowledge about JPT - Each VU needs to run a real browser, even if it's not used. It limits the The goal of this change is to expose API for lightweight HttpClient based scenarios. --- CHANGELOG.md | 3 + .../tools/virtualusers/HttpClientWebDriver.kt | 334 ++++++++++++++++++ .../api/config/VirtualUserBehavior.kt | 50 +++ .../api/scenarios/HttpClientScenario.kt | 12 + .../scenarios/HttpClientScenarioAdapter.kt | 37 ++ .../api/scenarios/HttpClientScenarioIT.kt | 102 ++++++ 6 files changed, 538 insertions(+) create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt create mode 100644 src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd9241..61b4bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ Dropping a requirement of a major version of a dependency is a new contract. ## [Unreleased] [Unreleased]: https://github.com/atlassian/virtual-users/compare/release-3.10.0...master +### Added +- Support HttpClient based scenarios. + ## [3.10.0] - 2019-08-02 [3.10.0]: https://github.com/atlassian/virtual-users/compare/release-3.9.1...release-3.10.0 diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt new file mode 100644 index 0000000..a60bcdf --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt @@ -0,0 +1,334 @@ +package com.atlassian.performance.tools.virtualusers + +import com.google.common.util.concurrent.SettableFuture +import net.jcip.annotations.GuardedBy +import org.apache.http.auth.AuthScope +import org.apache.http.auth.Credentials +import org.apache.http.auth.UsernamePasswordCredentials +import org.apache.http.client.CredentialsProvider +import org.apache.http.impl.client.BasicCredentialsProvider +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClientBuilder +import org.openqa.selenium.* +import org.openqa.selenium.interactions.Keyboard +import org.openqa.selenium.interactions.Mouse +import org.openqa.selenium.remote.* +import org.openqa.selenium.remote.internal.JsonToWebElementConverter +import java.util.concurrent.Future +import java.util.logging.Level + +internal class HttpClientWebDriver : RemoteWebDriver() { + private val httpClient: SettableFuture = SettableFuture.create() + + internal fun getHttpClientFuture(): Future { + return httpClient + } + + fun initHttpClient(userName: String, password: String) { + if (httpClient.isDone.not()) { + val provider: CredentialsProvider = BasicCredentialsProvider() + val credentials: Credentials = UsernamePasswordCredentials(userName, password) + provider.setCredentials(AuthScope.ANY, credentials) + httpClient.set( + HttpClientBuilder.create() + .setDefaultCredentialsProvider(provider) + .build() + ) + } + } + + override fun executeScript(script: String?, vararg args: Any?): Any { + throw Exception("not implemented") + } + + override fun findElementById(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun setFileDetector(detector: FileDetector?) { + throw Exception("not implemented") + } + + override fun getSessionId(): SessionId { + throw Exception("not implemented") + } + + override fun log(sessionId: SessionId?, commandName: String?, toLog: Any?, `when`: When?) { + throw Exception("not implemented") + } + + override fun findElementByTagName(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun findElementByXPath(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun findElementsByXPath(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun getTitle(): String { + throw Exception("not implemented") + } + + override fun executeAsyncScript(script: String?, vararg args: Any?): Any { + throw Exception("not implemented") + } + + override fun getMouse(): Mouse { + throw Exception("not implemented") + } + + override fun close() { + throw Exception("not implemented") + } + + override fun findElementByPartialLinkText(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun getWindowHandles(): MutableSet { + throw Exception("not implemented") + } + + override fun setElementConverter(converter: JsonToWebElementConverter?) { + throw Exception("not implemented") + } + + override fun getExecuteMethod(): ExecuteMethod { + throw Exception("not implemented") + } + + override fun setSessionId(opaqueKey: String?) { + throw Exception("not implemented") + } + + override fun findElementsById(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun getScreenshotAs(outputType: OutputType?): X { + return outputType!!.convertFromBase64Png("") + } + + override fun findElementsByPartialLinkText(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun toString(): String { + throw Exception("not implemented") + } + + override fun getErrorHandler(): ErrorHandler { + throw Exception("not implemented") + } + + override fun setFoundBy(context: SearchContext?, element: WebElement?, by: String?, using: String?) { + throw Exception("not implemented") + } + + override fun getCommandExecutor(): CommandExecutor { + throw Exception("not implemented") + } + + override fun findElementsByName(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun findElementsByCssSelector(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun findElements(by: By?): MutableList { + throw Exception("not implemented") + } + + override fun findElements(by: String?, using: String?): MutableList { + throw Exception("not implemented") + } + + override fun getElementConverter(): JsonToWebElementConverter { + throw Exception("not implemented") + } + + override fun findElementByClassName(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun setLogLevel(level: Level?) { + throw Exception("not implemented") + } + + override fun findElementsByLinkText(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun findElementsByClassName(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun getPageSource(): String { + return "none" + } + + override fun setCommandExecutor(executor: CommandExecutor?) { + throw Exception("not implemented") + } + + override fun findElementByName(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun resetInputState() { + throw Exception("not implemented") + } + + override fun get(url: String?) { + throw Exception("not implemented") + } + + override fun findElement(by: By?): WebElement { + return HttpClientWebElement() + } + + override fun findElement(by: String?, using: String?): WebElement { + throw Exception("not implemented") + } + + override fun getWindowHandle(): String { + throw Exception("not implemented") + } + + override fun getFileDetector(): FileDetector { + throw Exception("not implemented") + } + + override fun execute(driverCommand: String?, parameters: MutableMap?): Response { + throw Exception("not implemented") + } + + override fun execute(command: String?): Response { + throw Exception("not implemented") + } + + override fun navigate(): WebDriver.Navigation { + throw Exception("not implemented") + } + + override fun manage(): WebDriver.Options { + throw Exception("not implemented") + } + + override fun findElementByLinkText(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun getKeyboard(): Keyboard { + throw Exception("not implemented") + } + + override fun getCurrentUrl(): String { + return "none" + } + + override fun getCapabilities(): Capabilities { + throw Exception("not implemented") + } + + override fun findElementByCssSelector(using: String?): WebElement { + throw Exception("not implemented") + } + + override fun setErrorHandler(handler: ErrorHandler?) { + throw Exception("not implemented") + } + + override fun switchTo(): WebDriver.TargetLocator { + throw Exception("not implemented") + } + + override fun quit() { + httpClient.get().close() + } + + override fun findElementsByTagName(using: String?): MutableList { + throw Exception("not implemented") + } + + override fun startSession(capabilities: Capabilities?) { + throw Exception("not implemented") + } + + private class HttpClientWebElement : WebElement { + override fun getScreenshotAs(target: OutputType?): X { + throw Exception("not implemented") + } + + override fun isDisplayed(): Boolean { + throw Exception("not implemented") + } + + override fun clear() { + throw Exception("not implemented") + } + + override fun submit() { + throw Exception("not implemented") + } + + override fun getLocation(): Point { + throw Exception("not implemented") + } + + override fun findElement(by: By?): WebElement { + throw Exception("not implemented") + } + + override fun click() { + throw Exception("not implemented") + } + + override fun getTagName(): String { + throw Exception("not implemented") + } + + override fun getSize(): Dimension { + throw Exception("not implemented") + } + + override fun getText(): String { + return "text" + } + + override fun isSelected(): Boolean { + throw Exception("not implemented") + } + + override fun isEnabled(): Boolean { + throw Exception("not implemented") + } + + override fun sendKeys(vararg keysToSend: CharSequence?) { + throw Exception("not implemented") + } + + override fun getAttribute(name: String?): String { + throw Exception("not implemented") + } + + override fun getRect(): Rectangle { + throw Exception("not implemented") + } + + override fun getCssValue(propertyName: String?): String { + throw Exception("not implemented") + } + + override fun findElements(by: By?): MutableList { + throw Exception("not implemented") + } + } + +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt index ff2ba2f..03b6a3d 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt @@ -1,13 +1,17 @@ package com.atlassian.performance.tools.virtualusers.api.config import com.atlassian.performance.tools.jiraactions.api.scenario.Scenario +import com.atlassian.performance.tools.virtualusers.HttpClientWebDriver import com.atlassian.performance.tools.virtualusers.api.VirtualUserLoad import com.atlassian.performance.tools.virtualusers.api.browsers.Browser +import com.atlassian.performance.tools.virtualusers.api.browsers.CloseableRemoteWebDriver import com.atlassian.performance.tools.virtualusers.api.browsers.HeadlessChromeBrowser +import com.atlassian.performance.tools.virtualusers.api.scenarios.HttpClientScenario import com.atlassian.performance.tools.virtualusers.api.users.RestUserGenerator import com.atlassian.performance.tools.virtualusers.api.users.SuppliedUserGenerator import com.atlassian.performance.tools.virtualusers.api.users.UserGenerator import com.atlassian.performance.tools.virtualusers.logs.LogConfiguration +import com.atlassian.performance.tools.virtualusers.scenarios.HttpClientScenarioAdapter import org.apache.logging.log4j.core.config.AbstractConfiguration import java.time.Duration @@ -166,4 +170,50 @@ class VirtualUserBehavior private constructor( userGenerator = userGenerator ) } + + class HttpClientVirtualUsersBuilder( + private var httpClientScenario: Class + ) { + private val browser: Class = HttpClientBrowser::class.java + private val skipSetup = true + private val userGenerator: Class = SuppliedUserGenerator::class.java + + private var load: VirtualUserLoad = VirtualUserLoad.Builder().build() + private var maxOverhead: Duration = Duration.ofMinutes(5) + private var seed: Long = 12345 + private var logging: Class = LogConfiguration::class.java + + fun load(load: VirtualUserLoad) = apply { this.load = load } + fun maxOverhead(maxOverhead: Duration) = apply { this.maxOverhead = maxOverhead } + fun seed(seed: Long) = apply { this.seed = seed } + fun logging(logging: Class) = apply { this.logging = logging } + + @Suppress("DEPRECATION") + fun build(): VirtualUserBehavior = VirtualUserBehavior( + help = false, + scenario = getScenario(), + load = load, + maxOverhead = maxOverhead, + seed = seed, + diagnosticsLimit = 0, + browser = browser, + logging = logging, + skipSetup = skipSetup, + userGenerator = userGenerator + ) + + private fun getScenario(): Class { + HttpClientScenarioAdapter.scenarioClass = httpClientScenario + return HttpClientScenarioAdapter::class.java + } + + internal class HttpClientBrowser : Browser { + override fun start(): CloseableRemoteWebDriver { + return CloseableRemoteWebDriver( + HttpClientWebDriver() + ) + } + } + } + } diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt new file mode 100644 index 0000000..ec3fbbd --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt @@ -0,0 +1,12 @@ +package com.atlassian.performance.tools.virtualusers.api.scenarios + +import com.atlassian.performance.tools.jiraactions.api.SeededRandom +import com.atlassian.performance.tools.jiraactions.api.action.Action +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter +import org.apache.http.impl.client.CloseableHttpClient +import java.net.URI +import java.util.concurrent.Future + +interface HttpClientScenario { + fun getActions(httpClient: Future, jiraUri: URI, seededRandom: SeededRandom, meter: ActionMeter): List +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt new file mode 100644 index 0000000..d255893 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt @@ -0,0 +1,37 @@ +package com.atlassian.performance.tools.virtualusers.scenarios + +import com.atlassian.performance.tools.jiraactions.api.SeededRandom +import com.atlassian.performance.tools.jiraactions.api.WebJira +import com.atlassian.performance.tools.jiraactions.api.action.Action +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter +import com.atlassian.performance.tools.jiraactions.api.memories.UserMemory +import com.atlassian.performance.tools.jiraactions.api.scenario.Scenario +import com.atlassian.performance.tools.virtualusers.HttpClientWebDriver +import com.atlassian.performance.tools.virtualusers.api.scenarios.HttpClientScenario + +internal class HttpClientScenarioAdapter : Scenario { + internal companion object { + @Volatile + internal var scenarioClass: Class? = null + } + + private val scenario: HttpClientScenario = scenarioClass!!.getConstructor().newInstance() + override fun getSetupAction(jira: WebJira, meter: ActionMeter): Action { + return NoopAction() + } + + override fun getLogInAction(jira: WebJira, meter: ActionMeter, userMemory: UserMemory): Action { + val user = userMemory.recall()!! + (jira.driver as HttpClientWebDriver).initHttpClient(user.name, user.password) + return NoopAction() + } + + override fun getActions(jira: WebJira, seededRandom: SeededRandom, meter: ActionMeter): List { + return scenario.getActions((jira.driver as HttpClientWebDriver).getHttpClientFuture(), jira.base, seededRandom, meter) + } + + private class NoopAction : Action { + override fun run() {} + } + +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt new file mode 100644 index 0000000..ab70121 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt @@ -0,0 +1,102 @@ +package com.atlassian.performance.tools.virtualusers.api.scenarios + +import com.atlassian.performance.tools.io.api.directories +import com.atlassian.performance.tools.jiraactions.api.ActionType +import com.atlassian.performance.tools.jiraactions.api.SeededRandom +import com.atlassian.performance.tools.jiraactions.api.action.Action +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter +import com.atlassian.performance.tools.virtualusers.LoadTest +import com.atlassian.performance.tools.virtualusers.api.VirtualUserLoad +import com.atlassian.performance.tools.virtualusers.api.VirtualUserOptions +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserBehavior +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget +import org.apache.http.client.methods.HttpGet +import org.apache.http.impl.client.CloseableHttpClient +import org.assertj.core.api.Assertions +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.net.URI +import java.nio.file.Paths +import java.time.Duration +import java.util.concurrent.Future + +class HttpClientScenarioIT { + private val workspace = Paths.get("test-results") + + @Before + fun before() { + workspace.toFile().deleteRecursively() + } + + @After + fun after() { + workspace.toFile().deleteRecursively() + } + + @Test + fun shouldConsumeHttpClientBasedScenario() { + // given + val loadTest = LoadTest( + VirtualUserOptions( + target = VirtualUserTarget( + webApplication = URI("http://dummy.restapiexample.com/"), + userName = "admin", + password = "admin" + ), + behavior = VirtualUserBehavior.HttpClientVirtualUsersBuilder( + HttpClientScenarioExample::class.java + ).load( + VirtualUserLoad.Builder() + .virtualUsers(3) + .ramp(Duration.ZERO) + .flat(Duration.ofSeconds(3)) + .build() + ).build() + ) + ) + + // when + loadTest.run() + + // then + val metrics = workspace.toFile().directories().flatMap { + it.listFiles()!!.flatMap { actionMetrics -> + actionMetrics.readLines() + } + } + + Assertions.assertThat(metrics.size).isGreaterThan(5) + Assertions.assertThat(metrics).anyMatch { metric -> + metric.contains(""""result":"OK"""") + } + Assertions.assertThat(metrics).noneMatch { metric -> + metric.contains(""""result":"ERROR"""") + } + } + + class HttpClientScenarioExample : HttpClientScenario { + override fun getActions(httpClient: Future, jiraUri: URI, seededRandom: SeededRandom, meter: ActionMeter): List { + return listOf(JqlSearchAction(httpClient, jiraUri, meter)) + } + + private class JqlSearchAction( + private val httpClientFuture: Future, + jiraUri: URI, + private val meter: ActionMeter + ) : Action { + private val httpGet = HttpGet(jiraUri.resolve("api/v1/employees")) + private val actionType = ActionType("A REST call") { Unit } + override fun run() { + val httpClient = httpClientFuture.get() + meter.measure(actionType) { + httpClient.execute(httpGet).use { response -> + if (response.statusLine.statusCode != 200) { + throw Exception("Failed to test Rest $httpGet : $response") + } + } + } + } + } + } +} From 6699a0576acae6431fe81ac84e5720cd5534293b Mon Sep 17 00:00:00 2001 From: Michal Wyrzykowski Date: Thu, 31 Oct 2019 12:38:02 +0100 Subject: [PATCH 2/5] wip generalise api --- .../tools/virtualusers/lib/api/Scenario.java | 42 +++ .../tools/virtualusers/HttpClientWebDriver.kt | 334 ------------------ .../tools/virtualusers/LoadTest.kt | 23 +- .../virtualusers/NewExploratoryVirtualUser.kt | 58 +++ .../tools/virtualusers/NewLoadSegment.kt | 41 +++ .../tools/virtualusers/NewLoadTest.kt | 186 ++++++++++ .../tools/virtualusers/ScenarioAdapter.kt | 58 --- .../tools/virtualusers/api/EntryPoint.kt | 10 +- .../virtualusers/api/VirtualUserOptions.kt | 12 +- .../api/config/VirtualUserBehavior.kt | 63 +--- .../api/config/VirtualUserTarget.kt | 8 +- .../api/scenarios/HttpClientScenario.kt | 12 - .../scenarios/HttpClientScenarioAdapter.kt | 37 -- .../tools/virtualusers/ScenarioAdapterTest.kt | 40 --- .../virtualusers/SimpleWebdriverScenario.kt | 57 +++ .../tools/virtualusers/api/EntryPointIT.kt | 2 +- .../api/NewScenarioApiSupportIT.kt | 37 ++ .../api/scenarios/HttpClientScenarioIT.kt | 102 ------ 18 files changed, 459 insertions(+), 663 deletions(-) create mode 100644 src/main/java/com/atlassian/performance/tools/virtualusers/lib/api/Scenario.java delete mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadSegment.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadTest.kt delete mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapter.kt delete mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt delete mode 100644 src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt delete mode 100644 src/test/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapterTest.kt create mode 100644 src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt create mode 100644 src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/NewScenarioApiSupportIT.kt delete mode 100644 src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt diff --git a/src/main/java/com/atlassian/performance/tools/virtualusers/lib/api/Scenario.java b/src/main/java/com/atlassian/performance/tools/virtualusers/lib/api/Scenario.java new file mode 100644 index 0000000..6aa11a5 --- /dev/null +++ b/src/main/java/com/atlassian/performance/tools/virtualusers/lib/api/Scenario.java @@ -0,0 +1,42 @@ +package com.atlassian.performance.tools.virtualusers.lib.api; + +import com.atlassian.performance.tools.jiraactions.api.action.Action; +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter; +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget; + +import java.util.List; + +/** + * IT doesn't need to be thread safe. Each VU will have own copy of the scenario and own Thread + */ +public abstract class Scenario { + + /** + * vu will use this constructor to create the scenario + * + * @param virtualUserTarget + * @param meter + */ + protected Scenario(VirtualUserTarget virtualUserTarget, ActionMeter meter) { + } + + /** + * The method will be called before VU starts executing actions + */ + public void before() { + + } + + /** + * The method will be called Once to setUp product instance + */ + public void setup() { + + } + + public abstract List getActions(); + + public void cleanUp() { + + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt deleted file mode 100644 index a60bcdf..0000000 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/HttpClientWebDriver.kt +++ /dev/null @@ -1,334 +0,0 @@ -package com.atlassian.performance.tools.virtualusers - -import com.google.common.util.concurrent.SettableFuture -import net.jcip.annotations.GuardedBy -import org.apache.http.auth.AuthScope -import org.apache.http.auth.Credentials -import org.apache.http.auth.UsernamePasswordCredentials -import org.apache.http.client.CredentialsProvider -import org.apache.http.impl.client.BasicCredentialsProvider -import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.impl.client.HttpClientBuilder -import org.openqa.selenium.* -import org.openqa.selenium.interactions.Keyboard -import org.openqa.selenium.interactions.Mouse -import org.openqa.selenium.remote.* -import org.openqa.selenium.remote.internal.JsonToWebElementConverter -import java.util.concurrent.Future -import java.util.logging.Level - -internal class HttpClientWebDriver : RemoteWebDriver() { - private val httpClient: SettableFuture = SettableFuture.create() - - internal fun getHttpClientFuture(): Future { - return httpClient - } - - fun initHttpClient(userName: String, password: String) { - if (httpClient.isDone.not()) { - val provider: CredentialsProvider = BasicCredentialsProvider() - val credentials: Credentials = UsernamePasswordCredentials(userName, password) - provider.setCredentials(AuthScope.ANY, credentials) - httpClient.set( - HttpClientBuilder.create() - .setDefaultCredentialsProvider(provider) - .build() - ) - } - } - - override fun executeScript(script: String?, vararg args: Any?): Any { - throw Exception("not implemented") - } - - override fun findElementById(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun setFileDetector(detector: FileDetector?) { - throw Exception("not implemented") - } - - override fun getSessionId(): SessionId { - throw Exception("not implemented") - } - - override fun log(sessionId: SessionId?, commandName: String?, toLog: Any?, `when`: When?) { - throw Exception("not implemented") - } - - override fun findElementByTagName(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun findElementByXPath(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun findElementsByXPath(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun getTitle(): String { - throw Exception("not implemented") - } - - override fun executeAsyncScript(script: String?, vararg args: Any?): Any { - throw Exception("not implemented") - } - - override fun getMouse(): Mouse { - throw Exception("not implemented") - } - - override fun close() { - throw Exception("not implemented") - } - - override fun findElementByPartialLinkText(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun getWindowHandles(): MutableSet { - throw Exception("not implemented") - } - - override fun setElementConverter(converter: JsonToWebElementConverter?) { - throw Exception("not implemented") - } - - override fun getExecuteMethod(): ExecuteMethod { - throw Exception("not implemented") - } - - override fun setSessionId(opaqueKey: String?) { - throw Exception("not implemented") - } - - override fun findElementsById(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun getScreenshotAs(outputType: OutputType?): X { - return outputType!!.convertFromBase64Png("") - } - - override fun findElementsByPartialLinkText(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun toString(): String { - throw Exception("not implemented") - } - - override fun getErrorHandler(): ErrorHandler { - throw Exception("not implemented") - } - - override fun setFoundBy(context: SearchContext?, element: WebElement?, by: String?, using: String?) { - throw Exception("not implemented") - } - - override fun getCommandExecutor(): CommandExecutor { - throw Exception("not implemented") - } - - override fun findElementsByName(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun findElementsByCssSelector(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun findElements(by: By?): MutableList { - throw Exception("not implemented") - } - - override fun findElements(by: String?, using: String?): MutableList { - throw Exception("not implemented") - } - - override fun getElementConverter(): JsonToWebElementConverter { - throw Exception("not implemented") - } - - override fun findElementByClassName(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun setLogLevel(level: Level?) { - throw Exception("not implemented") - } - - override fun findElementsByLinkText(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun findElementsByClassName(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun getPageSource(): String { - return "none" - } - - override fun setCommandExecutor(executor: CommandExecutor?) { - throw Exception("not implemented") - } - - override fun findElementByName(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun resetInputState() { - throw Exception("not implemented") - } - - override fun get(url: String?) { - throw Exception("not implemented") - } - - override fun findElement(by: By?): WebElement { - return HttpClientWebElement() - } - - override fun findElement(by: String?, using: String?): WebElement { - throw Exception("not implemented") - } - - override fun getWindowHandle(): String { - throw Exception("not implemented") - } - - override fun getFileDetector(): FileDetector { - throw Exception("not implemented") - } - - override fun execute(driverCommand: String?, parameters: MutableMap?): Response { - throw Exception("not implemented") - } - - override fun execute(command: String?): Response { - throw Exception("not implemented") - } - - override fun navigate(): WebDriver.Navigation { - throw Exception("not implemented") - } - - override fun manage(): WebDriver.Options { - throw Exception("not implemented") - } - - override fun findElementByLinkText(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun getKeyboard(): Keyboard { - throw Exception("not implemented") - } - - override fun getCurrentUrl(): String { - return "none" - } - - override fun getCapabilities(): Capabilities { - throw Exception("not implemented") - } - - override fun findElementByCssSelector(using: String?): WebElement { - throw Exception("not implemented") - } - - override fun setErrorHandler(handler: ErrorHandler?) { - throw Exception("not implemented") - } - - override fun switchTo(): WebDriver.TargetLocator { - throw Exception("not implemented") - } - - override fun quit() { - httpClient.get().close() - } - - override fun findElementsByTagName(using: String?): MutableList { - throw Exception("not implemented") - } - - override fun startSession(capabilities: Capabilities?) { - throw Exception("not implemented") - } - - private class HttpClientWebElement : WebElement { - override fun getScreenshotAs(target: OutputType?): X { - throw Exception("not implemented") - } - - override fun isDisplayed(): Boolean { - throw Exception("not implemented") - } - - override fun clear() { - throw Exception("not implemented") - } - - override fun submit() { - throw Exception("not implemented") - } - - override fun getLocation(): Point { - throw Exception("not implemented") - } - - override fun findElement(by: By?): WebElement { - throw Exception("not implemented") - } - - override fun click() { - throw Exception("not implemented") - } - - override fun getTagName(): String { - throw Exception("not implemented") - } - - override fun getSize(): Dimension { - throw Exception("not implemented") - } - - override fun getText(): String { - return "text" - } - - override fun isSelected(): Boolean { - throw Exception("not implemented") - } - - override fun isEnabled(): Boolean { - throw Exception("not implemented") - } - - override fun sendKeys(vararg keysToSend: CharSequence?) { - throw Exception("not implemented") - } - - override fun getAttribute(name: String?): String { - throw Exception("not implemented") - } - - override fun getRect(): Rectangle { - throw Exception("not implemented") - } - - override fun getCssValue(propertyName: String?): String { - throw Exception("not implemented") - } - - override fun findElements(by: By?): MutableList { - throw Exception("not implemented") - } - } - -} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/LoadTest.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/LoadTest.kt index c5654b9..480f9ef 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/LoadTest.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/LoadTest.kt @@ -178,24 +178,23 @@ internal class LoadTest( userMemory: UserMemory, diagnostics: Diagnostics ): ExploratoryVirtualUser { - val scenarioAdapter = ScenarioAdapter(scenario) val maxOverallLoad = load.maxOverallLoad return ExploratoryVirtualUser( node = WebJiraNode(jira), nodeCounter = nodeCounter, - actions = scenarioAdapter.getActions( - jira = jira, - seededRandom = SeededRandom(random.random.nextLong()), - meter = meter + actions = scenario.getActions( + jira, + SeededRandom(random.random.nextLong()), + meter ), - setUpAction = scenarioAdapter.getSetupAction( - jira = jira, - meter = meter + setUpAction = scenario.getSetupAction( + jira, + meter ), - logInAction = scenarioAdapter.getLogInAction( - jira = jira, - meter = meter, - userMemory = userMemory + logInAction = scenario.getLogInAction( + jira, + meter, + userMemory ), maxLoad = maxOverallLoad / load.virtualUsers, diagnostics = diagnostics diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt new file mode 100644 index 0000000..e82e47f --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt @@ -0,0 +1,58 @@ +package com.atlassian.performance.tools.virtualusers + +import com.atlassian.performance.tools.jiraactions.api.action.Action +import com.atlassian.performance.tools.virtualusers.api.TemporalRate +import com.atlassian.performance.tools.virtualusers.collections.CircularIterator +import com.atlassian.performance.tools.virtualusers.measure.ApplicationNode +import com.atlassian.performance.tools.virtualusers.measure.JiraNodeCounter +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import java.time.Duration +import java.time.Instant.now +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Applies load on a Jira via page objects. Explores the instance to learn about data and choose pages to visit. + * Wanders preset Jira pages with different proportions of each page. Their order is random. + */ +internal class NewExploratoryVirtualUser( + private val node: ApplicationNode, + private val nodeCounter: JiraNodeCounter, + private val actions: Iterable, + private val maxLoad: TemporalRate +) { + private val logger: Logger = LogManager.getLogger(this::class.java) + + /** + * Repeats [actions] until [done] is `true`. + */ + fun applyLoad( + done: AtomicBoolean + ) { + logger.info("Applying load...") + nodeCounter.count(node) + val actionNames = actions.map { it.javaClass.simpleName } + logger.debug("Circling through $actionNames") + var actionsPerformed = 0.0 + val start = now() + for (action in CircularIterator(actions)) { + if (done.get()) { + logger.info("Done applying load") + break + } + try { + action.run() + actionsPerformed++ + val expectedTimeSoFar = maxLoad.scaleChange(actionsPerformed).time + val actualTimeSoFar = Duration.between(start, now()) + val extraTime = expectedTimeSoFar - actualTimeSoFar + if (extraTime > Duration.ZERO) { + Thread.sleep(extraTime.toMillis()) + } + } catch (e: Exception) { + logger.error("Failed to run $action, but we keep running", e) + } + } + } + +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadSegment.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadSegment.kt new file mode 100644 index 0000000..2040b19 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadSegment.kt @@ -0,0 +1,41 @@ +package com.atlassian.performance.tools.virtualusers + +import com.atlassian.performance.tools.virtualusers.lib.api.Scenario +import org.apache.logging.log4j.LogManager +import java.io.BufferedWriter +import java.time.Duration +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +internal class NewLoadSegment( + val scenario: Scenario, + val output: BufferedWriter, + val done: AtomicBoolean, + val id: UUID, + val index: Int +) : AutoCloseable { + + override fun close() { + done.set(true) + output.close() + val executor = Executors.newSingleThreadExecutor { + Thread(it) + .apply { name = "close-driver" } + .apply { isDaemon = true } + } + try { + executor + .submit { scenario.cleanUp() } + .get(DRIVER_CLOSE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) + } catch (e: Exception) { + LOGGER.warn("Failed to close WebDriver", e) + } + } + + internal companion object { + private val LOGGER = LogManager.getLogger(this::class.java) + internal val DRIVER_CLOSE_TIMEOUT = Duration.ofSeconds(30) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadTest.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadTest.kt new file mode 100644 index 0000000..cbf2707 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewLoadTest.kt @@ -0,0 +1,186 @@ +package com.atlassian.performance.tools.virtualusers + +import com.atlassian.performance.tools.io.api.ensureDirectory +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter +import com.atlassian.performance.tools.jiraactions.api.measure.output.AppendableActionMetricOutput +import com.atlassian.performance.tools.virtualusers.api.VirtualUserOptions +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget +import com.atlassian.performance.tools.virtualusers.api.diagnostics.* +import com.atlassian.performance.tools.virtualusers.lib.api.Scenario +import com.atlassian.performance.tools.virtualusers.measure.ApplicationNode +import com.atlassian.performance.tools.virtualusers.measure.JiraNodeCounter +import com.google.common.util.concurrent.ThreadFactoryBuilder +import org.apache.logging.log4j.CloseableThreadContext +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.openqa.selenium.WebDriver +import org.openqa.selenium.remote.RemoteWebDriver +import java.io.BufferedWriter +import java.nio.file.Paths +import java.time.Duration +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A [load test](https://en.wikipedia.org/wiki/Load_testing). + */ +internal class NewLoadTest( + private val options: VirtualUserOptions +) { + private val logger: Logger = LogManager.getLogger(this::class.java) + private val behavior = options.behavior + private val target = options.target + private val workspace = Paths.get("test-results") + private val nodeCounter = JiraNodeCounter() + private val diagnosisPatience = DiagnosisPatience(Duration.ofSeconds(5)) + private val diagnosisLimit = DiagnosisLimit(behavior.diagnosticsLimit) + + + private fun createScenario(virtualUserTarget: VirtualUserTarget, actionMeter: ActionMeter): Scenario { + return behavior + .scenario + .getConstructor(VirtualUserTarget::class.java, ActionMeter::class.java) + .newInstance( + virtualUserTarget, + actionMeter + ) as Scenario + } + + private val load = behavior.load + + fun run() { + logger.info("Holding for ${load.hold}.") + Thread.sleep(load.hold.toMillis()) + workspace.toFile().ensureDirectory() + setUpJira() + applyLoad() + val nodesDump = workspace.resolve("nodes.csv") + nodesDump.toFile().bufferedWriter().use { + nodeCounter.dump(it) + } + logger.debug("Dumped node's counts to $nodesDump") + } + + private fun setUpJira() { + CloseableThreadContext.push("setup").use { + val createScenario = createScenario( + virtualUserTarget = target, + actionMeter = ActionMeter(virtualUser = UUID.randomUUID()) + ) + createScenario.setup() + createScenario.cleanUp() + } + } + + private fun applyLoad() { + val userCount = load.virtualUsers + val finish = load.ramp + load.flat + val loadPool = ThreadPoolExecutor( + userCount, + userCount, + 0L, + TimeUnit.MILLISECONDS, + LinkedBlockingQueue(), + ThreadFactoryBuilder().setNameFormat("virtual-user-%d").setDaemon(true).build() + ) + logger.info("Segmenting load across $userCount VUs") + val segments = (0..userCount).map { index -> + segmentLoad(index + 1) + } + logger.info("Load segmented") + segments.forEach { loadPool.submit { applyLoad(it) } } + Thread.sleep(finish.toMillis()) + close(segments) + } + + private fun segmentLoad( + index: Int + ): NewLoadSegment { + val uuid = UUID.randomUUID() + val output = output(uuid) + val scenario = createScenario(options.target, + ActionMeter( + virtualUser = uuid, + output = AppendableActionMetricOutput(output) + ) + ) + + return NewLoadSegment( + scenario = scenario, + output = output, + done = AtomicBoolean(false), + id = uuid, + index = index + ) + } + + private fun output(uuid: UUID): BufferedWriter { + return workspace + .resolve(uuid.toString()) + .toFile() + .ensureDirectory() + .resolve("action-metrics.jpt") + .bufferedWriter() + } + + private fun applyLoad( + segment: NewLoadSegment + ) { + CloseableThreadContext.push("applying load #${segment.id}").use { + val rampUpWait = load.rampInterval.multipliedBy(segment.index.toLong()) + logger.info("Waiting for $rampUpWait") + Thread.sleep(rampUpWait.toMillis()) + val virtualUser = createVirtualUser(segment) + segment.scenario.before() + virtualUser.applyLoad(segment.done) + } + } + + private fun createVirtualUser( + segment: NewLoadSegment + ): NewExploratoryVirtualUser { + val maxOverallLoad = load.maxOverallLoad + return NewExploratoryVirtualUser( + node = object : ApplicationNode { + override fun identify(): String = "todo??" + }, + nodeCounter = nodeCounter, + actions = segment.scenario.actions, + maxLoad = maxOverallLoad / load.virtualUsers + ) + } + + private fun close( + segments: List + ) { + logger.info("Closing segments") + val closePool = Executors.newCachedThreadPool { Thread(it, "close-segment") } + segments + .map { closePool.submit { it.close() } } + .forEach { it.get() } + logger.info("Segments closed") + closePool.shutdown() + } + + private fun RemoteWebDriver.toDiagnosableDriver(): DiagnosableDriver { + return DiagnosableDriver( + this, + LimitedDiagnostics( + ImpatientDiagnostics( + WebDriverDiagnostics(this), + diagnosisPatience + ), + diagnosisLimit + ) + ) + } + + internal data class DiagnosableDriver( + val driver: WebDriver, + val diagnostics: Diagnostics + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapter.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapter.kt deleted file mode 100644 index 5f00389..0000000 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.atlassian.performance.tools.virtualusers - -import com.atlassian.performance.tools.jiraactions.api.SeededRandom -import com.atlassian.performance.tools.jiraactions.api.WebJira -import com.atlassian.performance.tools.jiraactions.api.action.Action -import com.atlassian.performance.tools.jiraactions.api.action.LogInAction -import com.atlassian.performance.tools.jiraactions.api.action.SetUpAction -import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter -import com.atlassian.performance.tools.jiraactions.api.memories.UserMemory -import com.atlassian.performance.tools.jiraactions.api.scenario.Scenario - -/** - * Compatibility layer to avoid MAJOR version bump, because of a transitive dependency bump. - * Adapter avoids to require `jira-actions:3.x` at compile time. We can remove the class, as soon we stop - * to support `jira-actions:2.x`. - */ -internal class ScenarioAdapter( - private val scenario: Scenario -) { - fun getActions(jira: WebJira, seededRandom: SeededRandom, meter: ActionMeter): List { - return scenario.getActions(jira, seededRandom, meter) - } - - fun getLogInAction(jira: WebJira, meter: ActionMeter, userMemory: UserMemory): Action { - return if (isScenario3Compatible()) { - val getLogInActionMethod = scenario::class.java.getMethod("getLogInAction", WebJira::class.java, ActionMeter::class.java, UserMemory::class.java) - getLogInActionMethod(scenario, jira, meter, userMemory) as Action - } else { - LogInAction( - jira = jira, - meter = meter, - userMemory = userMemory - ) - } - } - - fun getSetupAction(jira: WebJira, meter: ActionMeter): Action { - return if (isScenario3Compatible()) { - val getSetupActionMethod = scenario::class.java.getMethod("getSetupAction", WebJira::class.java, ActionMeter::class.java) - getSetupActionMethod(scenario, jira, meter) as Action - } else { - SetUpAction( - jira = jira, - meter = meter - ) - } - } - - private fun isScenario3Compatible(): Boolean { - val methods = scenario::class - .java - .methods - .map { it.name } - - return methods.contains("getSetupAction") && methods.contains("getLogInAction") - } - -} \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPoint.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPoint.kt index 276ee7b..593f5c4 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPoint.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPoint.kt @@ -1,6 +1,7 @@ package com.atlassian.performance.tools.virtualusers.api import com.atlassian.performance.tools.virtualusers.LoadTest +import com.atlassian.performance.tools.virtualusers.NewLoadTest import com.atlassian.performance.tools.virtualusers.logs.LogConfiguration import com.atlassian.performance.tools.virtualusers.logs.LogConfigurationFactory import org.apache.logging.log4j.LogManager @@ -41,7 +42,14 @@ class Application { if (options.help) { options.printHelp() } else { - LoadTest(options).run() + val scenarioPackage = options.behavior.scenario.packageName + if (scenarioPackage == "com.atlassian.performance.tools.virtualusers.lib.api") { + NewLoadTest(options).run() + } else if (scenarioPackage == "com.atlassian.performance.tools.jiraactions.api.scenario") { + LoadTest(options).run() + } else { + throw Exception("not implemented") + } } } diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/VirtualUserOptions.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/VirtualUserOptions.kt index 0d88c24..6f36b2d 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/VirtualUserOptions.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/VirtualUserOptions.kt @@ -49,7 +49,7 @@ class VirtualUserOptions( get() = behavior.load @Deprecated(deprecatedGetterMessage) - val scenario: Class + val scenario: Class<*> get() = behavior.scenario @Deprecated(deprecatedGetterMessage) @@ -78,7 +78,7 @@ class VirtualUserOptions( adminLogin: String = "admin", adminPassword: String = "admin", virtualUserLoad: VirtualUserLoad = VirtualUserLoad(), - scenario: Class = JiraSoftwareScenario::class.java, + scenario: Class<*> = JiraSoftwareScenario::class.java, seed: Long = Random().nextLong(), diagnosticsLimit: Int = 64, allowInsecureConnections: Boolean = false @@ -106,7 +106,7 @@ class VirtualUserOptions( adminLogin: String, adminPassword: String, virtualUserLoad: VirtualUserLoad, - scenario: Class, + scenario: Class<*>, seed: Long, diagnosticsLimit: Int, browser: Class @@ -416,11 +416,9 @@ class VirtualUserOptions( ) } - private fun getScenario(commandLine: CommandLine): Class { + private fun getScenario(commandLine: CommandLine): Class<*> { val scenario = commandLine.getOptionValue(scenarioParameter) - val scenarioClass = Class.forName(scenario) - val scenarioConstructor = scenarioClass.getConstructor() - return (scenarioConstructor.newInstance() as Scenario)::class.java + return Class.forName(scenario) } private fun getBrowser(commandLine: CommandLine): Class { diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt index 03b6a3d..c5d9d74 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserBehavior.kt @@ -1,17 +1,13 @@ package com.atlassian.performance.tools.virtualusers.api.config import com.atlassian.performance.tools.jiraactions.api.scenario.Scenario -import com.atlassian.performance.tools.virtualusers.HttpClientWebDriver import com.atlassian.performance.tools.virtualusers.api.VirtualUserLoad import com.atlassian.performance.tools.virtualusers.api.browsers.Browser -import com.atlassian.performance.tools.virtualusers.api.browsers.CloseableRemoteWebDriver import com.atlassian.performance.tools.virtualusers.api.browsers.HeadlessChromeBrowser -import com.atlassian.performance.tools.virtualusers.api.scenarios.HttpClientScenario import com.atlassian.performance.tools.virtualusers.api.users.RestUserGenerator import com.atlassian.performance.tools.virtualusers.api.users.SuppliedUserGenerator import com.atlassian.performance.tools.virtualusers.api.users.UserGenerator import com.atlassian.performance.tools.virtualusers.logs.LogConfiguration -import com.atlassian.performance.tools.virtualusers.scenarios.HttpClientScenarioAdapter import org.apache.logging.log4j.core.config.AbstractConfiguration import java.time.Duration @@ -23,7 +19,7 @@ class VirtualUserBehavior private constructor( message = "There should be no need to display help from Java API. Read the Javadoc or sources instead." ) internal val help: Boolean, - internal val scenario: Class, + internal val scenario: Class<*>, val load: VirtualUserLoad, val maxOverhead: Duration, internal val seed: Long, @@ -39,7 +35,7 @@ class VirtualUserBehavior private constructor( ) constructor( help: Boolean, - scenario: Class, + scenario: Class<*>, load: VirtualUserLoad, seed: Long, diagnosticsLimit: Int, @@ -64,7 +60,7 @@ class VirtualUserBehavior private constructor( @Suppress("DEPRECATION") constructor( help: Boolean, - scenario: Class, + scenario: Class<*>, load: VirtualUserLoad, seed: Long, diagnosticsLimit: Int, @@ -84,7 +80,7 @@ class VirtualUserBehavior private constructor( message = "Use the VirtualUserBehavior.Builder instead" ) constructor( - scenario: Class, + scenario: Class<*>, load: VirtualUserLoad, seed: Long, diagnosticsLimit: Int, @@ -109,8 +105,10 @@ class VirtualUserBehavior private constructor( load: VirtualUserLoad ): VirtualUserBehavior = Builder(this).load(load).build() + @Deprecated("TODO - can we provide more type safe builder?") + // TODO detect when the new scenario is used with parameters that are no longer supported in new Scenario class Builder( - private var scenario: Class + private var scenario: Class<*> ) { private var load: VirtualUserLoad = VirtualUserLoad.Builder().build() private var maxOverhead: Duration = Duration.ofMinutes(5) @@ -126,6 +124,7 @@ class VirtualUserBehavior private constructor( fun maxOverhead(maxOverhead: Duration) = apply { this.maxOverhead = maxOverhead } fun seed(seed: Long) = apply { this.seed = seed } fun diagnosticsLimit(diagnosticsLimit: Int) = apply { this.diagnosticsLimit = diagnosticsLimit } + @Deprecated("TODO -> new API doesn't use it") fun browser(browser: Class) = apply { this.browser = browser } fun logging(logging: Class) = apply { this.logging = logging } fun skipSetup(skipSetup: Boolean) = apply { this.skipSetup = skipSetup } @@ -170,50 +169,4 @@ class VirtualUserBehavior private constructor( userGenerator = userGenerator ) } - - class HttpClientVirtualUsersBuilder( - private var httpClientScenario: Class - ) { - private val browser: Class = HttpClientBrowser::class.java - private val skipSetup = true - private val userGenerator: Class = SuppliedUserGenerator::class.java - - private var load: VirtualUserLoad = VirtualUserLoad.Builder().build() - private var maxOverhead: Duration = Duration.ofMinutes(5) - private var seed: Long = 12345 - private var logging: Class = LogConfiguration::class.java - - fun load(load: VirtualUserLoad) = apply { this.load = load } - fun maxOverhead(maxOverhead: Duration) = apply { this.maxOverhead = maxOverhead } - fun seed(seed: Long) = apply { this.seed = seed } - fun logging(logging: Class) = apply { this.logging = logging } - - @Suppress("DEPRECATION") - fun build(): VirtualUserBehavior = VirtualUserBehavior( - help = false, - scenario = getScenario(), - load = load, - maxOverhead = maxOverhead, - seed = seed, - diagnosticsLimit = 0, - browser = browser, - logging = logging, - skipSetup = skipSetup, - userGenerator = userGenerator - ) - - private fun getScenario(): Class { - HttpClientScenarioAdapter.scenarioClass = httpClientScenario - return HttpClientScenarioAdapter::class.java - } - - internal class HttpClientBrowser : Browser { - override fun start(): CloseableRemoteWebDriver { - return CloseableRemoteWebDriver( - HttpClientWebDriver() - ) - } - } - } - } diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserTarget.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserTarget.kt index 8f28b27..b59b2b3 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserTarget.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/config/VirtualUserTarget.kt @@ -3,7 +3,7 @@ package com.atlassian.performance.tools.virtualusers.api.config import java.net.URI class VirtualUserTarget( - internal val webApplication: URI, - internal val userName: String, - internal val password: String -) \ No newline at end of file + val webApplication: URI, + val userName: String, + val password: String +) diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt deleted file mode 100644 index ec3fbbd..0000000 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenario.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.atlassian.performance.tools.virtualusers.api.scenarios - -import com.atlassian.performance.tools.jiraactions.api.SeededRandom -import com.atlassian.performance.tools.jiraactions.api.action.Action -import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter -import org.apache.http.impl.client.CloseableHttpClient -import java.net.URI -import java.util.concurrent.Future - -interface HttpClientScenario { - fun getActions(httpClient: Future, jiraUri: URI, seededRandom: SeededRandom, meter: ActionMeter): List -} diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt deleted file mode 100644 index d255893..0000000 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/scenarios/HttpClientScenarioAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.atlassian.performance.tools.virtualusers.scenarios - -import com.atlassian.performance.tools.jiraactions.api.SeededRandom -import com.atlassian.performance.tools.jiraactions.api.WebJira -import com.atlassian.performance.tools.jiraactions.api.action.Action -import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter -import com.atlassian.performance.tools.jiraactions.api.memories.UserMemory -import com.atlassian.performance.tools.jiraactions.api.scenario.Scenario -import com.atlassian.performance.tools.virtualusers.HttpClientWebDriver -import com.atlassian.performance.tools.virtualusers.api.scenarios.HttpClientScenario - -internal class HttpClientScenarioAdapter : Scenario { - internal companion object { - @Volatile - internal var scenarioClass: Class? = null - } - - private val scenario: HttpClientScenario = scenarioClass!!.getConstructor().newInstance() - override fun getSetupAction(jira: WebJira, meter: ActionMeter): Action { - return NoopAction() - } - - override fun getLogInAction(jira: WebJira, meter: ActionMeter, userMemory: UserMemory): Action { - val user = userMemory.recall()!! - (jira.driver as HttpClientWebDriver).initHttpClient(user.name, user.password) - return NoopAction() - } - - override fun getActions(jira: WebJira, seededRandom: SeededRandom, meter: ActionMeter): List { - return scenario.getActions((jira.driver as HttpClientWebDriver).getHttpClientFuture(), jira.base, seededRandom, meter) - } - - private class NoopAction : Action { - override fun run() {} - } - -} diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapterTest.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapterTest.kt deleted file mode 100644 index ac6397d..0000000 --- a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/ScenarioAdapterTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.atlassian.performance.tools.virtualusers - -import com.atlassian.performance.tools.jiraactions.api.SeededRandom -import com.atlassian.performance.tools.jiraactions.api.WebJira -import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter -import com.atlassian.performance.tools.jiraactions.api.memories.adaptive.AdaptiveUserMemory -import com.atlassian.performance.tools.jirasoftwareactions.api.JiraSoftwareScenario -import com.atlassian.performance.tools.virtualusers.api.diagnostics.DriverMock -import org.junit.Assert -import org.junit.Test -import java.net.URI -import java.util.* - -class ScenarioAdapterTest { - private val scenarioAdapter = ScenarioAdapter(JiraSoftwareScenario()) - private val webJira = WebJira( - driver = DriverMock(), - base = URI("http://localhost"), - adminPassword = "admin" - ) - private val meter = ActionMeter( - virtualUser = UUID.randomUUID() - ) - private val userMemory = AdaptiveUserMemory(SeededRandom()) - - @Test - fun shouldReturnLogInAction() { - val logInAction = scenarioAdapter.getLogInAction(webJira, meter, userMemory) - - Assert.assertNotEquals(logInAction, null) - } - - @Test - fun shouldReturnSetupAction() { - val setupAction = scenarioAdapter.getSetupAction(webJira, meter) - - Assert.assertNotEquals(setupAction, null) - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt new file mode 100644 index 0000000..16b0049 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt @@ -0,0 +1,57 @@ +package com.atlassian.performance.tools.virtualusers + +import com.atlassian.performance.tools.jiraactions.api.ActionType +import com.atlassian.performance.tools.jiraactions.api.WebJira +import com.atlassian.performance.tools.jiraactions.api.action.Action +import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter +import com.atlassian.performance.tools.jiraactions.api.memories.User +import com.atlassian.performance.tools.virtualusers.api.browsers.CloseableRemoteWebDriver +import com.atlassian.performance.tools.virtualusers.api.browsers.HeadlessChromeBrowser +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget +import com.atlassian.performance.tools.virtualusers.lib.api.Scenario + +// TODO show that diagnostics can be added via action or scenario decorator +// TODO show that RTE can be disabled via setup +class SimpleWebdriverScenario( + virtualUserTarget: VirtualUserTarget, + private val meter: ActionMeter +) : Scenario( + virtualUserTarget, meter +) { + private val driver: CloseableRemoteWebDriver = HeadlessChromeBrowser().start() + private val jira: WebJira = WebJira( + driver = driver.getDriver(), + base = virtualUserTarget.webApplication, + adminPassword = "admin" + ) + + override fun before() { //todo don't use hardcoded credentials + jira.goToLogin().logIn(User( + "admin", + "admin" + )) + } + + override fun cleanUp() { + driver.close() + } + + override fun getActions(): List { + return listOf(HardcodedViewIssueAction(meter, jira)) + } + + + class HardcodedViewIssueAction( + private val meter: ActionMeter, + private val jira: WebJira + ) : Action { + private val viewIssueAction = ActionType("View Issue") { Unit } + + override fun run() { + meter.measure(viewIssueAction) { + jira.goToIssue("DSEI-1").waitForSummary() + } + } + } + +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPointIT.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPointIT.kt index 29e489f..2e3c06b 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPointIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/EntryPointIT.kt @@ -31,4 +31,4 @@ class EntryPointIT { )) } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/NewScenarioApiSupportIT.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/NewScenarioApiSupportIT.kt new file mode 100644 index 0000000..45a5547 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/NewScenarioApiSupportIT.kt @@ -0,0 +1,37 @@ +package com.atlassian.performance.tools.virtualusers.api + +import com.atlassian.performance.tools.virtualusers.NewLoadTest +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserBehavior +import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget +import com.atlassian.performance.tools.virtualusers.SimpleWebdriverScenario +import org.junit.Test +import java.net.URI +import java.time.Duration + +class NewScenarioApiSupportIT { + + @Test + fun shouldConsumeHttpClientBasedScenario() { + val loadTest = NewLoadTest( + VirtualUserOptions( + target = VirtualUserTarget( + webApplication = URI("http://localhost:8090/jira/"), + userName = "admin", + password = "admin" + ), + behavior = VirtualUserBehavior.Builder( + SimpleWebdriverScenario::class.java + ).load( + VirtualUserLoad.Builder() + .virtualUsers(1) + .ramp(Duration.ZERO) + .flat(Duration.ofSeconds(120)) + .build() + ).build() + ) + ) + + loadTest.run() + } + +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt deleted file mode 100644 index ab70121..0000000 --- a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/api/scenarios/HttpClientScenarioIT.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.atlassian.performance.tools.virtualusers.api.scenarios - -import com.atlassian.performance.tools.io.api.directories -import com.atlassian.performance.tools.jiraactions.api.ActionType -import com.atlassian.performance.tools.jiraactions.api.SeededRandom -import com.atlassian.performance.tools.jiraactions.api.action.Action -import com.atlassian.performance.tools.jiraactions.api.measure.ActionMeter -import com.atlassian.performance.tools.virtualusers.LoadTest -import com.atlassian.performance.tools.virtualusers.api.VirtualUserLoad -import com.atlassian.performance.tools.virtualusers.api.VirtualUserOptions -import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserBehavior -import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget -import org.apache.http.client.methods.HttpGet -import org.apache.http.impl.client.CloseableHttpClient -import org.assertj.core.api.Assertions -import org.junit.After -import org.junit.Before -import org.junit.Test -import java.net.URI -import java.nio.file.Paths -import java.time.Duration -import java.util.concurrent.Future - -class HttpClientScenarioIT { - private val workspace = Paths.get("test-results") - - @Before - fun before() { - workspace.toFile().deleteRecursively() - } - - @After - fun after() { - workspace.toFile().deleteRecursively() - } - - @Test - fun shouldConsumeHttpClientBasedScenario() { - // given - val loadTest = LoadTest( - VirtualUserOptions( - target = VirtualUserTarget( - webApplication = URI("http://dummy.restapiexample.com/"), - userName = "admin", - password = "admin" - ), - behavior = VirtualUserBehavior.HttpClientVirtualUsersBuilder( - HttpClientScenarioExample::class.java - ).load( - VirtualUserLoad.Builder() - .virtualUsers(3) - .ramp(Duration.ZERO) - .flat(Duration.ofSeconds(3)) - .build() - ).build() - ) - ) - - // when - loadTest.run() - - // then - val metrics = workspace.toFile().directories().flatMap { - it.listFiles()!!.flatMap { actionMetrics -> - actionMetrics.readLines() - } - } - - Assertions.assertThat(metrics.size).isGreaterThan(5) - Assertions.assertThat(metrics).anyMatch { metric -> - metric.contains(""""result":"OK"""") - } - Assertions.assertThat(metrics).noneMatch { metric -> - metric.contains(""""result":"ERROR"""") - } - } - - class HttpClientScenarioExample : HttpClientScenario { - override fun getActions(httpClient: Future, jiraUri: URI, seededRandom: SeededRandom, meter: ActionMeter): List { - return listOf(JqlSearchAction(httpClient, jiraUri, meter)) - } - - private class JqlSearchAction( - private val httpClientFuture: Future, - jiraUri: URI, - private val meter: ActionMeter - ) : Action { - private val httpGet = HttpGet(jiraUri.resolve("api/v1/employees")) - private val actionType = ActionType("A REST call") { Unit } - override fun run() { - val httpClient = httpClientFuture.get() - meter.measure(actionType) { - httpClient.execute(httpGet).use { response -> - if (response.statusLine.statusCode != 200) { - throw Exception("Failed to test Rest $httpGet : $response") - } - } - } - } - } - } -} From 3aa1aae995e580090884fdb2a4564e6eaeb717cf Mon Sep 17 00:00:00 2001 From: Michal Wyrzykowski Date: Thu, 31 Oct 2019 16:57:50 +0100 Subject: [PATCH 3/5] Add diagnostics to SimpleWebdriverScenario --- .../virtualusers/SimpleWebdriverScenario.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt index 16b0049..9466dcb 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt @@ -8,9 +8,9 @@ import com.atlassian.performance.tools.jiraactions.api.memories.User import com.atlassian.performance.tools.virtualusers.api.browsers.CloseableRemoteWebDriver import com.atlassian.performance.tools.virtualusers.api.browsers.HeadlessChromeBrowser import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget +import com.atlassian.performance.tools.virtualusers.api.diagnostics.WebDriverDiagnostics import com.atlassian.performance.tools.virtualusers.lib.api.Scenario -// TODO show that diagnostics can be added via action or scenario decorator // TODO show that RTE can be disabled via setup class SimpleWebdriverScenario( virtualUserTarget: VirtualUserTarget, @@ -37,7 +37,9 @@ class SimpleWebdriverScenario( } override fun getActions(): List { - return listOf(HardcodedViewIssueAction(meter, jira)) + val actions = listOf(HardcodedViewIssueAction(meter, jira)) + val diagnostics = WebDriverDiagnostics(driver.getDriver()) + return actions.map { action -> DiagnosableAction(action, diagnostics) } } @@ -54,4 +56,17 @@ class SimpleWebdriverScenario( } } + class DiagnosableAction( // should be available in API? + private val action: Action, + private val diagnostics: WebDriverDiagnostics + ) : Action { + override fun run() { + try { + action.run() + } catch (e: Exception) { + diagnostics.diagnose(e) + } + } + } + } From 02448bf052900c7c60e121056b8ce8eb88754db5 Mon Sep 17 00:00:00 2001 From: Michal Wyrzykowski Date: Thu, 31 Oct 2019 17:00:35 +0100 Subject: [PATCH 4/5] Limit diagnostics in SimpleWebdriverScenario --- .../virtualusers/SimpleWebdriverScenario.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt index 9466dcb..02c47a6 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/virtualusers/SimpleWebdriverScenario.kt @@ -10,6 +10,7 @@ import com.atlassian.performance.tools.virtualusers.api.browsers.HeadlessChromeB import com.atlassian.performance.tools.virtualusers.api.config.VirtualUserTarget import com.atlassian.performance.tools.virtualusers.api.diagnostics.WebDriverDiagnostics import com.atlassian.performance.tools.virtualusers.lib.api.Scenario +import java.util.concurrent.atomic.AtomicInteger // TODO show that RTE can be disabled via setup class SimpleWebdriverScenario( @@ -39,7 +40,7 @@ class SimpleWebdriverScenario( override fun getActions(): List { val actions = listOf(HardcodedViewIssueAction(meter, jira)) val diagnostics = WebDriverDiagnostics(driver.getDriver()) - return actions.map { action -> DiagnosableAction(action, diagnostics) } + return actions.map { action -> DiagnosableAction(action, diagnostics, 10) } } @@ -58,13 +59,24 @@ class SimpleWebdriverScenario( class DiagnosableAction( // should be available in API? private val action: Action, - private val diagnostics: WebDriverDiagnostics + private val diagnostics: WebDriverDiagnostics, + limit: Int = Int.MAX_VALUE ) : Action { + companion object { + private val limitCounter = AtomicInteger(Int.MAX_VALUE) + } + + init { + limitCounter.compareAndSet(Int.MAX_VALUE, limit) + } + override fun run() { try { action.run() } catch (e: Exception) { - diagnostics.diagnose(e) + if (limitCounter.getAndDecrement() > 0) { + diagnostics.diagnose(e) + } } } } From 3e455a617d0505fa1653a913c3c2faf6393e4cde Mon Sep 17 00:00:00 2001 From: Michal Wyrzykowski Date: Thu, 31 Oct 2019 17:05:25 +0100 Subject: [PATCH 5/5] add todo --- .../performance/tools/virtualusers/NewExploratoryVirtualUser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt index e82e47f..b7dd080 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/virtualusers/NewExploratoryVirtualUser.kt @@ -32,7 +32,7 @@ internal class NewExploratoryVirtualUser( logger.info("Applying load...") nodeCounter.count(node) val actionNames = actions.map { it.javaClass.simpleName } - logger.debug("Circling through $actionNames") + logger.debug("Circling through $actionNames") // TODO Circling through [DiagnosableAction] :( var actionsPerformed = 0.0 val start = now() for (action in CircularIterator(actions)) {