@@ -4,7 +4,17 @@ import ch.tutteli.kbox.blankToNull
44import ch.tutteli.kbox.takeIf
55import com.tegonal.minimalist.config.Env
66import 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
714import 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 */
1525class 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)\n l: $localPropertiesPath \n p: $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