Skip to content

Commit 095b00c

Browse files
committed
Initial commit of virtualdig tool
Currently very rough around the edges, but the GoTo(url) action is working. Setting up the Dig test suite is tricky and dangerous right now. Spring boot is taking forever to load, replacing with Jetty may be better
0 parents  commit 095b00c

34 files changed

+1400
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.gradle
2+
.idea
3+
gradle
4+
build
5+
**/.DS_Store
6+
**/elm-stuff
7+
**/node_modules
8+
elm-app/assets/elm.js

build.gradle

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
group 'io.virtualdig'
2+
version '0.1-SNAPSHOT'
3+
4+
task wrapper(type: Wrapper) {
5+
gradleVersion = '3.3'
6+
}

dig/build.gradle

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
group 'io.virtualdig'
2+
version '0.1-SNAPSHOT'
3+
4+
buildscript {
5+
ext.kotlin_version = '1.1.0'
6+
repositories {
7+
mavenCentral()
8+
}
9+
dependencies {
10+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11+
}
12+
}
13+
14+
apply plugin: 'kotlin'
15+
16+
repositories {
17+
jcenter()
18+
mavenCentral()
19+
}
20+
21+
ext {
22+
springBootVersion = '1.5.2.RELEASE'
23+
jacksonVersion = '2.8.7'
24+
25+
mockitoKotlinVersion = '1.3.0'
26+
}
27+
28+
dependencies {
29+
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
30+
compile "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion"
31+
compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion"
32+
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
33+
34+
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
35+
testCompile 'org.assertj:assertj-core:3.3.0'
36+
testCompile 'io.damo.aspen:aspen-spring:2.0.0'
37+
testCompile "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}"
38+
}
39+
40+
sourceSets {
41+
test {
42+
kotlin.srcDir 'src/test'
43+
java.srcDir 'src/test'
44+
resources.srcDir 'src/test/resources'
45+
}
46+
main {
47+
kotlin.srcDir 'src/main'
48+
java.srcDir 'src/main'
49+
resources.srcDir 'src/main/resources'
50+
}
51+
}
52+
53+
54+
task wrapper(type: Wrapper) {
55+
gradleVersion = '3.3'
56+
}
57+
58+
task copyFrontEnd(dependsOn: ":elm-app:build", type: Copy) {
59+
from project(':elm-app').buildDir
60+
into "build/resources/main/public/"
61+
}
62+
63+
build.dependsOn('copyFrontEnd')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.virtualdig
2+
3+
import java.io.File
4+
import java.net.URI
5+
import java.util.*
6+
import java.util.Locale.ENGLISH
7+
8+
open class BrowserLauncher(val env : Map<String, String>, val commandRunner: CommandRunner, val operatingSystem: OperatingSystem) {
9+
private var browserProcess : Process? = null
10+
private val dirsToDelete : MutableList<File> = ArrayList()
11+
12+
open fun launchBrowser(uri : URI, headless: Boolean = false) : Boolean {
13+
if(operatingSystem.isOSX()) {
14+
if(headless) throw NotImplementedError("Headless OSX is not implemented")
15+
16+
val browserPath = env["DIG_BROWSER"] ?: "\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\""
17+
val url = uri.toURL().toExternalForm()
18+
if(browserPath.toLowerCase(ENGLISH).contains("firefox")) {
19+
return launchFirefox(browserPath, url)
20+
} else if(browserPath.toLowerCase(ENGLISH).contains("chrome")) {
21+
return launchChrome(browserPath, url)
22+
}
23+
} else if(operatingSystem.isLinux()) {
24+
var browserPath = env["DIG_BROWSER"] ?: "google-chrome"
25+
if(headless) {
26+
browserPath = "xvfb-run $browserPath"
27+
}
28+
val url = uri.toURL().toExternalForm()
29+
if(browserPath.toLowerCase(ENGLISH).contains("firefox")) {
30+
return launchFirefox(browserPath, url)
31+
} else if(browserPath.toLowerCase(ENGLISH).contains("chrome")) {
32+
return launchChrome(browserPath, url)
33+
}
34+
}
35+
36+
return false
37+
}
38+
39+
open fun stopBrowser() {
40+
Thread.sleep(100)
41+
browserProcess?.destroy()
42+
browserProcess = null
43+
dirsToDelete.forEach(File::deleteOnExit)
44+
}
45+
46+
private fun launchFirefox(browserCommand: String?, url : String?) : Boolean {
47+
val dir = File("/")
48+
49+
val command = "$browserCommand -safe-mode \"$url\""
50+
browserProcess = commandRunner.runCommand(command, dir)
51+
return true
52+
}
53+
54+
55+
private fun launchChrome(browserCommand: String?, url : String?) : Boolean {
56+
val dir = File("/")
57+
val random = Random().nextInt()
58+
dirsToDelete.add(File("/tmp/$random"))
59+
60+
val switches = "--app=\"$url\" --no-first-run --user-data-dir=/tmp/$random --disk-cache-dir=/dev/null"
61+
browserProcess = commandRunner.runCommand("$browserCommand $switches", dir)
62+
return true
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.virtualdig
2+
3+
import java.io.File
4+
import java.util.*
5+
6+
open class CommandRunner {
7+
open fun runCommand(command: String, workingDir: File): Process? {
8+
val split: List<String> = getSplitArgs(command)
9+
return ProcessBuilder(split)
10+
.directory(workingDir)
11+
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
12+
.redirectError(ProcessBuilder.Redirect.INHERIT)
13+
.start()
14+
}
15+
16+
companion object {
17+
fun getSplitArgs(command: String): List<String> {
18+
val split: List<String> = command.split(" ")
19+
val finalSplit: MutableList<String> = ArrayList()
20+
for (index in 0..split.lastIndex) {
21+
val s = split[index]
22+
if (!finalSplit.none() && finalSplit.last().contains(s)) continue
23+
24+
if (s.startsWith('"')) {
25+
var joinedString = s
26+
for (x in split.slice(index + 1..split.lastIndex)) {
27+
joinedString = "$joinedString $x"
28+
if (x.endsWith('"')) {
29+
break
30+
}
31+
}
32+
finalSplit.add(joinedString)
33+
} else {
34+
finalSplit.add(s)
35+
}
36+
}
37+
return finalSplit.map { it.replace("\"", "") }
38+
}
39+
}
40+
}

dig/src/main/io/virtualdig/Dig.kt

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.virtualdig
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.context.ConfigurableApplicationContext
5+
import java.net.URI
6+
import java.util.concurrent.CompletableFuture
7+
import java.util.concurrent.TimeUnit
8+
9+
class Dig(
10+
val browserLauncher: BrowserLauncher = BrowserLauncher(System.getenv(), CommandRunner(), OperatingSystem())
11+
) {
12+
private val context: CompletableFuture<ConfigurableApplicationContext> = CompletableFuture()
13+
private val controller: CompletableFuture<DigController> = CompletableFuture()
14+
private val elmUri: URI = URI("http://localhost:8650")
15+
16+
init {
17+
start(arrayOf("--server.port=8650"))
18+
}
19+
20+
fun goTo(uri: URI) {
21+
digController().goTo(uri)
22+
}
23+
24+
fun clickLink(withText: String, withId: String? = null) {
25+
digController().clickLink(withText, withId)
26+
}
27+
28+
fun close() {
29+
this.context.get(5, TimeUnit.SECONDS).close()
30+
Thread.sleep(100)
31+
browserLauncher.stopBrowser()
32+
}
33+
34+
private fun digController() = this.controller.get(5, TimeUnit.SECONDS)
35+
36+
private fun start(args: Array<String>) {
37+
val context: ConfigurableApplicationContext = SpringApplication.run(WebSocketConfig::class.java, *args)
38+
this.context.complete(context)
39+
this.controller.complete(context.getBean(DigController::class.java))
40+
browserLauncher.launchBrowser(elmUri, false)
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.virtualdig
2+
3+
4+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.readValue
6+
import io.virtualdig.exceptions.DigWebsiteException
7+
import org.springframework.beans.factory.config.BeanDefinition.SCOPE_SINGLETON
8+
import org.springframework.context.annotation.Scope
9+
import org.springframework.web.socket.TextMessage
10+
import org.springframework.web.socket.WebSocketSession
11+
import org.springframework.web.socket.handler.TextWebSocketHandler
12+
import java.net.URI
13+
import java.net.URL
14+
import java.util.*
15+
import java.util.concurrent.CompletableFuture
16+
import java.util.concurrent.TimeUnit
17+
import kotlin.properties.Delegates
18+
19+
@Scope(SCOPE_SINGLETON)
20+
class DigController : TextWebSocketHandler() {
21+
private val futureSession: CompletableFuture<WebSocketSession> = CompletableFuture()
22+
private fun webSocketSession() = futureSession.get(5, TimeUnit.SECONDS)
23+
24+
var messageListeners: MutableList<(String) -> Unit> = mutableListOf()
25+
fun listenToNextMessage(handler: (String) -> Unit) {
26+
synchronized(messageListeners) {
27+
messageListeners.add(handler)
28+
}
29+
}
30+
31+
var message: String by Delegates.observable("latestMessage") {
32+
_, _, new ->
33+
synchronized(messageListeners) {
34+
messageListeners.forEach { it(new) }
35+
messageListeners.clear()
36+
}
37+
}
38+
39+
@Throws(Exception::class)
40+
override fun afterConnectionEstablished(session: WebSocketSession) {
41+
if (futureSession.isDone && futureSession.get().id != session.id) {
42+
throw Exception("Session ids do not match. VirtualDig does not support multiple websocket connections at once")
43+
}
44+
45+
if (!futureSession.isDone) {
46+
futureSession.complete(session)
47+
}
48+
}
49+
50+
override fun handleTextMessage(session: WebSocketSession?, message: TextMessage?) {
51+
if (message == null) {
52+
throw Exception("Message was null in the main websocket hanndler!")
53+
}
54+
if (session == null) {
55+
throw Exception("Session was null in the main websocket hanndler!")
56+
}
57+
58+
if (futureSession.isDone && futureSession.get().id != session.id) {
59+
throw Exception("Session ids do not match. VirtualDig does not support multiple websocket connections at once")
60+
}
61+
62+
if (message.payload == null) {
63+
throw Exception("Response was empty, something went wrong")
64+
}
65+
66+
this.message = message.payload
67+
val payload = message.payload
68+
println("RECEIVED MESSAGE $payload")
69+
}
70+
71+
fun goTo(uri: URI) {
72+
val resultWaiter: CompletableFuture<TestResult> = CompletableFuture()
73+
listenToNextMessage({ message ->
74+
val result: TestResult = jacksonObjectMapper().readValue(message)
75+
resultWaiter.complete(result)
76+
})
77+
78+
val session: WebSocketSession = webSocketSession() ?: throw Exception("No session exists yet")
79+
80+
val url: URL = uri.toURL() ?: throw Exception("URI provided was invalid")
81+
82+
val urlString = url.toExternalForm()
83+
84+
val goToAction = GoToAction(uri = urlString)
85+
session.sendMessage(TextMessage(jacksonObjectMapper().writeValueAsString(goToAction)))
86+
87+
val (result, testMessage) = resultWaiter.get(10, TimeUnit.SECONDS)
88+
89+
if (result == "Failure") throw DigWebsiteException("Browser failed to go to URL: $urlString\n\n$testMessage")
90+
}
91+
92+
93+
fun clickLink(withText: String, withId: String? = null) {
94+
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
95+
}
96+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.virtualdig
2+
3+
data class GoToAction(val action: TestAction = TestAction("GoTo"), val uri: String)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.virtualdig
2+
3+
import java.util.Locale.ENGLISH
4+
5+
6+
open class OperatingSystem {
7+
open fun isOSX() : Boolean {
8+
return osPropertyString().indexOf("mac") >= 0 || osPropertyString().indexOf("darwin") >= 0
9+
}
10+
11+
open fun isLinux() : Boolean {
12+
return osPropertyString().indexOf("nux") >= 0
13+
}
14+
15+
open fun isWindows() : Boolean {
16+
return osPropertyString().indexOf("win") >= 0
17+
}
18+
19+
private fun osPropertyString() = System.getProperty("os.name").toLowerCase(ENGLISH)
20+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.virtualdig
2+
3+
data class TestAction (
4+
val actionType : String
5+
)
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.virtualdig
2+
3+
data class TestResult(val result: String, val message: String)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.virtualdig
2+
3+
4+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
5+
import org.springframework.boot.autoconfigure.SpringBootApplication
6+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
7+
import org.springframework.context.annotation.Bean
8+
import org.springframework.context.annotation.Configuration
9+
import org.springframework.web.socket.WebSocketHandler
10+
import org.springframework.web.socket.config.annotation.EnableWebSocket
11+
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
12+
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
13+
14+
@SpringBootApplication
15+
@Configuration
16+
@EnableWebSocket
17+
@EnableAutoConfiguration(exclude = arrayOf(DataSourceAutoConfiguration::class))
18+
open class WebSocketConfig : WebSocketConfigurer {
19+
20+
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
21+
registry.addHandler(digHandler(), "/dig").setAllowedOrigins("*")
22+
}
23+
24+
@Bean
25+
open fun digHandler(): WebSocketHandler {
26+
return DigController()
27+
}
28+
}

0 commit comments

Comments
 (0)