Compare commits

...

4 commits
v0.2.0 ... dev

Author SHA1 Message Date
Augusto Dwenger dfcd3cd35a Merge branch '7-feature-ranking-command' into 'dev'
Resolve "[FEATURE] "ranking" command"

Closes #7

See merge request hamburghammer/playtime!14
2020-02-08 22:00:37 +00:00
Augusto Dwenger ad2afd85d5 Resolve "[FEATURE] "ranking" command" 2020-02-08 22:00:37 +00:00
Augusto Dwenger e8fff7b200 implement coverage report with jacoco 2020-02-08 21:54:28 +00:00
Augusto Dwenger 0895514469 Implements the getTopPlayer method to get all player sorted by there playtime 2020-02-06 19:49:15 +00:00
14 changed files with 333 additions and 10 deletions

View file

@ -32,10 +32,16 @@ unit-test:
artifacts:
expire_in: 3 days
paths:
- build/reports/tests/test/index.html
- build/reports/tests/test/
reports:
junit: build/test-results/test/TEST-*.xml
coverage:
stage: analyse
script:
- "./gradlew $GRADLE_OPTS clean jacocoTestReport"
- "cat build/reports/jacoco/test/html/index.html"
lint:
stage: analyse
script:

View file

@ -1,4 +1,6 @@
# PlayTime
[![pipeline status](https://gitlab.com/hamburghammer/playtime/badges/dev/pipeline.svg)](https://gitlab.com/hamburghammer/playtime/-/commits/dev)
[![coverage report](https://gitlab.com/hamburghammer/playtime/badges/dev/coverage.svg)](https://gitlab.com/hamburghammer/playtime/-/commits/dev)
It's a plugin for a [Spigot](https://www.spigotmc.org) Minecraft server to log the time players spend on the server and returns it to them.
@ -17,9 +19,14 @@ It's a plugin for a [Spigot](https://www.spigotmc.org) Minecraft server to log t
`playtime` shows the total play time on the server (Format DD:HH:MM)
`playtimeof <player>` shows the total play time of the player (Format DD:HH:MM)
`toptime` shows the top 5 player with the highest total playtime
## Development
The plugin gets build with gradle. It is important to run the shadowJar task to include all dependencies.
It's entirely written in Kotlin and for testing it uses [Mockk](https://mockk.io/) and [Junit5](https://junit.org/junit5/).
This plugin uses the [linter from Pinterest for Kotlin](https://www.kotlinresources.com/library/ktlint/)

View file

@ -3,6 +3,7 @@ plugins {
id("org.jlleitschuh.gradle.ktlint") version "9.0.0"
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jetbrains.dokka") version "0.10.0"
jacoco
}
group = "de.augustodwenger"
@ -45,11 +46,23 @@ tasks {
test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
events("skipped", "failed")
// events("passed", "skipped", "failed")
}
extensions.configure(JacocoTaskExtension::class) {
classDumpDir = file("$buildDir/jacoco/classpathdumps")
}
}
dokka {
outputFormat = "html"
outputDirectory = "$buildDir/dokka"
}
jacocoTestReport {
reports {
xml.isEnabled = false
csv.isEnabled = false
html.isEnabled = true
}
dependsOn("test")
}
}

View file

@ -1,6 +1,7 @@
import commands.PlayTime
import commands.PlayTimeOfCommand
import commands.TestCommand
import commands.TopTime
import commands.UpTime
import listener.JoinListener
import listener.QuitListener
@ -31,6 +32,7 @@ class Main : JavaPlugin() {
this.getCommand("playtime")?.setExecutor(PlayTime())
this.getCommand("uptime")?.setExecutor(UpTime())
this.getCommand("playtimeof")?.setExecutor(PlayTimeOfCommand())
this.getCommand("toptime")?.setExecutor(TopTime())
server.pluginManager.registerEvents(JoinListener(), this)
server.pluginManager.registerEvents(QuitListener(), this)

View file

@ -1,5 +1,6 @@
import java.time.Duration
import java.util.UUID
import models.Player
interface PlayerTime {
/**
@ -28,6 +29,19 @@ interface PlayerTime {
*/
fun timePlayed(uuid: UUID): Duration
/**
* Search for the players with the highest playtime
*
* @return List<Player>
*/
fun getTopPlayers(): List<Player>
/**
* Formats the duration to a simple String
*
* @param duration Duration
* @return String (DD:HH:MM)
*/
fun timeToString(duration: Duration): String {
val days: String =
if (duration.toDaysPart() < 10) """0${duration.toDaysPart()}""" else """${duration.toDaysPart()}"""

View file

@ -13,7 +13,7 @@ import models.Player
class SimplePlayerTime(private val db: PlayerTimeDB) : PlayerTime {
/**
* Adds the player to the playerMap.
* Adds the player to the [db].
* It creates an new player if it's no persisted and adds/refreshes the join time
*
* @param uuid the actual uuid of a player
@ -34,20 +34,18 @@ class SimplePlayerTime(private val db: PlayerTimeDB) : PlayerTime {
}
/**
* Updates the playtime and saves it
* Updates the playtime and saves it from the player with the [uuid]
*
* @param uuid UUID
*/
override fun updatePlayTime(uuid: UUID) {
val player = db.findById(uuid)
val playTime = timePlayed(uuid)
player.playTime = playTime
player.playTime = calculatePlaytime(player)
db.save(player)
}
/**
* Returns the playtime including the old time
* if the stats got save it should use the save time instead of the join time
* Returns the playtime of the player with the the [uuid]
*
* @param uuid player uuid
*
@ -55,6 +53,27 @@ class SimplePlayerTime(private val db: PlayerTimeDB) : PlayerTime {
*/
override fun timePlayed(uuid: UUID): Duration {
val player = db.findById(uuid)
return calculatePlaytime(player)
}
/**
* Search for the players with the highest playtime
*
* @return List<Player> in a descending order by playtime
*/
override fun getTopPlayers(): List<Player> {
val playerList: List<Player> = db.findAll().onEach { player -> player.playTime = calculatePlaytime(player) }
return playerList.sortedByDescending { player -> player.playTime }
}
/**
* Calculates the time based on the join and on the last save time of the player
* if the playtime got saved it should use the saved time instead of the join time
*
* @param player Player
* @return Duration the playtime of the [player]
*/
private fun calculatePlaytime(player: Player): Duration {
val playTime: Duration =
if ((player.lastSave != null) && Duration.between(player.lastSave, player.joinTime).isNegative) {
Duration.between(player.lastSave, LocalDateTime.now())

View file

@ -0,0 +1,28 @@
package commands
import PlayerTime
import PlayerTimeService
import java.util.stream.Collectors
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
/**
* Command to get the top players with the most playtime
*
* @property playerTime PlayerTime
* @property playerListSize Long default = 5
* @constructor
*/
class TopTime(private val playerTime: PlayerTime = PlayerTimeService, private val playerListSize: Long = 5) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
val playerTopList = playerTime.getTopPlayers().stream().limit(playerListSize).collect(Collectors.toList()).toList()
val sb = StringBuilder().appendln("Top list")
playerTopList.forEach { sb.appendln("${it.playerName}: ${playerTime.timeToString(it.playTime)}") }
sender.sendMessage(sb.toString())
return true
}
}

View file

@ -1,10 +1,12 @@
package db
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import java.io.File
import java.time.LocalDateTime
import java.util.UUID
import models.Player
import mu.KotlinLogging
/**
* FileDB manages the file based PlayerTime DB and provides basic functionality to interact with it
@ -14,6 +16,8 @@ import models.Player
*/
class FilePlayerTimeDB(private val path: File = File("./plugins/PlayTime/")) : PlayerTimeDB {
val logger = KotlinLogging.logger {}
private val gson = Gson()
/**
@ -100,6 +104,24 @@ class FilePlayerTimeDB(private val path: File = File("./plugins/PlayTime/")) : P
throw PlayerNotFoundException("""The player with the name $name was not found""")
}
/**
* Should read all player and return theme
*
* @return List<Player>
*/
override fun findAll(): List<Player> {
val playerList = mutableListOf<Player>()
val fileList = path.listFiles() ?: return emptyList()
fileList.forEach { file: File ->
try {
playerList.add(gson.fromJson(file.readText(), Player::class.java))
} catch (e: JsonSyntaxException) {
logger.error { "The player with the ID ${{ file.name }} could not be read out of the files!" }
}
}
return playerList.toList()
}
/**
* Writs a player into an existing file
* To create a new player file @see createPlayer(player: Player)

View file

@ -45,4 +45,11 @@ interface PlayerFinder {
* @throws PlayerNotFoundException
*/
fun findByName(name: String): Player
/**
* Should read all player and return theme
*
* @return List<Player>
*/
fun findAll(): List<Player>
}

View file

@ -16,4 +16,7 @@ commands:
description: "Shows the up time of the server"
playtimeof:
usage: /<command> <player>
description: "Shows the playtime of the player"
description: "Shows the playtime of the player"
toptime:
usage: /<command>
description: "Shows top players with the highest playtime"

View file

@ -1,6 +1,7 @@
import java.time.Duration
import java.util.UUID
import models.Player
class FakePlayerTime : PlayerTime {
@ -15,4 +16,8 @@ class FakePlayerTime : PlayerTime {
override fun timePlayed(uuid: UUID): Duration {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun getTopPlayers(): List<Player> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
}

View file

@ -0,0 +1,105 @@
package commands
import SimplePlayerTime
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
import java.time.Duration
import java.util.UUID
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import models.Player
import org.bukkit.command.CommandSender
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertTrue
class TopTimeTest {
@BeforeTest
fun setUp() {
unmockkAll()
}
@Test
fun `send string`() {
val captureList = mutableListOf<String>()
val uuid1 = UUID.randomUUID()
val uuid2 = UUID.randomUUID()
val uuid3 = UUID.randomUUID()
val player1 = Player(uuid1, "player1", playTime = Duration.ZERO)
val player2 = Player(uuid2, "player2", playTime = Duration.ofMinutes(10))
val player3 = Player(uuid3, "player3", playTime = Duration.ofHours(2))
val mockPlayTime = mockk<SimplePlayerTime>()
every { mockPlayTime.getTopPlayers() } returns listOf(player3, player2, player1)
every { mockPlayTime.timeToString(any()) } returns "00:00:00"
val topTime = TopTime(mockPlayTime)
val mockCommandSender = mockk<CommandSender>()
every { mockCommandSender.sendMessage(capture(captureList)) } just Runs
assertTrue(topTime.onCommand(mockCommandSender, mockk(), "", arrayOf()))
assertTrue(captureList.isNotEmpty())
assertNotEquals("", captureList.first())
}
@Test
fun `correct formatted string output`() {
val captureList = mutableListOf<String>()
val uuid1 = UUID.randomUUID()
val uuid2 = UUID.randomUUID()
val uuid3 = UUID.randomUUID()
val player1 = Player(uuid1, "player1", playTime = Duration.ZERO)
val player2 = Player(uuid2, "player2", playTime = Duration.ofMinutes(10))
val player3 = Player(uuid3, "player3", playTime = Duration.ofHours(2))
val mockPlayTime = mockk<SimplePlayerTime>()
every { mockPlayTime.getTopPlayers() } returns listOf(player3, player2, player1)
every { mockPlayTime.timeToString(any()) } returns "00:00:00"
val mockCommandSender = mockk<CommandSender>()
every { mockCommandSender.sendMessage(capture(captureList)) } just Runs
val topTime = TopTime(mockPlayTime)
assertTrue(topTime.onCommand(mockCommandSender, mockk(), "", arrayOf()))
assertEquals(
"""Top list
player3: 00:00:00
player2: 00:00:00
player1: 00:00:00
""", captureList.first().toString()
)
}
@Test
fun `show only top 6 player`() {
val playerListSize = 6
val defaultNewLines = 2
val captureList = mutableListOf<String>()
val playerList = mutableListOf<Player>()
for (i in 1..7) {
playerList.add(Player(UUID.randomUUID(), "$i", playTime = Duration.ZERO))
}
val mockPlayTime = mockk<SimplePlayerTime>()
every { mockPlayTime.getTopPlayers() } returns playerList.toList()
every { mockPlayTime.timeToString(any()) } returns "00:00:00"
val mockCommandSender = mockk<CommandSender>()
every { mockCommandSender.sendMessage(capture(captureList)) } just Runs
val topTime = TopTime(mockPlayTime, playerListSize.toLong())
assertTrue(topTime.onCommand(mockCommandSender, mockk(), "", arrayOf()))
assertEquals(playerListSize + defaultNewLines, captureList.first().split("\n").count())
}
}

View file

@ -156,4 +156,40 @@ class FilePlayerTimeDBTest {
assertFalse(fileDB.existsByName("foo"))
}
}
@Test
fun `find all players`() {
val playerName = "playerName"
val fileDB = FilePlayerTimeDB(dbDir)
val uuid = UUID.randomUUID()
val uuid2 = UUID.randomUUID()
val player = Player(uuid, playerName)
val player2 = Player(uuid2, playerName)
File(dbDir, uuid.toString()).writeText(Gson().toJson(player))
File(dbDir, uuid2.toString()).writeText(Gson().toJson(player2))
val playerList = fileDB.findAll()
assertFalse(playerList.isEmpty())
assertEquals(2, playerList.size)
assertTrue(playerList.contains(player))
assertTrue(playerList.contains(player2))
}
@Test
fun `find all players if there is no one`() {
val fileDB = FilePlayerTimeDB(dbDir)
assertTrue(fileDB.findAll().isEmpty())
}
@Test
fun `error by deserialization`() {
val fileDB = FilePlayerTimeDB(dbDir)
val uuid = UUID.randomUUID()
File(dbDir, uuid.toString()).writeText("Foo")
assertTrue(fileDB.findAll().isEmpty())
}
}

View file

@ -11,13 +11,13 @@ import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import models.Player
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
class SimplePlayerTimeTest {
@ -113,6 +113,24 @@ class SimplePlayerTimeTest {
}
}
@Test
fun `update the play time and save`() {
val capturedPlayers = mutableListOf<Player>()
val uuid = UUID.randomUUID()
val player = Player(uuid, "", playTime = Duration.ZERO)
val mockDBPlayerTimeDB = mockk<PlayerTimeDB>()
every { mockDBPlayerTimeDB.findById(uuid) } returns player
every { mockDBPlayerTimeDB.save(capture(capturedPlayers)) } just Runs
val simplePlayerTime = SimplePlayerTime(mockDBPlayerTimeDB)
simplePlayerTime.updatePlayTime(uuid)
assertEquals(1, capturedPlayers.size)
assertNotEquals(Duration.ZERO, capturedPlayers.first().playTime)
}
@Nested
inner class TimePlayedTest {
@ -184,4 +202,42 @@ class SimplePlayerTimeTest {
println(playTime.toMinutes())
}
}
@Nested
inner class TopPlayer {
@Test
fun `get list with players`() {
val player1 = Player(UUID.randomUUID(), "player1", playTime = Duration.ZERO)
val player2 = Player(UUID.randomUUID(), "player2", playTime = Duration.ofMinutes(10))
val mockPlayerTimeDB = mockk<PlayerTimeDB>()
every { mockPlayerTimeDB.findAll() } returns listOf(player1, player2)
val playerService = SimplePlayerTime(mockPlayerTimeDB)
val topPlayer = playerService.getTopPlayers()
assertFalse(topPlayer.isEmpty())
assertEquals(2, topPlayer.size)
}
@Test
fun `get list with players sorted by most time`() {
val player1 = Player(UUID.randomUUID(), "player1", playTime = Duration.ZERO)
val player2 = Player(UUID.randomUUID(), "player2", playTime = Duration.ofMinutes(10))
val player3 = Player(UUID.randomUUID(), "player3", playTime = Duration.ofHours(2))
val mockPlayerTimeDB = mockk<PlayerTimeDB>()
every { mockPlayerTimeDB.findAll() } returns listOf(player1, player2, player3)
val playerService = SimplePlayerTime(mockPlayerTimeDB)
val topPlayer = playerService.getTopPlayers()
assertEquals(player3, topPlayer[0])
assertEquals(player2, topPlayer[1])
assertEquals(player1, topPlayer[2])
}
}
}