Skip to content

Commit c1a0609

Browse files
Add the initial implementation on the preprocessor side (#1955)
- Related: #1096
1 parent 6438258 commit c1a0609

File tree

12 files changed

+367
-0
lines changed

12 files changed

+367
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.saveourtool.save.test
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* @property checkId the unique check id.
7+
* @property checkName the human-readable check name.
8+
* @property message the error message (w/o the trailing dot).
9+
*/
10+
@Serializable
11+
data class TestSuiteValidationError(
12+
override val checkId: String,
13+
override val checkName: String,
14+
val message: String,
15+
) : TestSuiteValidationResult() {
16+
override fun toString(): String =
17+
"$checkName: $message."
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.saveourtool.save.test
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* @property checkId the unique check id.
7+
* @property checkName the human-readable check name.
8+
* @property percentage the completion percentage (`0..100`).
9+
*/
10+
@Serializable
11+
data class TestSuiteValidationProgress(
12+
override val checkId: String,
13+
override val checkName: String,
14+
val percentage: Int
15+
) : TestSuiteValidationResult() {
16+
init {
17+
@Suppress("MAGIC_NUMBER")
18+
require(percentage in 0..100) {
19+
"Percentage should be in range of [0..100]: $percentage"
20+
}
21+
}
22+
23+
override fun toString(): String =
24+
when (percentage) {
25+
100 -> "Check $checkName is complete."
26+
else -> "Check $checkName is running, $percentage% complete\u2026"
27+
}
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.saveourtool.save.test
2+
3+
/**
4+
* The validation result — either a progress message (intermediate or
5+
* terminal) or an error message (terminal).
6+
*/
7+
sealed class TestSuiteValidationResult {
8+
/**
9+
* The unique check id.
10+
*/
11+
abstract val checkId: String
12+
13+
/**
14+
* The human-readable check name.
15+
*/
16+
abstract val checkName: String
17+
}

save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import java.nio.file.Path as NioPath
4646
@Service
4747
class TestDiscoveringService(
4848
private val testsPreprocessorToBackendBridge: TestsPreprocessorToBackendBridge,
49+
private val validationService: TestSuiteValidationService,
4950
) {
5051
/**
5152
* @param repositoryPath
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.saveourtool.save.preprocessor.service
2+
3+
import com.saveourtool.save.preprocessor.test.suite.TestSuiteValidator
4+
import com.saveourtool.save.test.TestSuiteValidationError
5+
import com.saveourtool.save.test.TestSuiteValidationResult
6+
import com.saveourtool.save.testsuite.TestSuiteDto
7+
import com.saveourtool.save.utils.getLogger
8+
import org.springframework.jmx.export.annotation.ManagedAttribute
9+
import org.springframework.jmx.export.annotation.ManagedResource
10+
import org.springframework.stereotype.Service
11+
import reactor.core.publisher.Flux
12+
import reactor.core.publisher.ParallelFlux
13+
import reactor.core.scheduler.Schedulers
14+
import java.lang.Runtime.getRuntime
15+
import kotlin.math.min
16+
17+
/**
18+
* Validates test suites discovered by [TestDiscoveringService].
19+
*
20+
* @see TestDiscoveringService
21+
*/
22+
@Service
23+
@ManagedResource
24+
@Suppress("WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES")
25+
class TestSuiteValidationService(private val validators: Array<out TestSuiteValidator>) {
26+
init {
27+
if (validators.isEmpty()) {
28+
logger.warn("No test suite validators configured.")
29+
}
30+
}
31+
32+
@Suppress(
33+
"CUSTOM_GETTERS_SETTERS",
34+
"WRONG_INDENTATION",
35+
)
36+
private val parallelism: Int
37+
get() =
38+
when {
39+
validators.isEmpty() -> 1
40+
else -> min(validators.size, getRuntime().availableProcessors())
41+
}
42+
43+
/**
44+
* @return the class names of discovered validators.
45+
*/
46+
@Suppress(
47+
"CUSTOM_GETTERS_SETTERS",
48+
"WRONG_INDENTATION",
49+
)
50+
@get:ManagedAttribute
51+
val validatorTypes: List<String>
52+
get() =
53+
validators.asSequence()
54+
.map(TestSuiteValidator::javaClass)
55+
.map(Class<TestSuiteValidator>::getName)
56+
.toList()
57+
58+
/**
59+
* Invokes all discovered validators and checks [testSuites].
60+
*
61+
* @param testSuites the test suites to check.
62+
* @return the [Flux] of intermediate status updates terminated with the
63+
* final update for each check discovered.
64+
*/
65+
fun validateAll(testSuites: List<TestSuiteDto>): ParallelFlux<TestSuiteValidationResult> =
66+
when {
67+
testSuites.isEmpty() -> Flux.just<TestSuiteValidationResult>(
68+
TestSuiteValidationError(javaClass.name, "Common", "No test suites found")
69+
).parallel(parallelism)
70+
71+
validators.isEmpty() -> Flux.empty<TestSuiteValidationResult>().parallel(parallelism)
72+
73+
else -> validators.asSequence()
74+
.map { validator ->
75+
validator.validate(testSuites)
76+
}
77+
.reduce { left, right ->
78+
left.mergeWith(right)
79+
}
80+
.parallel(parallelism)
81+
.runOn(Schedulers.parallel())
82+
}
83+
84+
private companion object {
85+
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION")
86+
private val logger = getLogger<TestSuiteValidationService>()
87+
}
88+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.saveourtool.save.preprocessor.test.suite
2+
3+
import com.saveourtool.save.test.TestSuiteValidationResult
4+
import com.saveourtool.save.testsuite.TestSuiteDto
5+
import com.saveourtool.save.utils.getLogger
6+
import reactor.core.publisher.Flux
7+
import reactor.core.scheduler.Schedulers
8+
9+
/**
10+
* The common part of [TestSuiteValidator] implementations.
11+
*/
12+
abstract class AbstractTestSuiteValidator : TestSuiteValidator {
13+
private val logger = getLogger(javaClass)
14+
15+
/**
16+
* Validates test suites.
17+
*
18+
* @param testSuites the test suites to check.
19+
* @param onStatusUpdate the callback to invoke when there's a validation
20+
* status update.
21+
*/
22+
protected abstract fun validate(
23+
testSuites: List<TestSuiteDto>,
24+
onStatusUpdate: (status: TestSuiteValidationResult) -> Unit,
25+
)
26+
27+
final override fun validate(testSuites: List<TestSuiteDto>): Flux<TestSuiteValidationResult> =
28+
Flux
29+
.create { sink ->
30+
validate(testSuites) { status ->
31+
sink.next(status)
32+
}
33+
sink.complete()
34+
}
35+
36+
/*
37+
* Should never be invoked, since this will be a hot Flux.
38+
*/
39+
.doOnCancel {
40+
logger.warn("Validator ${javaClass.simpleName} cancelled.")
41+
}
42+
43+
/*
44+
* Off-load from the main thread.
45+
*/
46+
.subscribeOn(Schedulers.boundedElastic())
47+
48+
/*-
49+
* Turn this cold Flux into a hot one.
50+
*
51+
* `cache()` is identical to `replay(history = Int.MAX_VALUE).autoConnect(minSubscribers = 1)`.
52+
*
53+
* We want `replay()` instead of `publish()`, so that late
54+
* subscribers, if any, will observe early published data.
55+
*/
56+
.cache()
57+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.saveourtool.save.preprocessor.test.suite
2+
3+
import com.saveourtool.save.test.TestSuiteValidationProgress
4+
import com.saveourtool.save.test.TestSuiteValidationResult
5+
import com.saveourtool.save.testsuite.TestSuiteDto
6+
import com.saveourtool.save.utils.getLogger
7+
8+
/**
9+
* Plug-ins without tests.
10+
*/
11+
@TestSuiteValidatorComponent
12+
class PluginsWithoutTests : AbstractTestSuiteValidator() {
13+
override fun validate(
14+
testSuites: List<TestSuiteDto>,
15+
onStatusUpdate: (status: TestSuiteValidationResult) -> Unit,
16+
) {
17+
require(testSuites.isNotEmpty())
18+
19+
@Suppress("MAGIC_NUMBER")
20+
for (i in 0..10) {
21+
val status = TestSuiteValidationProgress(javaClass.name, CHECK_NAME, i * 10)
22+
logger.info("Emitting \"$status\"...")
23+
onStatusUpdate(status)
24+
Thread.sleep(500L)
25+
}
26+
}
27+
28+
private companion object {
29+
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION")
30+
private val logger = getLogger<PluginsWithoutTests>()
31+
private const val CHECK_NAME = "Searching for plug-ins with zero tests"
32+
}
33+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.saveourtool.save.preprocessor.test.suite
2+
3+
import com.saveourtool.save.test.TestSuiteValidationResult
4+
import com.saveourtool.save.testsuite.TestSuiteDto
5+
import reactor.core.publisher.Flux
6+
import reactor.core.scheduler.Scheduler
7+
import reactor.core.scheduler.Schedulers
8+
9+
/**
10+
* A particular validation check.
11+
*
12+
* Implementations _should_:
13+
* - make sure the [Flux] returned by [validate] is a hot [Flux], so that
14+
* cancelling a particular subscriber (e.g.: in case of a network outage)
15+
* doesn't affect validation;
16+
* - off-load the actual work to a separate [Scheduler], such as
17+
* [Schedulers.boundedElastic].
18+
* - be annotated with [TestSuiteValidatorComponent].
19+
*
20+
* Implementations _may_:
21+
* - inherit from [AbstractTestSuiteValidator].
22+
*
23+
* @see TestSuiteValidatorComponent
24+
* @see AbstractTestSuiteValidator
25+
*/
26+
fun interface TestSuiteValidator {
27+
/**
28+
* Validates test suites, returning a [Flux] of intermediate status updates
29+
* terminated with the final update.
30+
*
31+
* @param testSuites the test suites to check.
32+
* @return the [Flux] of intermediate status updates terminated with the
33+
* final update.
34+
*/
35+
fun validate(testSuites: List<TestSuiteDto>): Flux<TestSuiteValidationResult>
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.saveourtool.save.preprocessor.test.suite
2+
3+
import org.springframework.stereotype.Component
4+
5+
/**
6+
* Can be used to annotate implementations of [TestSuiteValidator] so that
7+
* they're discoverable by _Spring_.
8+
*
9+
* @see TestSuiteValidator
10+
*/
11+
@Component
12+
annotation class TestSuiteValidatorComponent
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.saveourtool.save.preprocessor.test.suite
2+
3+
import com.saveourtool.save.test.TestSuiteValidationProgress
4+
import com.saveourtool.save.test.TestSuiteValidationResult
5+
import com.saveourtool.save.testsuite.TestSuiteDto
6+
import com.saveourtool.save.utils.getLogger
7+
8+
/**
9+
* Test suites with wildcard mode.
10+
*/
11+
@TestSuiteValidatorComponent
12+
class TestSuitesWithWildcardMode : AbstractTestSuiteValidator() {
13+
override fun validate(
14+
testSuites: List<TestSuiteDto>,
15+
onStatusUpdate: (status: TestSuiteValidationResult) -> Unit,
16+
) {
17+
require(testSuites.isNotEmpty())
18+
19+
@Suppress("MAGIC_NUMBER")
20+
for (i in 0..10) {
21+
val status = TestSuiteValidationProgress(javaClass.name, CHECK_NAME, i * 10)
22+
logger.info("Emitting \"$status\"...")
23+
onStatusUpdate(status)
24+
Thread.sleep(500L)
25+
}
26+
}
27+
28+
private companion object {
29+
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION")
30+
private val logger = getLogger<TestSuitesWithWildcardMode>()
31+
private const val CHECK_NAME = "Searching for test suites with wildcard mode"
32+
}
33+
}

0 commit comments

Comments
 (0)