Compose Desktop testing - how to check if something is visible? - kotlin

Given some simple content:
#Composable
fun MyContent() {
var showThing by remember { mutableStateOf(false) }
if (showThing) {
Box(Modifier.testTag("thing")) {
Text("The Thing")
}
}
}
If I try to test whether the thing has been displayed:
#OptIn(ExperimentalTestApi::class)
class Scratch {
#get:Rule
val compose = createComposeRule()
#Test
fun test() {
runBlocking(Dispatchers.Main) {
compose.setContent {
MyContent()
}
compose.awaitIdle()
compose.onNodeWithTag("thing").assertIsNotDisplayed()
}
}
}
I get this:
An operation is not implemented.
kotlin.NotImplementedError: An operation is not implemented.
at androidx.compose.ui.test.DesktopAssertions_desktopKt.checkIsDisplayed(DesktopAssertions.desktop.kt:23)
at androidx.compose.ui.test.AssertionsKt.assertIsNotDisplayed(Assertions.kt:49)
at Scratch$test$1.invokeSuspend(Scratch.kt:44)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
...
I thought testing whether something was displayed or not would be the most basic thing to test, but it isn't supported by the framework yet. The test framework is experimental, so I was expecting to find things missing, but not like this.
Is there another way to do this which I'm missing? All the tutorials out there talk about assertIsDisplayed() being the way, but maybe there is an alternative?

It's not a direct substitute, but unfortunately, JB Compose Desktop has these limitations in the UI test suite. Aside from using only JUnit 4, and not being compatible with the newer version, many assertion methods and also screen interaction methods are not implemented, such as the .assertIsNotDisplayed() that you tried to use, and also actions like .performTextInput().
An alternative for your problem would be using other methods like .assertDoesNotExist() and .assertExists().
It's not going to tell you if the element is in the boundaries of the screen and visible, but at least will tell you that your node exists and is instantiated, which is something, and it's better than nothing.
Until JetBrains implement the complete desktop test suite, we need to work with what we have, or maybe try implementing some things as a workaround.
In your case, this will work:
#OptIn(ExperimentalTestApi::class)
class Scratch {
#get:Rule
val compose = createComposeRule()
#Test
fun test() {
runBlocking(Dispatchers.Main) {
compose.setContent {
MyContent()
}
compose.awaitIdle()
compose.onNodeWithTag("thing").assertDoesNotExist()
}
}

Related

Kotlin: retain coroutine context in scenario with nested runBlocking

I'm fairly new to kotlin coroutines, and I have what I think is a somewhat esoteric use case related to how runBlocking and coroutine contexts interact.
To start with, a simple example. Let's say I've got a dead simple context element. Nothing fancy.
class ExampleContext(val s: String) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<ExampleContext>
}
When I run these examples, they behave exactly the way I'd expect them to:
runBlocking(ExampleContext("foo")) {
println(coroutineContext[ExampleContext.Key]?.s) // prints "foo
}
runBlocking(ExampleContext("foo")) {
launch {
println(coroutineContext[ExampleContext.Key]?.s) // prints "foo"
}
}
runBlocking(ExampleContext("foo")) {
launch(ExampleContext("bar")) {
println(coroutineContext[ExampleContext.Key]?.s) // prints "bar"
}
}
When I do this it prints null (as I would expect it to, because it runBlocking defaults to having EmptyContext in its constructor):
runBlocking(ExampleContext("foo")) {
runBlocking {
println(coroutineContext[ExampleContext.Key]?.s) // prints null
}
}
So here's my conundrum. The docs (and all the guidance I've found on the web) basically say don't do this: runBlocking is supposed to be run at the outermost layer of the coroutine logic and that's it. No nesting. What I'm working on is a library that needs to populate some context for access inside code that I don't own that gets called later (basically, you can think of it like an interceptor). The rough pseudocode looks a little like this:
class MyLibrary(otherPeoplesLogic: OtherPeoplesBusinessLogic) {
fun <IN, OUT> execute(input: IN): OUT {
... do my library's thing, including adding in a custom context element ...
try {
return otherPeoplesLogic.execute(input)
} finally {
... do my library's cleanup ...
}
}
}
To support coroutines in OtherPeoplesBusinessLogic, all I'd really have to do is add runBlocking like this:
class MyLibrary(otherPeoplesLogic: OtherPeoplesBusinessLogic) {
fun <IN, OUT> execute(input: IN): OUT {
... do my library's thing ...
runBlocking(myCustomContext) {
try {
return otherPeoplesLogic.execute(input)
} finally {
... do my library's cleanup ...
}
}
}
}
So long as all OtherPeoplesBusinessLogic::execute does is launch/async/etc, everything is fine: myCustomContext will be accessible. What I'm worried about is what happens if OtherPeoplesBusinessLogic::execute (which I'm not in control of) misbehaves and does its own runBlocking call with no context argument passed at all: what I think will happen is that myCustomContext will just silently get dropped like the example above. Not good, because it needs to be accessible.
Phew. A lot of explanation. Thanks for bearing with me. :)
So my ultimate question here is this: is there anything I can do (outside of scolding the users of my library to not call runBlocking) to prevent an accidental nested runBlocking call from dropping my context? Or am I just out of luck here and should scrap the whole idea?

How to do Sequential Tests with Espresso Kotlin

So I want to make my Practice test suite sequential, meaning, tests build on top of each other. Currently its restarting the app every time a test finishes, but I would like for the app to remain open.
I've tried using #BeforeAll but its not working, it makes me add JUnit5.4 to the class path and even after I do It still red, meaning It doesn't like it for some reason.
Anyway, I think its the rule, I think the activity is making my tests restart every time each one finishes, id like it to not do that or if there's a different test rule that I can use that doesn't do that then that would be magnificent.
class Practice {
#get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java);
#Before
fun setUp() {
onView(withText("Log In With Mobile Code")).perform(click());
onView(withResourceName("txtMobileAccessCode")).check(matches(withText("Company Code
or\nMobile Access Code")));
}
#Test
fun clickOnEnterAccessCode() {
onView(withResourceName("txtCodeEntry")).perform(typeText("CodeGoesHere"));
}
#Test
fun enterCode() {
onView(withResourceName("btnCodeSubmit")).perform(click());
}
}
The problem is in using an ActivityScenarioRule to drive things; this calls ActivityScenario.close() by default at the end of each test, which will reset your app.
Instead, look at controlling the activity's lifecycle yourself by dealing directly with ActivityScenario. There will be more overhead, but you'll have much more control over things and be able to not call close() until you want to.
From the Android Developer Docs the syntax you're looking for is the following:
val scenario = launchActivity<MainActivity>()
Thank you Mike Collins, couldn't have done this without you.
This is the end product, with this is possible to make my whole suite sequential I only have to declare scenario in the first test and the rest keep using the activity.
#Before
fun setUp() {
val scenario = launchActivity<MainActivity>()
Espresso.onView(ViewMatchers.withText("Log In With Mobile
Code")).perform(ViewActions.scrollTo())
Espresso.onView(ViewMatchers.withText("Log In With Mobile
Code")).perform(ViewActions.click());
Espresso.onView(ViewMatchers.withResourceName("txtMobileAccessCode"))
.check(ViewAssertions.matches(ViewMatchers.withText("Company Code or\nMobile
Access Code")));
}
And with this I dont even have to use
#get:Rule
val activityRule = ActivityScenarioRulePractice(MainActivity::class.java);
I can leave that out all together
class Test1 {
#Before
fun setUp() {
val scenario = launchActivity<MainActivity>()
Espresso.onView(ViewMatchers.withText("Log In With Mobile Code")).perform(ViewActions.scrollTo())
Espresso.onView(ViewMatchers.withText("Log In With Mobile Code")).perform(ViewActions.click());
Espresso.onView(ViewMatchers.withResourceName("txtMobileAccessCode")).check(ViewAssertions.matches(ViewMatchers.withText("Company Code or\nMobile Access Code")));
}
#Test
fun clickOnEnterAccessCode() {
Espresso.onView(ViewMatchers.withResourceName("txtCodeEntry")).perform(ViewActions.typeText("Bluebook"));
Espresso.onView(ViewMatchers.withResourceName("btnCodeSubmit")).perform(ViewActions.click());
Espresso.onView(ViewMatchers.withText("<b>Network connection unavailable</b><br/>\n" +
"Please check your network settings and try again.")).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
}
#After
fun cleanUp() {
reportHelper.label("Stopping App");
}
}
I might have to put something on the #After but so far its closing everything automatically.

Kodein + Ktor = mutation attempt of frozen kotlin.collections.HashMap - why?

I have been struggling with this exception for couple of days recently.
I have a kotlin multiplatform project with these dependencies:
kotlin=1.5.10
kodein=7.6.0
ktor=1.6.0 (uses kotlin coroutines 1.5.0-native-mt internally)
And I have been getting mentioned exception while trying to use httpClient in native:
at kotlin.Throwable#<init>(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/Throwable.kt:23)
at kotlin.Exception#<init>(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/Exceptions.kt:23)
at kotlin.RuntimeException#<init>(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/Exceptions.kt:34)
at kotlin.native.concurrent.InvalidMutabilityException#<init>(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/native/concurrent/Freezing.kt:22)
at <global>.ThrowInvalidMutabilityException(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/native/concurrent/Internal.kt:93)
at <global>.MutationCheck(Unknown Source)
at kotlin.collections.HashMap.<set-length>#internal(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/collections/HashMap.kt:16)
at kotlin.collections.HashMap#addKey(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/collections/HashMap.kt:292)
at kotlin.collections.HashMap#put(/Users/teamcity3/buildAgent/work/290aee0e088a1666/runtime/src/main/kotlin/kotlin/collections/HashMap.kt:68)
at org.kodein.di.internal.DITreeImpl#find(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/internal/DITreeImpl.kt:132)
at org.kodein.di.DITree#find$default(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/DITree.kt:36)
at org.kodein.di.internal.DIContainerImpl#factory(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/internal/DIContainerImpl.kt:158)
at org.kodein.di.DIContainer#factory$default(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/DIContainer.kt:32)
at org.kodein.di.DIContainer#provider(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/DIContainer.kt:76)
at org.kodein.di.DIContainer#provider$default(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/DIContainer.kt:75)
at org.kodein.di.internal.DirectDIBaseImpl#Instance(/Users/runner/work/Kodein-DI/Kodein-DI/kodein-di/src/commonMain/kotlin/org/kodein/di/internal/DirectDIImpl.kt:30)
at InvalidMutabilitySampleTest.<init>$lambda-6$lambda-5$lambda-4$lambda-3$lambda-2#internal(/Users/r.juszczyk/StudioProjects/lmhu-multiplatform-app/MultiplatformApp/src/iosTest/kotlin/InvalidMutabilitySampleTest.kt:26)
at InvalidMutabilitySampleTest.$<init>$lambda-6$lambda-5$lambda-4$lambda-3$lambda-2$FUNCTION_REFERENCE$3.invoke#internal(/Users/r.juszczyk/StudioProjects/lmhu-multiplatform-app/MultiplatformApp/src/iosTest/kotlin/InvalidMutabilitySampleTest.kt:25)
at InvalidMutabilitySampleTest.$<init>$lambda-6$lambda-5$lambda-4$lambda-3$lambda-2$FUNCTION_REFERENCE$3.$<bridge-UNNN>invoke(/Users/r.juszczyk/StudioProjects/lmhu-multiplatform-app/MultiplatformApp/src/iosTest/kotlin/InvalidMutabilitySampleTest.kt:25)
at io.ktor.client.HttpClientConfig.install$<anonymous>_1#internal(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:69)
at io.ktor.client.HttpClientConfig.$install$<anonymous>_1$FUNCTION_REFERENCE$17.invoke#internal(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:65)
at io.ktor.client.HttpClientConfig.$install$<anonymous>_1$FUNCTION_REFERENCE$17.$<bridge-UNNN>invoke(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:65)
at io.ktor.client.features.json.JsonFeature.Feature#prepare(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt:129)
at io.ktor.client.HttpClientConfig.install$<anonymous>_1-2#internal(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:77)
at io.ktor.client.HttpClientConfig.$install$<anonymous>_1-2$FUNCTION_REFERENCE$18.invoke#internal(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig.$install$<anonymous>_1-2$FUNCTION_REFERENCE$18.$<bridge-UNNN>invoke(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig#install(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt:97)
at io.ktor.client.HttpClient#<init>(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt:172)
at io.ktor.client.HttpClient#<init>(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt:81)
at io.ktor.client#HttpClient(/Users/administrator/Documents/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt:43)
I managed to reproduce that crash in a test:
class InvalidMutabilitySampleTest {
val di = DI {
import(DI.Module("Some Module") {
bind<Json>() with provider {
Json {
prettyPrint = true
isLenient = true
}
}
bind<HttpClient>() with provider {
HttpClient{
install(JsonFeature) {
serializer = KotlinxSerializer(instance())
}
}
}
})
}
val httpClient: HttpClient by di.instance()
#Test
fun invalidMutabilityTest() {
println(httpClient)
}
}
I also managed to fix it by changing:
HttpClient{
install(JsonFeature) {
serializer = KotlinxSerializer(instance())
}
}
to:
val json = instance<Json>()
HttpClient{
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
}
Then I noticed that HttpClient does a very specific thing - it freezes itself in the init block.
I managed to reproduce it with this sample code:
class FrozenConstructor(val block: ()->Unit) {
init {
freeze()
}
}
class InvalidMutabilitySampleTest2 {
val di = DI {
import(DI.Module("Some Module") {
bind<String>() with provider {
"lolo"
}
bind<FrozenConstructor>() with provider {
FrozenConstructor{
instance<String>()
}
}
})
}
val frozenConstructor: FrozenConstructor by di.instance()
#Test
fun invalidMutabilityTest() {
println(frozenConstructor)
}
}
So my theory is following:
kodein tries to provide FrozenConstructor class
FrozenConstructor is created and it freezes itself, and its member which references kodein
kodein tries to cache provided dependency and try to mutate internal MutableMap which is frozen and everything crashes
Can someone confirm that this is more or less correct , and correct me if not?
And also can you guys suggest the best way of handling it, and other pitfalls which are waiting there?
Is it a kodein bug?
Why kodein has to store something in a mutable map if I am using with provider not with singleton ?
I think you've pretty much figured it out. Ktor freezes itself and all of its configuration in order to ensure that it can be used across threads in Kotlin/Native. Kodein assumes you will only ever touch it from one thread and is not safe to freeze. (Whether that's a limitation, a bug, or a design flaw is maybe up for interpretation.)
To work around these issues, you want to avoid accidentally capturing this references in your HttpClient configuration that refer to Kodein internals. A good way to do this is to grab instances from DI in helper variables outside the HttpClient lambda, as you've discovered.
You are correct.
FrozenConstructor freezes itself, and therefore its block property, which, as a lambda, freezes all its captures, which includes the DI container.
Kodein-DI does not support freezing (and therefore native multi-threading).
The decision has been made not to invest in making it compatible with native multi-thrading since JB has announced working on a new GC.

ModelBuilder end() not working as it should "com.badlogic.gdx.utils.GdxRuntimeException: Call end() first"

I'm just trying to get a basic box rendered with LibGDX (using Kotlin & LibKTX) but am running into some issues.
If I call the ModelBuilder createBox function without the specified begin() and end() functions my box is not rendered. I checked the materials, camera position, the bounding box, added a light source, etc. but it's just.. not there. I figured the issue was with the way I was building the nodes, as I can't find an issue with the Material.
This is how I am trying to render my box:
class HomeView(private val baseUI: BaseUI) : KtxScreen {
private val cam by lazy { PerspectiveCamera(67f, baseUI.aWidth, baseUI.aHeight) }
private val boxInstance: ModelInstance
private val modelBatch: ModelBatch
private val modelBuilder: ModelBuilder by lazy { ModelBuilder() }
private val vertexAttributes =
VertexAttributes.Usage.Position.toLong() or
VertexAttributes.Usage.Normal.toLong() or
VertexAttributes.Usage.TextureCoordinates.toLong()
private val greenMat by lazy { Material(ColorAttribute.createDiffuse(Color.GREEN)) }
private val environment by lazy { Environment() }
//------ end global
init {
cam.position.set(vec3(-10f, -10f, 10f))
cam.lookAt(0f, 0f, 0f)
cam.near = .1f
cam.far = 10f
cam.update()
modelBatch = ModelBatch()
modelBuilder.begin()
modelBuilder.createBox(5f, 5f, 5f, greenMat, vertexAttributes)
boxInstance = ModelInstance(modelBuilder.end())
environment.set(ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f))
environment.add(DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f))
}
override fun render(delta: Float) {
Gdx.gl.glViewport(0, 0, baseUI.aWidth.toInt(), baseUI.aHeight.toInt())
//this is working, screen appears grey
clearScreen(.3f, .3f, .3f, 1f)
modelBatch.begin(cam)
modelBatch.render(boxInstance, environment)
modelBatch.end()
}
//rest omitted
}
and here is my BaseUI class that I'm using to add the screen (I'm just trying to test screens out, this is all just for testing purposes so ignore the inefficiency please)
class BaseUI : KtxGame<KtxScreen>(), KtxApplicationAdapter {
val aWidth by lazy { Gdx.graphics.width.toFloat() }
val aHeight by lazy { Gdx.graphics.height.toFloat() }
override fun create() {
addScreen(HomeView(this))
setScreen<HomeView>()
}
override fun render() {
super<KtxGame>.render()
}
//rest ommitted
}
When I run this I get the following error:
Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: Call end() first
which makes it seem like I need to call modelBatch.end() before I even create the nodes, which is confusing. I feel like I am doing something very basic wrong here, as I was able to get the basic 3D examples working back when I was trying this with Java a few years back.
So, two questions:
Why is LibGDX saying that I need to call end() before I create the nodes with ModelBuilder?
Is using modelBuilder.begin() and modelBuilder.end() actually the best way to use the ModelBuilder? I've yet to see a 3D example do this. Admittedly, all the 3D examples I have found have been from like 2013 so this might just be something that's been added. The LibGDX 3D section says to use this set of tutorials that do not use the begin() and end() functions, so I'm a bit confused as to what is the "best practice".
Thanks for any help!
edit: I tried it with a loaded model and it's having the same issue. Hmm..
edit2: Thank you Xoppa for helping me figure out what was wrong. The LibKTX specific function clearScreen() did not incorporate the GL20.GL_COLOR_BUFFER_BIT clear function as expected, which was my bad from reading their documentation. Adding this to my render function displayed the green box as expected:
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT)
LibGDX is not saying to call end() before you create a node, but that there is already an existing model that was created. Therefore, before creating another one you need to call end().
From your code sample, the reason why GdxRuntimeException is being thrown is that you call begin() and then createBox(). createBox() actually calls begin() in the function. (take a look here: https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g3d/utils/ModelBuilder.java) Therefore, the next begin() call the model is not null and the exception is thrown.
For best practices, if createBox() can satisfy your request than just use that. If you need something more complicated, such that createSphere(), createCapsule() don't work for you, then you need to call begin(), part(...), and end().
Hope this helps!

Programmatically execute Gatling tests

I want to use something like Cucumber JVM to drive performance tests written for Gatling.
Ideally the Cucumber features would somehow build a scenario dynamically - probably reusing predefined chain objects similar to the method described in the "Advanced Tutorial", e.g.
val scn = scenario("Scenario Name").exec(Search.search("foo"), Browse.browse, Edit.edit("foo", "bar")
I've looked at how the Maven plugin executes the scripts, and I've also seen mention of using an App trait but I can't find any documentation for the later and it strikes me that somebody else will have wanted to do this before...
Can anybody point (a Gatling noob) in the direction of some documentation or example code of how to achieve this?
EDIT 20150515
So to explain a little more:
I have created a trait which is intended to build up a sequence of, I think, ChainBuilders that are triggered by Cucumber steps:
trait GatlingDsl extends ScalaDsl with EN {
private val gatlingActions = new ArrayBuffer[GatlingBehaviour]
def withGatling(action: GatlingBehaviour): Unit = {
gatlingActions += action
}
}
A GatlingBehaviour would look something like:
object Google {
class Home extends GatlingBehaviour {
def execute: ChainBuilder =
exec(http("Google Home")
.get("/")
)
}
class Search extends GatlingBehaviour {...}
class FindResult extends GatlingBehaviour {...}
}
And inside the StepDef class:
class GoogleStepDefinitions extends GatlingDsl {
Given( """^the Google search page is displayed$""") { () =>
println("Loading www.google.com")
withGatling(Home())
}
When( """^I search for the term "(.*)"$""") { (searchTerm: String) =>
println("Searching for '" + searchTerm + "'...")
withGatling(Search(searchTerm))
}
Then( """^"(.*)" appears in the search results$""") { (expectedResult: String) =>
println("Found " + expectedResult)
withGatling(FindResult(expectedResult))
}
}
The idea being that I can then execute the whole sequence of actions via something like:
val scn = Scenario(cucumberScenario).exec(gatlingActions)
setup(scn.inject(atOnceUsers(1)).protocols(httpConf))
and then check the reports or catch an exception if the test fails, e.g. response time too long.
It seems that no matter how I use the 'exec' method it tries to instantly execute it there and then, not waiting for the scenario.
Also I don't know if this is the best approach to take, we'd like to build some reusable blocks for our Gatling tests that can be constructed via Cucumber's Given/When/Then style. Is there a better or already existing approach?
Sadly, it's not currently feasible to have Gatling directly start a Simulation instance.
Not that's it's not technically feasible, but you're just the first person to try to do this.
Currently, Gatling is usually in charge of compiling and can only be passed the name of the class to load, not an instance itself.
You can maybe start by forking io.gatling.app.Gatling and io.gatling.core.runner.Runner, and then provide a PR to support this new behavior. The former is the main entry point, and the latter the one can instanciate and run the simulation.
I recently ran into a similar situation, and did not want to fork gatling. And while this solved my immediate problem, it only partially solves what you are trying to do, but hopefully someone else will find this useful.
There is an alternative. Gatling is written in Java and Scala so you can call Gatling.main directly and pass it the arguments you need to run the Gatling Simulation you want. The problem is, the main explicitly calls System.exit so you have to also use a custom security manager to prevent it from actually exiting.
You need to know two things:
the class (with the full package) of the Simulation you want to run
example: com.package.your.Simulation1
the path where the binaries are compiled.
The code to run a Simulation:
protected void fire(String gatlingGun, String binaries){
SecurityManager sm = System.getSecurityManager();
System.setSecurityManager(new GatlingSecurityManager());
String[] args = {"--simulation", gatlingGun,
"--results-folder", "gatling-results",
"--binaries-folder", binaries};
try {
io.gatling.app.Gatling.main(args);
}catch(SecurityException se){
LOG.debug("gatling test finished.");
}
System.setSecurityManager(sm);
}
The simple security manager i used:
public class GatlingSecurityManager extends SecurityManager {
#Override
public void checkExit(int status){
throw new SecurityException("Tried to exit.");
}
#Override
public void checkPermission(Permission perm) {
return;
}
}
The problem is then getting the information you want out of the simulation after it has been run.