Skip to content

Commit

Permalink
Merge pull request #90 from Shynixn/development
Browse files Browse the repository at this point in the history
Merge changes to Master --release
  • Loading branch information
Shynixn authored Dec 21, 2022
2 parents 165e299 + 70da48c commit 6e24d13
Show file tree
Hide file tree
Showing 19 changed files with 432 additions and 19 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ tasks.register("printVersion") {

subprojects {
group 'com.github.shynixn.mccoroutine'
version '2.8.0'
version '2.9.0'

sourceCompatibility = 1.8

Expand Down
20 changes: 10 additions & 10 deletions docs/wiki/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,35 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.9.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.9.0")
}
```

=== "BungeeCord"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.9.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.9.0")
}
```

=== "Sponge"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.9.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.9.0")
}
```

=== "Velocity"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.8.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.9.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.9.0")
}
```

Expand All @@ -60,8 +60,8 @@ dependencies {
**plugin.yml**
```yaml
libraries:
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.8.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.8.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.9.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.9.0
```

=== "Other Server"
Expand Down
117 changes: 117 additions & 0 deletions docs/wiki/docs/unittests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Unit-Tests with MCCoroutine

(This site is only relevant for Spigot, Paper and CraftBukkit. If you need Unit-Tests support for BungeeCord, Sponge or
Velocity, please submit an issue on GitHub)

If you try to write Unit- or IntegrationTests for your Minecraft plugin, you may need to test suspend functions. These
functions
may use ``plugin.launch{...}`` or other extension methods from MCCoroutine.

However, extensive mocking is required to get MCCoroutine to work without a running server. As a solution to this
problem, a new test dependency is available, which
closely simulates MCCoroutine under real conditions. This means you can focus on writing your tests and get a very close
feedback to the real environment.

### 1. Add the dependency

**Do not** shade this library into your final plugin.jar file. This should only be available during UnitTests.

```kotlin
dependencies {
testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.9.0")
}
```

### 2. Create a test method

```kotlin
import org.junit.jupiter.api.Test

class MyExampleTest {
@Test
fun testCase01(){
}
}
```

### 3. Change the MCCoroutine Production-Driver to the Test-Driver


```kotlin
import org.junit.jupiter.api.Test

class MyExampleTest {

init {
/**
* This switches MCCoroutine to the test implementation of MCCoroutine.
* It affects all the tests in the current session.
*/
MCCoroutine.Driver = TestMCCoroutine.Driver
}

@Test
fun testCase01(){
}
}
```

#### 4. Use MCCoroutine in the same way as on your server

```kotlin
import org.junit.jupiter.api.Test

class MyExampleTest {

init {
/**
* This switches MCCoroutine to the test implementation of MCCoroutine.
* It affects all the tests in the current session.
*/
MCCoroutine.Driver = TestMCCoroutine.Driver
}

@Test
fun testCase01(){
// Uses the mocking library called Mockito to mock a plugin instance.
// It does not matter how you create a plugin instance. Other mocking libraries work as well.
val plugin = Mockito.mock(Plugin::class.java)

// We need to use runBlocking here, otherwise the test exits early
runBlocking(plugin.minecraftDispatcher) {
println("Step 1: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

withContext(Dispatchers.IO) {
println("Step 2: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
}

println("Step 3: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

// As always, prefer using Dispatchers.IO instead of plugin.asyncDispatcher.
withContext(plugin.asyncDispatcher) {
println("Step 4: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
}

println("Step 5: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

// Just as an example, we can also use plugin.launch
plugin.launch {
println("Step 6: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
println("Step 7: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
}.join() // Wait until finished.
}
}
}
```









1 change: 1 addition & 0 deletions docs/wiki/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ nav:
- 'Coroutines in onDisable': plugindisable.md
- 'Exception Handling': exception.md
- 'Timing Measurements': timings.md
- 'Unit-Testing': unittests.md
- 'FAQ': faq.md
theme:
name: material
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import kotlin.coroutines.CoroutineContext
*/
internal val mcCoroutine: MCCoroutine by lazy {
try {
Class.forName("com.github.shynixn.mccoroutine.bukkit.impl.MCCoroutineImpl")
Class.forName(MCCoroutine.Driver)
.getDeclaredConstructor().newInstance() as MCCoroutine
} catch (e: Exception) {
throw RuntimeException(
Expand All @@ -24,11 +24,10 @@ internal val mcCoroutine: MCCoroutine by lazy {
}
}


/**
* Gets the configuration instance of MCCoroutine.
*/
val Plugin.mcCoroutineConfiguration : MCCoroutineConfiguration
val Plugin.mcCoroutineConfiguration: MCCoroutineConfiguration
get() {
return mcCoroutine.getCoroutineSession(this).mcCoroutineConfiguration
}
Expand Down Expand Up @@ -221,6 +220,14 @@ val Int.ticks: Long
* Hidden internal MCCoroutine interface.
*/
interface MCCoroutine {
companion object {
/**
* Allows to change the driver to load different kinds of MCCoroutine implementations.
* e.g. loading the implementation for UnitTests.
*/
var Driver: String = "com.github.shynixn.mccoroutine.bukkit.impl.MCCoroutineImpl"
}

/**
* Get coroutine session for the given plugin.
*/
Expand Down
2 changes: 2 additions & 0 deletions mccoroutine-bukkit-sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9")

compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT")

testImplementation(project(":mccoroutine-bukkit-test"))
testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT")
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDa
*/
suspend fun getUserDataFromPlayerAsync(player: Player): Deferred<UserData> {
return coroutineScope {
println("[UserDataCache/getUserDataFromPlayerAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
println("[UserDataCache/getUserDataFromPlayerAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}")
if (!cache.containsKey(player)) {
cache[player] = async(plugin.asyncDispatcher) {
println("[UserDataCache/getUserDataFromPlayerAsync] Is downloading data on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
println("[UserDataCache/getUserDataFromPlayerAsync] Is downloading data on Thread:${Thread.currentThread().name}/${Thread.currentThread().id})}")
fakeDatabase.getUserDataFromPlayer(player)
}
}
Expand Down
2 changes: 1 addition & 1 deletion mccoroutine-bukkit-sample/src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: MCCoroutine-Sample
version: 2.8.0
version: 2.9.0
author: Shynixn
main: com.github.shynixn.mccoroutine.bukkit.sample.MCCoroutineSamplePlugin
commands:
Expand Down
82 changes: 82 additions & 0 deletions mccoroutine-bukkit-sample/src/test/java/ExampleUnitTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import com.github.shynixn.mccoroutine.bukkit.MCCoroutine
import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher
import com.github.shynixn.mccoroutine.bukkit.launch
import com.github.shynixn.mccoroutine.bukkit.minecraftDispatcher
import com.github.shynixn.mccoroutine.bukkit.sample.impl.FakeDatabase
import com.github.shynixn.mccoroutine.bukkit.sample.impl.UserDataCache
import com.github.shynixn.mccoroutine.bukkit.test.TestMCCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import kotlin.test.assertEquals

class ExampleUnitTest {

init {
/**
* This switches MCCoroutine to the test implementation of MCCoroutine.
* It affects all the tests in the current session.
*/
MCCoroutine.Driver = TestMCCoroutine.Driver
}

@Test
fun test1() {
// Uses the mocking library called Mockito to mock a plugin instance.
// It does not matter how you create a plugin instance. Other mocking libraries work as well.
val plugin = Mockito.mock(Plugin::class.java)

// We need to use runBlocking here, otherwise the test exits early
runBlocking(plugin.minecraftDispatcher) {
println("Step 1: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

withContext(Dispatchers.IO) {
println("Step 2: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
}

println("Step 3: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

// As always, prefer using Dispatchers.IO instead of plugin.asyncDispatcher.
withContext(plugin.asyncDispatcher) {
println("Step 4: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
}

println("Step 5: " + Thread.currentThread().name + "/" + Thread.currentThread().id)

// Just as an example, we can also use plugin.launch
plugin.launch {
println("Step 6: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
delay(1000)
println("Step 7: " + Thread.currentThread().name + "/" + Thread.currentThread().id)
}.join() // Wait until finished.
}
}

@Test
fun test2() {
// Uses the mocking library called Mockito to mock a plugin and a player instance.
// It does not matter how you create a plugin instance. Other mocking libraries work as well.
val plugin = Mockito.mock(Plugin::class.java)
val player = Mockito.mock(Player::class.java)

// The 'Unit' we want to test.
val fakeDatabase = FakeDatabase()
val classUnderTest = UserDataCache(plugin, fakeDatabase)

// Act and Assert.
runBlocking(plugin.minecraftDispatcher) {
val data1 = classUnderTest.getUserDataFromPlayerAsync(player)
val data2 = classUnderTest.getUserDataFromPlayerAsync(player)

// Should be the same instance because of cache hit. Hashcode should be equal.
assertEquals(data1, data2)
}
}
}
15 changes: 15 additions & 0 deletions mccoroutine-bukkit-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repositories {
maven {
url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
}
}

dependencies {
implementation(project(":mccoroutine-bukkit-api"))

compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT")

testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.shynixn.mccoroutine.bukkit.test

import com.github.shynixn.mccoroutine.bukkit.test.impl.TestMCCoroutineImpl

interface TestMCCoroutine {
companion object {
/**
* The driver to load the test implementation of MCCoroutine.
* Useful for UnitTests.
*/
val Driver = TestMCCoroutineImpl::class.java.name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.github.shynixn.mccoroutine.bukkit.test.dispatcher

import kotlinx.coroutines.CoroutineDispatcher
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext

internal class TestAsyncCoroutineDispatcher(private val minecraftDispatcher: TestMinecraftCoroutineDispatcher) :
CoroutineDispatcher() {
private val threadPool: ExecutorService = Executors.newFixedThreadPool(4)

override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return Thread.currentThread().id == minecraftDispatcher.threadId
}

override fun dispatch(context: CoroutineContext, block: Runnable) {
threadPool.submit {
Thread.currentThread().name = "[TestAsyncThread]"
block.run()
}
}

fun dispose() {
threadPool.shutdown()
}
}
Loading

0 comments on commit 6e24d13

Please sign in to comment.