Skip to content

Commit 1e2c5ce

Browse files
authored
Merge pull request tegonal#260 from tegonal/feature/deadlines
introduce deadlines for fixed values
2 parents d95ea40 + edd29bc commit 1e2c5ce

6 files changed

Lines changed: 265 additions & 96 deletions

File tree

README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ version: [README of v2.0.0-RC-2](https://github.com/tegonal/minimalist/tree/v2.0
5555
- [Configuration](#configuration)
5656
- [Profiles and Envs](#profiles-and-envs)
5757
- [Fixing the seed](#fixing-the-seed)
58+
- [Error Deadlines](#errordeadlines)
5859
- [Change the ArgsRangeDecider](#change-the-argsrangedecider)
5960
- [Use a SuffixArgsGeneratorDecider](#use-a-suffixargsgeneratordecider)
6061
- [Helpers](#helpers)
@@ -602,7 +603,7 @@ for.
602603
603604
### flatZipDependent
604605
605-
If you want to zip not only one value but multiple values from the `ArbArgsGenerator` which was created based on a
606+
If you want to zip not only one value but multiple values from the `ArbArgsGenerator` which was created based on a
606607
value of your `ArbArgsGenerator`/`SemiOrderedArbArgsGenerator`, then you can use flatZipDependent:
607608
608609
<code-flat-zip-dependent-arb>
@@ -620,8 +621,8 @@ ordered.intFromTo(1, 10).flatZipDependent(amount = 3) { a ->
620621
</code-flat-zip-dependent-arb>
621622
622623
`OrderedArgsGenerator` provides a `flatZipDependentMaterialised` which expects a factory that creates another
623-
`OrderedArgsGenerator` based on a given value from the first `OrderedArgsGenerator` and in contrast to
624-
`flatZipDependent` does not take an `amount` but the individual lengths of the created `OrderedArgsGenerator`s.
624+
`OrderedArgsGenerator` based on a given value from the first `OrderedArgsGenerator` and in contrast to
625+
`flatZipDependent` does not take an `amount` but the individual lengths of the created `OrderedArgsGenerator`s.
625626
Following an example:
626627
627628
<code-flat-zip-dependent-ordered-ordered>
@@ -836,10 +837,9 @@ class DynamicTest : PredefinedArgsProviders {
836837
837838
Note however, that all the magic of `ArgsSource` is not available (yet). Which means:
838839
839-
- you need to combine ArgsGenerators manually (see [arb.zip](#arb-zip) and [ordered.cartesian](#ordered-cartesian)) or
840+
- you need to combine ArgsGenerators manually (see [zip](#zip) and [ordered.cartesian](#ordered-cartesian)) or
840841
use [combineAll](#generic-combine) if you deal with generators in `Tuple`s -- the good side, you do not lose the types
841-
as you would
842-
with JUnit's `Arguments`.
842+
as you would with JUnit's `Arguments`.
843843
- A defined [SuffixArgsGenerator](#use-a-suffixargsgeneratordecider) is ignored (we would lose the types again)
844844
- definitions like `@ArgSourceOptions` are ignored, but as long as you use `generateAndTakeBasedOnDecider` the defined
845845
seed and co. (see [fixing the seed](#fixing-the-seed) are taken into account
@@ -895,6 +895,29 @@ Minimalist outputs the used seed once the config is fully loaded. Use it in `min
895895
seed to e.g. a previous run. You might want to restrict `maxArgs` in such a case as well and use `skip`
896896
to skip some runs, i.e. jump to a particular run.
897897
898+
### ErrorDeadlines
899+
900+
If you fix one of the following properties, then an error deadline is added to your `minimalist.local.properties`:
901+
902+
- seed
903+
- skip
904+
- maxArgs
905+
- requestedMinArgs
906+
907+
The deadline will remind you that you should remove (comment out) those values again, as they are intended for debugging
908+
or when you temporarily want to execute more tests than defined by your [activeEnv](#profiles-and-envs).
909+
910+
You can adjust the default deadline (60 minutes) via `remindAboutFixedPropertiesAfterMinutes`.
911+
912+
Minimalist assumes your `minimalist.local.properties` is located under `./src/test/resources` relative to the
913+
the directory from which you execute java -- if you run tests in IntelliJ this corresponds to the project dir.
914+
If you place it under a different directory, then use the property `minimalistPropertiesDir` to adjust it (e.g. in the
915+
`minimalist.properties`-file or directly in `minimalist.local.properties`). Following an example:
916+
917+
```properties
918+
minimalistPropertiesDir=./src/jvmTest/resources
919+
```
920+
898921
## Change the ArgsRangeDecider
899922
900923
An `ArgsRangeDecider` is responsible to decide from which offset and how many arguments shall be taken from an

src/main/kotlin/com/tegonal/minimalist/config/MinimalistConfig.kt

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import com.tegonal.minimalist.providers.impl.ProfileBasedArgsRangeDecider
99
import com.tegonal.minimalist.providers.impl.SuffixArgsGeneratorNeverDecider
1010
import com.tegonal.minimalist.utils.impl.checkIsNotBlank
1111
import com.tegonal.minimalist.utils.impl.checkIsPositive
12-
import com.tegonal.minimalist.utils.impl.failIfNegative
1312
import com.tegonal.minimalist.utils.seedToOffset
1413
import kotlin.random.Random
1514

@@ -159,6 +158,7 @@ class MinimalistConfig(
159158
),
160159
),
161160
) {
161+
162162
init {
163163
skip?.also { checkIsPositive(it, "skip") }
164164
requestedMinArgs?.also { checkIsPositive(it, "requestedMinArgs") }
@@ -181,17 +181,19 @@ class MinimalistConfig(
181181
}
182182

183183
fun copy(configure: MinimalistConfigBuilder.() -> Unit): MinimalistConfig =
184-
MinimalistConfigBuilder(
185-
seed = seed.value,
186-
skip = skip,
187-
maxArgs = maxArgs,
188-
requestedMinArgs = requestedMinArgs,
189-
activeArgsRangeDecider = activeArgsRangeDecider,
190-
activeSuffixArgsGeneratorDecider = activeSuffixArgsGeneratorDecider,
191-
activeEnv = activeEnv,
192-
defaultProfile = defaultProfile,
193-
testProfiles = testProfiles.toHashMap()
194-
).apply(configure).build()
184+
toBuilder().apply(configure).build()
185+
186+
fun toBuilder(): MinimalistConfigBuilder = MinimalistConfigBuilder(
187+
seed = seed.value,
188+
skip = skip,
189+
maxArgs = maxArgs,
190+
requestedMinArgs = requestedMinArgs,
191+
activeArgsRangeDecider = activeArgsRangeDecider,
192+
activeSuffixArgsGeneratorDecider = activeSuffixArgsGeneratorDecider,
193+
activeEnv = activeEnv,
194+
defaultProfile = defaultProfile,
195+
testProfiles = testProfiles.toHashMap()
196+
)
195197
}
196198

197199
/**
@@ -206,8 +208,9 @@ class MinimalistConfigBuilder(
206208
var activeSuffixArgsGeneratorDecider: String,
207209
var activeEnv: String,
208210
var defaultProfile: String,
209-
var testProfiles: HashMap<String, HashMap<String, TestConfig>>
211+
var testProfiles: HashMap<String, HashMap<String, TestConfig>>,
210212
) {
213+
211214
fun build(): MinimalistConfig = MinimalistConfig(
212215
seed = Seed(seed),
213216
skip = skip,
@@ -217,7 +220,7 @@ class MinimalistConfigBuilder(
217220
activeSuffixArgsGeneratorDecider = activeSuffixArgsGeneratorDecider,
218221
activeEnv = activeEnv,
219222
defaultProfile = defaultProfile,
220-
testProfiles = TestProfiles.create(testProfiles)
223+
testProfiles = TestProfiles.create(testProfiles),
221224
)
222225
}
223226

src/main/kotlin/com/tegonal/minimalist/config/impl/MinimalistConfigViaPropertiesLoader.kt

Lines changed: 153 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@ import ch.tutteli.kbox.blankToNull
44
import ch.tutteli.kbox.takeIf
55
import com.tegonal.minimalist.config.Env
66
import com.tegonal.minimalist.config.MinimalistConfig
7+
import com.tegonal.minimalist.config.MinimalistConfigBuilder
8+
import com.tegonal.minimalist.config.impl.MinimalistPropertiesParser.Companion.ERROR_DEADLINES_PREFIX
9+
import com.tegonal.minimalist.utils.impl.checkIsPositive
10+
import java.nio.file.Path
11+
import java.nio.file.Paths
12+
import java.time.LocalDateTime
13+
import java.time.format.DateTimeFormatter
714
import java.util.*
15+
import kotlin.io.path.appendText
16+
import kotlin.io.path.readText
17+
import kotlin.io.path.writeText
818

919
/**
1020
* !! No backward compatibility guarantees !!
@@ -13,72 +23,151 @@ import java.util.*
1323
* @since 2.0.0
1424
*/
1525
class MinimalistConfigViaPropertiesLoader {
16-
val config by lazy {
26+
val config: MinimalistConfig by lazy {
1727
val parser = MinimalistPropertiesParser()
1828
val initialConfig = MinimalistConfig()
19-
initialConfig
20-
.run {
21-
mergeWithPropertiesInResource("/minimalist.properties", parser).also {
22-
check(it.seed == initialConfig.seed) {
23-
errorMessageNotAllowedToModify("seed")
24-
}
25-
check(it.skip == initialConfig.skip) {
26-
errorMessageNotAllowedToModify("skip")
27-
}
28-
check(it.requestedMinArgs == initialConfig.requestedMinArgs) {
29-
errorMessageNotAllowedToModify("requestedMinArgs")
30-
}
31-
check(it.maxArgs == initialConfig.maxArgs) {
32-
errorMessageNotAllowedToModify("maxArgs")
33-
}
29+
val configFileSpecifics = ConfigFileSpecifics()
30+
initialConfig.toBuilder()
31+
.apply {
32+
setByPropertiesInResource("/minimalist.properties", configFileSpecifics, parser)
33+
check(seed == initialConfig.seed.value) {
34+
errorMessageNotAllowedToModify("seed")
35+
}
36+
check(skip == initialConfig.skip) {
37+
errorMessageNotAllowedToModify("skip")
38+
}
39+
check(requestedMinArgs == initialConfig.requestedMinArgs) {
40+
errorMessageNotAllowedToModify("requestedMinArgs")
41+
}
42+
check(maxArgs == initialConfig.maxArgs) {
43+
errorMessageNotAllowedToModify("maxArgs")
3444
}
3545
}
36-
.run { mergeWithEnv() }
37-
.run { mergeWithPropertiesInResource("/minimalist.local.properties", parser) }
38-
.also {
39-
val fixedSeed = it.seed != initialConfig.seed
40-
println("Minimalist${if (fixedSeed) " fixed" else ""} seed ${it.seed} in env ${it.activeEnv} ")
41-
//TODO 2.1.0 add seedFixedAt to MinimalistConfig and write it to minimalist.local.properties in
42-
// case the seed is fixed and error after x hours, minutes or whatever (could be configurable)
43-
// so that we warn a user if he forgot to remove it again - a user could set seedFixedAt manually to
44-
// current time, this way the user would be reminded again. Or we introduce a property
45-
// errorAboutFixedSeeedAt. Would be simpler for a user to define. Imagine the following, config property
46-
// errorAboutFixedSeedAfterDuration = PT30M is set and the user gets notified after 30min (because
47-
// they did not figure out in those 30min why the test failed. They estimate they need another 1h
48-
// instead of calculating what seedFixedAt they should set so that they are reminded after 1h, it
49-
// would be simpler if they could just set the DateTime in errorAboutFixedSeedAt
50-
}
46+
.apply { setByEnv() }
47+
.apply { setByPropertiesInResource("/minimalist.local.properties", configFileSpecifics, parser) }
48+
.apply {
49+
val fixedSeed = seed != initialConfig.seed.value
50+
51+
println("Minimalist${if (fixedSeed) " fixed" else ""} seed $seed ${if (skip != null) "skipping $skip " else ""} in env $activeEnv ")
52+
53+
with(configFileSpecifics) {
54+
checkIsPositive(
55+
remindAboutFixedPropertiesAfterMinutes,
56+
"remindAboutFixedPropertiesAfterMinutes"
57+
)
58+
val projectRootDir = Paths.get("").toAbsolutePath().normalize()
59+
val localPropertiesPath =
60+
configFileSpecifics.minimalistPropertiesDir.resolve("minimalist.local.properties")
61+
.toAbsolutePath()
62+
.normalize()
63+
check(localPropertiesPath.startsWith(projectRootDir)) {
64+
"localPropertiesPath (l) must be within the projects root directory (p)\nl: $localPropertiesPath\np: $projectRootDir"
65+
}
66+
checkDeadline(localPropertiesPath, seed.takeIf { fixedSeed }, "seed")
67+
checkDeadline(localPropertiesPath, skip, "skip")
68+
checkDeadline(localPropertiesPath, maxArgs, "maxArgs")
69+
checkDeadline(localPropertiesPath, requestedMinArgs, "requestedMinArgs")
70+
}
71+
}.build()
72+
5173
}
5274

5375
private fun errorMessageNotAllowedToModify(what: String) =
5476
"You are not allowed to modify $what via minimalist.properties use minimalist.local.properties to fix a seed"
5577

56-
private fun MinimalistConfig.mergeWithPropertiesInResource(
78+
79+
private fun ConfigFileSpecifics.checkDeadline(
80+
localPropertiesPath: Path,
81+
propertyValue: Any?,
82+
propertyName: String,
83+
) {
84+
val definedDeadline = errorDeadlines[propertyName]
85+
val deadlinePropertyName = """${ERROR_DEADLINES_PREFIX}${propertyName}"""
86+
if (propertyValue == null) {
87+
if (definedDeadline != null) {
88+
localPropertiesPath.unsetErrorDeadlineFor(deadlinePropertyName)
89+
}
90+
} else {
91+
if (definedDeadline == null) {
92+
localPropertiesPath.setErrorDeadlineFor(
93+
propertyName,
94+
LocalDateTime.now().plusMinutes(remindAboutFixedPropertiesAfterMinutes.toLong())
95+
)
96+
} else if (definedDeadline.isBefore(LocalDateTime.now())) {
97+
throw MinimalistDeadlineException(
98+
"""
99+
|$propertyName is still set (is $propertyValue) and $deadlinePropertyName (which is $definedDeadline) passed.
100+
|Either:
101+
|a) remove/comment out the property `$propertyName`
102+
|b) remove $deadlinePropertyName (in which case a new deadline is set)
103+
|c) set $deadlinePropertyName manually to a later date/time
104+
|The adjustments need to be made in the following file:
105+
|${localPropertiesPath}
106+
|
107+
""".trimMargin()
108+
)
109+
}
110+
}
111+
}
112+
113+
private fun Path.setErrorDeadlineFor(
114+
propertyName: String,
115+
deadline: LocalDateTime
116+
) {
117+
appendText(
118+
"""
119+
|
120+
|# You have set `$propertyName` and this deadline will remind you to remove it again.
121+
|${ERROR_DEADLINES_PREFIX}$propertyName=${deadline.format(DateTimeFormatter.ISO_DATE_TIME)}
122+
|
123+
""".trimMargin()
124+
)
125+
}
126+
127+
private fun Path.unsetErrorDeadlineFor(deadlinePropertyName: String) {
128+
replaceText {
129+
it.replace(Regex("\n(#.*\n)*${deadlinePropertyName}=.*"), "")
130+
}
131+
}
132+
133+
private fun Path.replaceText(replace: (String) -> String) {
134+
val content = replace(readText())
135+
writeText(content)
136+
}
137+
138+
139+
private fun MinimalistConfigBuilder.setByPropertiesInResource(
57140
propertiesFile: String,
141+
configFileSpecifics: ConfigFileSpecifics,
58142
parser: MinimalistPropertiesParser
59-
): MinimalistConfig = this::class.java.getResourceAsStream(propertiesFile)?.let {
60-
it.use { input ->
61-
val props = Properties()
62-
props.load(input)
63-
parser.mergeWithProperties(this, props)
143+
) {
144+
this::class.java.getResourceAsStream(propertiesFile)?.also {
145+
it.use { input ->
146+
val props = Properties()
147+
props.load(input)
148+
parser.mergeWithProperties(this, configFileSpecifics, props)
149+
}
64150
}
65-
} ?: this
151+
}
66152

67-
private fun MinimalistConfig.mergeWithEnv(): MinimalistConfig =
68-
determineEnv()?.let { copy { activeEnv = it } } ?: this
69153

70-
private fun MinimalistConfig.determineEnv(): String? =
154+
private fun MinimalistConfigBuilder.setByEnv() {
155+
determineEnv()?.also { activeEnv = it }
156+
}
157+
158+
private fun MinimalistConfigBuilder.determineEnv(): String? =
71159
System.getenv("MINIMALIST_ENV") ?: run {
72-
val envs = testProfiles.envs(this.defaultProfile)
160+
val envs = testProfiles[defaultProfile] ?: error("profile $defaultProfile does not exist")
161+
// only determine envs if at least one standard env is defined (as others we don't know how to map)
73162
takeIf(Env.entries.any { it.name in envs }) {
74163
determineEnvBasedOnGitHubActions()
75164
?: determineEnvBasedOnGitLab()
76165
?: determineEnvBasedOnBitBucket()
77-
}?.let { it.name.takeIf { env -> env in envs } }
166+
}?.let { it.name.takeIf { env -> env in envs.keys } }
78167
}
79168

80169
private fun determineEnvBasedOnGitHubActions(): Env? =
81-
System.getenv("GITHUB_ENV")?.blankToNull()?.let { event ->
170+
System.getenv("GITHUB_EVENT_NAME")?.blankToNull()?.let { event ->
82171
when (event) {
83172
"pull_request" -> determinePrEnv(getGithubEnv("GITHUB_BASE_REF"))
84173
"push" -> determinePushEnv(getGithubEnv("GITHUB_REF_NAME"))
@@ -120,3 +209,23 @@ class MinimalistConfigViaPropertiesLoader {
120209
else -> Env.Main
121210
}
122211
}
212+
213+
/**
214+
* Contains properties which are not exposed via [MinimalistConfig] and are used during parsing a [MinimalistConfig]
215+
* file.
216+
*
217+
* !! No backward compatibility guarantees !!
218+
* Reuse at your own risk
219+
*
220+
* @since 2.0.0
221+
*/
222+
class ConfigFileSpecifics(
223+
/**
224+
* Defines in about how many minutes the reminder triggers when fixing a property (such as
225+
* [MinimalistConfigBuilder.seed], [MinimalistConfigBuilder.skip],
226+
* [MinimalistConfigBuilder.requestedMinArgs], [MinimalistConfigBuilder.maxArgs]).
227+
*/
228+
var remindAboutFixedPropertiesAfterMinutes: Int = 60,
229+
var minimalistPropertiesDir: Path = Paths.get("./src/test/resources"),
230+
var errorDeadlines: HashMap<String, LocalDateTime> = HashMap(),
231+
)

0 commit comments

Comments
 (0)