This commit is contained in:
parent
fa8052d053
commit
eae3aecb31
|
@ -50,7 +50,7 @@ dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||||
|
|
||||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
testImplementation("io.mockk:mockk:1.12.0")
|
testImplementation("io.mockk:mockk:1.12.1")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||||
testImplementation("org.springframework:spring-test:5.3.13")
|
testImplementation("org.springframework:spring-test:5.3.13")
|
||||||
|
@ -68,8 +68,8 @@ springBoot {
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@ -83,15 +83,18 @@ sourceSets {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
jvmArgs("-XX:+ShowCodeDetailsInExceptionMessages")
|
||||||
|
testLogging {
|
||||||
|
events("skipped", "failed")
|
||||||
|
setExceptionFormat("full")
|
||||||
|
}
|
||||||
|
|
||||||
reports {
|
reports {
|
||||||
junitXml.required.set(true)
|
junitXml.required.set(true)
|
||||||
html.required.set(false)
|
html.required.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
useJUnitPlatform()
|
|
||||||
testLogging {
|
|
||||||
events("skipped", "failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile>() {
|
tasks.withType<JavaCompile>() {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
|
|
||||||
|
class FileExistCache {
|
||||||
|
private val map = hashMapOf<FilePath, Boolean>()
|
||||||
|
|
||||||
|
operator fun contains(path: FilePath) =
|
||||||
|
path in map
|
||||||
|
|
||||||
|
fun exists(path: FilePath) =
|
||||||
|
map[path] ?: false
|
||||||
|
|
||||||
|
fun set(path: FilePath, exists: Boolean) {
|
||||||
|
map[path] = exists
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setExists(path: FilePath) =
|
||||||
|
set(path, true)
|
||||||
|
|
||||||
|
fun setDoesNotExists(path: FilePath) =
|
||||||
|
set(path, false)
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.WrappedFile
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.withFileAt
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.springframework.core.io.ByteArrayResource
|
import org.springframework.core.io.ByteArrayResource
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
|
@ -49,8 +52,17 @@ class FileServiceImpl(
|
||||||
private val creProperties: CreProperties,
|
private val creProperties: CreProperties,
|
||||||
private val logger: Logger
|
private val logger: Logger
|
||||||
) : WriteableFileService {
|
) : WriteableFileService {
|
||||||
override fun exists(path: String) = withFileAt(path.fullPath()) {
|
private val existsCache = FileExistCache()
|
||||||
this.exists() && this.isFile
|
|
||||||
|
override fun exists(path: String): Boolean {
|
||||||
|
val fullPath = path.fullPath()
|
||||||
|
return if (fullPath in existsCache) {
|
||||||
|
existsCache.exists(fullPath)
|
||||||
|
} else {
|
||||||
|
withFileAt(fullPath) {
|
||||||
|
this.exists() && this.isFile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(path: String) = ByteArrayResource(
|
override fun read(path: String) = ByteArrayResource(
|
||||||
|
@ -70,6 +82,7 @@ class FileServiceImpl(
|
||||||
try {
|
try {
|
||||||
withFileAt(fullPath) {
|
withFileAt(fullPath) {
|
||||||
this.create()
|
this.create()
|
||||||
|
existsCache.setExists(fullPath)
|
||||||
}
|
}
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
FileCreateException(path).logAndThrow(ex, logger)
|
FileCreateException(path).logAndThrow(ex, logger)
|
||||||
|
@ -89,9 +102,12 @@ class FileServiceImpl(
|
||||||
|
|
||||||
override fun delete(path: String) {
|
override fun delete(path: String) {
|
||||||
try {
|
try {
|
||||||
withFileAt(path.fullPath()) {
|
val fullPath = path.fullPath()
|
||||||
|
withFileAt(fullPath) {
|
||||||
if (!exists(path)) throw FileNotFoundException(path)
|
if (!exists(path)) throw FileNotFoundException(path)
|
||||||
!this.delete()
|
|
||||||
|
this.delete()
|
||||||
|
existsCache.setDoesNotExists(fullPath)
|
||||||
}
|
}
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
FileDeleteException(path).logAndThrow(ex, logger)
|
FileDeleteException(path).logAndThrow(ex, logger)
|
||||||
|
@ -106,7 +122,7 @@ class FileServiceImpl(
|
||||||
return FilePath("${creProperties.dataDirectory}/$this")
|
return FilePath("${creProperties.dataDirectory}/$this")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) {
|
private fun prepareWrite(path: String, overwrite: Boolean, op: WrappedFile.() -> Unit) {
|
||||||
val fullPath = path.fullPath()
|
val fullPath = path.fullPath()
|
||||||
|
|
||||||
if (exists(path)) {
|
if (exists(path)) {
|
||||||
|
@ -123,15 +139,6 @@ class FileServiceImpl(
|
||||||
FileWriteException(path).logAndThrow(ex, logger)
|
FileWriteException(path).logAndThrow(ex, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
|
||||||
private fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
|
||||||
fullPath.file.block()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FilePath(val path: String) {
|
|
||||||
val file: File
|
|
||||||
get() = File(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shortcut to create a file and its parent directories. */
|
/** Shortcut to create a file and its parent directories. */
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.files
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
import org.springframework.core.io.ResourceLoader
|
import org.springframework.core.io.ResourceLoader
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.utils
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.service.files.create
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
/** Mockable file wrapper, to prevent issues when mocking [java.io.File]. */
|
||||||
|
class WrappedFile(val file: File) {
|
||||||
|
val isFile: Boolean
|
||||||
|
get() = file.isFile
|
||||||
|
|
||||||
|
fun toPath(): Path =
|
||||||
|
file.toPath()
|
||||||
|
|
||||||
|
fun exists() =
|
||||||
|
file.exists()
|
||||||
|
|
||||||
|
fun readBytes() =
|
||||||
|
file.readBytes()
|
||||||
|
|
||||||
|
fun writeBytes(array: ByteArray) =
|
||||||
|
file.writeBytes(array)
|
||||||
|
|
||||||
|
fun create() =
|
||||||
|
file.create()
|
||||||
|
|
||||||
|
fun delete(): Boolean =
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(path: String) =
|
||||||
|
WrappedFile(File(path))
|
||||||
|
|
||||||
|
fun from(path: FilePath) =
|
||||||
|
from(path.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class FilePath(val path: String)
|
||||||
|
|
||||||
|
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||||
|
fun <T> withFileAt(fullPath: FilePath, block: WrappedFile.() -> T) =
|
||||||
|
WrappedFile.from(fullPath).block()
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.files
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.WrappedFile
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
@ -24,20 +25,18 @@ private class FileServiceTestContext {
|
||||||
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
||||||
every { error(any(), any<Exception>()) } just Runs
|
every { error(any(), any<Exception>()) } just Runs
|
||||||
}))
|
}))
|
||||||
val mockFile = mockk<File> {
|
val mockFile = mockk<WrappedFile> {
|
||||||
every { path } returns mockFilePath
|
every { file } returns mockk()
|
||||||
every { exists() } returns true
|
every { exists() } returns true
|
||||||
every { isFile } returns true
|
every { isFile } returns true
|
||||||
every { toPath() } returns mockFilePathPath
|
every { toPath() } returns mockFilePathPath
|
||||||
}
|
}
|
||||||
val mockFileFullPath = spyk(FilePath("${creProperties.dataDirectory}/$mockFilePath")) {
|
|
||||||
every { file } returns mockFile
|
|
||||||
|
|
||||||
with(fileService) {
|
|
||||||
every { mockFilePath.fullPath() } returns this@spyk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||||
|
|
||||||
|
init {
|
||||||
|
mockkObject(WrappedFile.Companion)
|
||||||
|
every { WrappedFile.from(any<String>()) } returns mockFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileServiceTest {
|
class FileServiceTest {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.files
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
Loading…
Reference in New Issue