[JAVA] I want to be eventually even in kotlin

It's become commonplace to partially automate tests. It seems that some people sometimes use Thread.sleep () to wait for the browser to work. wait a minute. Doesn't it change depending on the sleep time and execution environment? If you want to test the process of waiting for a fixed time, you can use Thread.sleep (). However, I think that the sleep time is adjusted on an ad hoc basis, such as "It depends on the network how long it will take, but it will not work unless you wait for about 2 seconds."

I wrote earlier How to wait for screen drawing in Selenium. At this time, I wanted to use normally {} that was in ScalaTest in Java, so I implemented it in Java. I want to use it in kotlin this time, no, I can call Java classes and methods from kotlin, but I think if I write it in kotlin, it will be more beautiful. I also use vavr. Unlike the last time, let's see how to flesh out after writing the basic part.

Think about the interface

Here, instead of the Java / Kotlin interface, we will consider what kind of form annually should be easy to use. When you use it, you want to feel free to use it like this.

driver.get(url) 
eventually {
    //It takes time to open the url, so an error will occur immediately after that.
    assert(driver.findElementById("id"))
}

Let's just allow the parentheses to increase when specifying the deadline and waiting time. You can prepare a default timeout, usually without parentheses, and specify it only when you absolutely need to specify it.

broswer.login()
eventually ({
    assert(browser.contents().contains(data))
}, Duration.ofSeconds(2), Duration.ofSeconds(1))

eventually implementation

Since kotilin has a default argument, you don't have to use it to create a similar method like Java with different arguments.

object Eventually {
    val DEFAULT_TIMEOUT = Duration.ofSeconds(10)
    val DEFAULT_INTERVAL = Duration.ofSeconds(3)

    fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
...

However, this interface required both parentheses and curly braces, as follows, even without a timeout.

eventually ({
    // ...
})

I can't help it, so I'll prepare another one.

    fun <R> eventually(f: () -> R) = Eventually.eventually(f, DEFAULT_TIMEOUT, DEFAULT_INTERVAL)
    fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
...

You can now call either of the following:

eventually { ... }
eventually ({ ... }, timeout, interval)
...

Of course, since it is kotlin, you can also specify the argument name.

//I want to specify only interval
eventually ({ ... }, interval=Duration.ofSeconds(1))
...

Logic implementation

First, consider a function that "processes f () and retries if an exception occurs".

fun <R> tryExecute(f: () -> R):R {
    return try {
        f()
    } catch (t: Throwable) {
        tryExecute(f)
    }
}

Now you have a function that loops forever as long as you keep getting errors. Let's set a deadline because it is a problem if it does not stop forever. Here we use java.time.Instant. I want to specify the timeout in Duration, but I will explain it later. Why should I add tailrec to the recursion?

tailrec fun <R> tryExecute(f: () -> R, until: Instant):R {
    if(now()>until) throw RuntimeException("I can not do it")
    return try {
        f()
    } catch (t: Throwable) {
        Thread.sleep(interval)
        tryExecute(f, until)
    }
}

If f () throws an exception immediately, one CPU (all depending on the implementation of f ()) will be used up until the deadline, and if an error occurs, it will wait for a certain period of time. This time we will use java.time.Duration to represent the period.

tailrec fun <R> tryExecute(f: () -> R, until: Instant, interval: Duration):R {
    if(now()>until) throw RuntimeException("I can not do it")
    return try {
        f()
    } catch (t: Throwable) {
        Thread.sleep(interval)
        tryExecute(f, until, interval)
    }
}

Now, you want the exception that f () throws. Required for debugging. Let's pass the exception that occurred when recursing and return it as the cause of the exception when it finally times out. The argument definition is getting longer, so I'll make it one character.

tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Throwable):R {
    if(now() > u) throw RuntimeException("I can not do it", t)
    return try {
        f()
    } catch (t: Throwable) {
        Thread.sleep(interval)
        tryExecute(f, u, i, t)
    }
}

Now when it times out, cause () will pick up the last caught exception. The logic is now complete.

eventually () implementation

Let's call the current logic from the method we just created. Calculate the expiration date by adding the timeout given by Duration to the current time.

val start = Instant.now()
val until = start.plusMillis(timeout.toMillis())

tryEvent takes the last caught exception as an argument, but at first no exception occurs and it is unpleasant to pass null, so set Option and Option.none () Let's pass. Oh, this Option uses vavr (formerly javaslang). I will not explain this this time.


    tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Option<Throwable>): R {
        if (Instant.now() > u) throw t.getOrElse(TimeoutException())
        return try {
            f()
        } catch (t: Throwable) {
            Thread.sleep(i.toMillis())
            tryExecute(f, u, i, Option.some(t))
        }
    }

tryExecute(f, until, interval, Option.none())

TimeoutException is thrown when a timeout occurs before the exception occurs even once. So, when you catch this exception, create an error message along with the elapsed time.

        //Caller
        try {
            tryExecute(f, until, interval, Option.none())
        } catch (t: Throwable) {
            throw createException(start, t)
        }

Message composition function. replace will convert to value if there is a key in map for string s. It's recursive, but you can fold it with map.fold.

    fun replace(s: String, m: Map<String, String>): String =
        if (m.isEmpty) s else replace(s.replace(":" + m.head()._1, m.head()._2), m.tail())

    private fun createException(start: Instant, t: Throwable): Throwable {
        val messageMap = HashMap.ofEntries<String, String>(
                Tuple2("time", Duration.between(start, Instant.now()).toString()),
                Tuple2("message", t.message)
        )
        return RuntimeException(replace(MESSAGE_TEMPLATE, messageMap), t)
    }

That's all there is to it. The whole thing is like this.

Eventually.kt


import io.vavr.Tuple2
import io.vavr.collection.HashMap
import io.vavr.collection.Map
import io.vavr.control.Option
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeoutException

object Eventually {
    val DEFAULT_TIMEOUT = Duration.ofSeconds(10)
    val DEFAULT_INTERVAL = Duration.ofSeconds(3)

    val MESSAGE_TEMPLATE = "Eventually failed over :time. Last message is:\n:message";
    internal tailrec fun replace(s: String, m: Map<String, String>): String =
        if (m.isEmpty) s 
        else replace(s.replace(":" + m.head()._1, m.head()._2), m.tail())

    private tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Option<Throwable>): R {
        if (Instant.now() > u) throw t.getOrElse(TimeoutException())
        return try {
            f()
        } catch (t: Throwable) {
            Thread.sleep(i.toMillis())
            tryExecute(f, u, i, Option.some(t))
        }
    }

    private fun createException(start: Instant, t: Throwable): Throwable {
        val messageMap = HashMap.ofEntries<String, String>(
                Tuple2("time", Duration.between(start, Instant.now()).toString()),
                Tuple2("message", t.message)
        )
        return RuntimeException(replace(MESSAGE_TEMPLATE, messageMap), t)
    }

    fun <R> eventually(f: () -> R) = Eventually.eventually(f, DEFAULT_TIMEOUT, DEFAULT_INTERVAL)
    fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
        val start = Instant.now()
        val until = start.plusMillis(timeout.toMillis())
        return try {
            tryExecute(f, until, interval, Option.none())
        } catch (t: Throwable) {
            throw createException(start, t)
        }
    }

At first, I tried to use io.vavr.kotlin.Try without using try {}, and tried and errored, and triedEvent () returned Either <R, Throwable>, but this one I changed it because it was cleaner. However, the number of lines has not decreased much compared to Java version. Please let me know if there is a better way.

test

Actually, we will test each function while implementing it. For example, when you write replace (), it checks if that part works as intended. Since replace can be private, you can delete testReplace () after confirming that replace () is called in the eventually test. In that case, change ʻinternal fun replace ()toprivate fun replace ()`. I'm leaving it here.

EventuallyTest.kt


import <src>.Eventually.eventually
import <src>.Eventually

import io.vavr.Tuple2
import io.vavr.collection.HashMap
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.slf4j.LoggerFactory
import java.lang.IllegalArgumentException
import java.time.Duration

typealias e = Tuple2<String, String>

class EventuallyTest {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @Test
    fun testReplace() {
        val r = Eventually.replace("this is :1, :2, :x", HashMap.ofEntries(
                e("1", "changed"),
                e("2", "zzzzz"),
                e("x", "yyyyy")
        ))
        log.info(r)
        assertEquals("this is changed, zzzzz, yyyyy", r)
    }

    @Test
    fun testEventually() {
        val r = eventually {
            log.info("aaa")
            "a"
        }
        assertEquals("a", r)
    }

    @Test
    fun testEventually2Sec() {
        try {
            eventually({
                log.info("aaa")
                throw IllegalArgumentException("x")
            }, timeout = Duration.ofSeconds(2))
        }catch (e: Exception){
            assertEquals("x", e.cause!!.message)
        }
    }
}

KotlinTest

Apparently KotlinTest [eventually](https://github.com/kotlintest/kotlintest/blob/master/kotlintest-assertions/src/jvmMain/kotlin/io/kotlintest/ There seems to be Eventually.kt). There is also FunSpec etc. and it seems convenient to use like ScalaTest. The package relationship of KotlinTest itself is complicated and the hurdle seems to be high, but if it can be introduced, this is also good. There is also an extension method for arrow.

Recommended Posts

I want to be eventually even in kotlin
I want to use Clojure's convenient functions in Kotlin
I want to set the conditions to be displayed in collection_check_boxes
Even in Java, I want to output true with a == 1 && a == 2 && a == 3
I want to create a Parquet file even in Ruby
I want to RSpec even at Jest!
I want to use @Autowired in Servlet
I want to send an email in Java.
I want to use arrow notation in Ruby
Even in Java, I want to output true with a == 1 && a == 2 && a == 3 (PowerMockito edition)
I want to pass APP_HOME to logback in Gradle
rsync4j --I want to touch rsync in Java.
[Xcode] I want to manage images in folders
I want to get the value in Ruby
Even in Java, I want to output true with a == 1 && a == 2 && a == 3 (Javassist second decoction)
Even in Java, I want to output true with a == 1 && a == 2 && a == 3 (black magic edition)
I want to do something like "cls" in Java
I want to embed any TraceId in the log
I want to use fish shell in Laradock too! !!
I want to use ES2015 in Java too! → (´ ・ ω ・ `)
I want to define a function in Rails Console
I want to transition screens with kotlin and java!
I want to stop snake case in table definition
I want to click a GoogleMap pin in RSpec
I want to be aware of the contents of variables!
I want to convert characters ...
[Active Admin] I want to specify the scope of the collection to be displayed in select_box
I want to find a relative path in a situation using Path
I want to perform high-speed prime factorization in Ruby (ABC177E)
I want to make a list with kotlin and java!
I want to make a function with kotlin and java!
I tried to implement a buggy web application in Kotlin
I want to transition to the same screen in the saved state
I want to implement various functions with kotlin and java!
I want to simplify the conditional if-else statement in Java
[CQ Engine] I want to handle collections like Stream or .Net LINQ even in Java 7.
I want to Flash Attribute in Spring even if I set a reverse proxy! (do not do)
Swift: I want to chain arrays
I want to use FormObject well
I want to convert InputStream to String
I want to docker-compose up Next.js!
I want to be able to think and write regular expressions myself. ..
I want to return to the previous screen with kotlin and java!
[Ruby] I want to put an array in a variable. I want to convert to an array
I want to get some properties as JSON strings in Jackson!
I want to display the images under assets/images in the production environment
I want to add devise in Rails, but I can't bundle install
[Java] I want to perform distinct with the key in the object
I want to change the value of Attribute in Selenium of Ruby
[Android] I want to get the listener from the button in ListView
I want to develop a web application!
[Rails] I want to send data of different models in a form
I want to write JSP in Emacs more easily than the default.
I want to write a nice build.gradle
I want to eliminate duplicate error messages
I want to make an ios.android app
I want to display background-ground-image on heroku.
I want to select multiple items with a custom layout in Dialog
I want to use DBViewer with Eclipse 2018-12! !!
I want to write a unit test!
(Limited to Java 7 or later) I want you to compare objects in Objects.equals