diff --git a/.github/workflows/publishing_packages.yml b/.github/workflows/publishing_packages.yml index 5c17818..142729e 100644 --- a/.github/workflows/publishing_packages.yml +++ b/.github/workflows/publishing_packages.yml @@ -14,11 +14,6 @@ jobs: sed -i -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" gradle.properties - name: prebuild run: ./gradlew clean build - - name: Publish to Gitea - continue-on-error: true - run: ./gradlew publishAllPublicationsToGiteaRepository - env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - name: Publish package continue-on-error: true run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository diff --git a/CHANGELOG.md b/CHANGELOG.md index 18fb0e0..54d6185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.5.1 + +* Add cleaning up of incoming template, which must remove all malformed parts from string template (fix of [#126](https://github.com/InsanusMokrassar/krontab/issues/126)) +* Add support of insufficient amount of arguments (fix of [#126](https://github.com/InsanusMokrassar/krontab/issues/126)) +* `Version`: + * `Kotlin`: `2.0.21` + * `AndroidXWork`: `2.10.0` + ## 2.5.0 * `Version`: diff --git a/gradle.properties b/gradle.properties index d6b7705..f9510e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ android.useAndroidX=true android.enableJetifier=false -kotlin_version=2.0.20 +kotlin_version=2.0.21 kotlin_coroutines_version=1.9.0 kotlin_serialization_version=1.7.3 @@ -32,9 +32,9 @@ junit_version=4.12 test_ext_junit_version=1.1.3 espresso_core=3.4.0 -androidx_work_version=2.9.1 +androidx_work_version=2.10.0 ## Common -version=2.5.0 -android_code_version=42 +version=2.5.1 +android_code_version=43 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 95d3181..2e8f7eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip diff --git a/src/commonMain/kotlin/KrontabConfig.kt b/src/commonMain/kotlin/KrontabConfig.kt index d0aa227..2afcb2f 100644 --- a/src/commonMain/kotlin/KrontabConfig.kt +++ b/src/commonMain/kotlin/KrontabConfig.kt @@ -1,5 +1,6 @@ package dev.inmo.krontab +import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.CronDateTimeScheduler import dev.inmo.krontab.internal.CronDateTimeSchedulerTz import dev.inmo.krontab.internal.createKronScheduler @@ -94,27 +95,37 @@ value class KrontabConfig( var dayOfWeekParsed: Array? = null var yearParsed: Array? = null var millisecondsParsed: Array? = null - val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = template.split(" ").also { - listOfNotNull( - it.getOrNull(5), - it.getOrNull(6), - it.getOrNull(7), - it.getOrNull(8) - ).forEach { - val offsetFromString = parseOffset(it) - val dayOfWeekFromString = parseWeekDay(it) - val millisecondsFromString = parseMilliseconds(it) - offsetParsed = offsetParsed ?: offsetFromString - dayOfWeekParsed = dayOfWeekParsed ?: dayOfWeekFromString - millisecondsParsed = millisecondsParsed ?: millisecondsFromString - when { - dayOfWeekFromString != null || offsetFromString != null || millisecondsFromString != null -> return@forEach - yearParsed == null -> { - yearParsed = parseYears(it) + val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = template + .split(" ") + .filter { checkIncomingPart(it) } // filter garbage from string + .let { + if (it.size < 5) { // reconstruction in case of insufficient arguments; 5 is amount of required arguments out of latest also code + it + (it.size until 5).map { "*" } + } else { + it + } + } + .also { + listOfNotNull( + it.getOrNull(5), + it.getOrNull(6), + it.getOrNull(7), + it.getOrNull(8) + ).forEach { + val offsetFromString = parseOffset(it) + val dayOfWeekFromString = parseWeekDay(it) + val millisecondsFromString = parseMilliseconds(it) + offsetParsed = offsetParsed ?: offsetFromString + dayOfWeekParsed = dayOfWeekParsed ?: dayOfWeekFromString + millisecondsParsed = millisecondsParsed ?: millisecondsFromString + when { + dayOfWeekFromString != null || offsetFromString != null || millisecondsFromString != null -> return@forEach + yearParsed == null -> { + yearParsed = parseYears(it) + } } } } - } val secondsParsed = parseSeconds(secondsSource) val minutesParsed = parseMinutes(minutesSource) diff --git a/src/commonMain/kotlin/internal/Parser.kt b/src/commonMain/kotlin/internal/Parser.kt index c02cfa0..caaad9b 100644 --- a/src/commonMain/kotlin/internal/Parser.kt +++ b/src/commonMain/kotlin/internal/Parser.kt @@ -37,6 +37,76 @@ private fun createSimpleScheduler(from: String, dataRange: IntRange, dataCon return results.map(dataConverter) } +/** + * FSM for parsing of incoming data. If at the end of parsing it have non-null state and string is not empty, data passed check + * + * 1. + * * \\d -> 1 + * * \\* -> 2 + * * \\- -> 5 + * * , -> 1 + * * m -> 6 + * * o -> 7 + * * w -> 7 + * 2. + * * / -> 3 + * 3. + * * \\d -> 3 + * * \\* -> 4 + * 4. + * * , -> 1 + * 5. + * * \\d -> 5 + * * , -> 1 + * 6. + * * s -> 7 + * 7. Empty, end of parse + */ +private val checkIncomingPartTransitionsMap = listOf( + listOf( + Regex("\\d") to 0, + Regex("\\*") to 1, + Regex("-") to 4, + Regex(",") to 0, + Regex("m") to 5, + Regex("o") to 6, + Regex("w") to 6, + ), + listOf( + Regex("/") to 2, + ), + listOf( + Regex("\\d") to 2, + Regex("\\*") to 3, + ), + listOf( + Regex(",") to 0, + ), + listOf( + Regex("\\d") to 4, + Regex(",") to 0, + ), + listOf( + Regex("s") to 6, // end of ms + ), + listOf(), // empty state, end of parsing +) +internal fun checkIncomingPart(part: String): Boolean { + var i = 0 + var state = checkIncomingPartTransitionsMap[0] + while (i < part.length) { + val char = part[i] + val nextState = state.firstNotNullOfOrNull { + it.second.takeIf { _ -> it.first.matches("$char") } + } + if (nextState == null) return false + state = checkIncomingPartTransitionsMap[nextState] + i++ + } + + return part.isNotEmpty() +} + internal fun parseWeekDay(from: String?) = from ?.let { if (it.endsWith("w")) createSimpleScheduler(it.removeSuffix("w"), dayOfWeekRange, intToByteConverter) ?.toTypedArray() else null } internal fun parseOffset(from: String?) = from ?.let { if (it.endsWith("o")) it.removeSuffix("o").toIntOrNull() else null } internal fun parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() } diff --git a/src/commonTest/kotlin/StringParseTest.kt b/src/commonTest/kotlin/StringParseTest.kt index 3e76d0a..0e44c94 100644 --- a/src/commonTest/kotlin/StringParseTest.kt +++ b/src/commonTest/kotlin/StringParseTest.kt @@ -29,6 +29,57 @@ class StringParseTest { } } @Test + fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnStringWithWrongAmountOfSpaces() { + val kronScheduler = buildSchedule("*/1 * * * * ") + + val flow = kronScheduler.asFlowWithoutDelays() + + runTest { + val mustBeCollected = 10 + var collected = 0 + flow.takeWhile { + collected < mustBeCollected + }.collect { + collected++ + } + assertEquals(mustBeCollected, collected) + } + } + @Test + fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnStringWithGarbageInTemplate() { + val kronScheduler = buildSchedule(" sdf */1 * * * oo * ") + + val flow = kronScheduler.asFlowWithoutDelays() + + runTest { + val mustBeCollected = 10 + var collected = 0 + flow.takeWhile { + collected < mustBeCollected + }.collect { + collected++ + } + assertEquals(mustBeCollected, collected) + } + } + @Test + fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnStringWithInsufficientArgsInTemplate() { + val kronScheduler = buildSchedule(" sdf */1 ") + + val flow = kronScheduler.asFlowWithoutDelays() + + runTest { + val mustBeCollected = 10 + var collected = 0 + flow.takeWhile { + collected < mustBeCollected + }.collect { + collected++ + } + assertEquals(mustBeCollected, collected) + } + } + @Test fun testThatFlowIsCorrectlyWorkEverySecondWhenMillisIsHalfOfSecondBuiltOnString() { val kronScheduler = buildSchedule("*/1 * * * * 500ms")