Skip to content

Commit 26fbf37

Browse files
Steve RamageSJrX
authored andcommitted
Resolves #242 - Add support for config_parse_sec
1 parent 12580d0 commit 26fbf37

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
${{ runner.os }}-gradle-
4444
- name: Build with Gradle
4545
run: |
46+
./gradlew composeUp
4647
./gradlew test buildPlugin
4748
./gradlew --stop
4849
- name: Publish Unit Test Results

build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,16 @@ tasks {
345345
dependsOn("generateUnitAutoCompleteData")
346346
}
347347

348+
checkstyleMain {
349+
dependsOn("generateDataFromManPages")
350+
dependsOn("generateUnitAutoCompleteData")
351+
}
352+
353+
instrumentedJar {
354+
dependsOn("generateDataFromManPages")
355+
dependsOn("generateUnitAutoCompleteData")
356+
}
357+
348358
compileTestKotlin {
349359
dependsOn("generateUnitAutoCompleteData")
350360
dependsOn("generateDataFromManPages")

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class SemanticDataRepository private constructor() {
8181
validatorMap.putAll(UnsignedIntegerOptionValue.validators)
8282
validatorMap.putAll(PathOptionValue.validators)
8383
validatorMap.putAll(EnumOptionValues.validators)
84+
validatorMap.putAll(ConfigParseSecOptionValue.validators)
8485

8586
// Scopes are not supported since they aren't standard unit files.
8687

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues
2+
3+
import com.intellij.openapi.project.Project
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator
5+
6+
class ConfigParseSecOptionValue : OptionValueInformation {
7+
override fun getAutoCompleteOptions(project: Project): Set<String> {
8+
return emptySet()
9+
}
10+
11+
override fun getErrorMessage(value: String): String? {
12+
try {
13+
TimeHelper.parseSecs(value)
14+
return null
15+
} catch (e : IllegalArgumentException) {
16+
return "Invalid value: ${e.message}"
17+
18+
}
19+
}
20+
21+
override val validatorName : String
22+
get() = VALIDATOR_NAME
23+
24+
25+
companion object {
26+
const val VALIDATOR_NAME = "config_parse_sec"
27+
28+
val validators = mapOf(Validator(VALIDATOR_NAME, "0") to ConfigParseSecOptionValue())
29+
}
30+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues
2+
3+
/**
4+
* This class is adapted from time-util.h/c
5+
* https://github.com/systemd/systemd/blob/main/src/basic/time-util.h#L46
6+
* https://github.com/systemd/systemd/blob/main/src/basic/time-util.c#L1131
7+
*/
8+
9+
10+
data class Multiplier(val str : String, val usecs : Long)
11+
12+
13+
object TimeHelper {
14+
val USEC_PER_SEC: Long = 1000000
15+
val USEC_PER_MSEC: Long = 1000
16+
val NSEC_PER_SEC: Long = 1000000000
17+
val NSEC_PER_MSEC: Long = 1000000
18+
val NSEC_PER_USEC: Long = 1000
19+
20+
val USEC_PER_MINUTE: Long = 60 * USEC_PER_SEC
21+
val NSEC_PER_MINUTE: Long = 60 * NSEC_PER_SEC
22+
val USEC_PER_HOUR: Long = 60 * USEC_PER_MINUTE
23+
val NSEC_PER_HOUR: Long = 60 * NSEC_PER_MINUTE
24+
val USEC_PER_DAY: Long = 24 * USEC_PER_HOUR
25+
val NSEC_PER_DAY: Long = 24 * NSEC_PER_HOUR
26+
val USEC_PER_WEEK: Long = 7 * USEC_PER_DAY
27+
val NSEC_PER_WEEK: Long = 7 * NSEC_PER_DAY
28+
val USEC_PER_MONTH: Long = 2629800 * USEC_PER_SEC
29+
val NSEC_PER_MONTH: Long = 2629800 * NSEC_PER_SEC
30+
val USEC_PER_YEAR: Long = 31557600 * USEC_PER_SEC
31+
val NSEC_PER_YEAR: Long = 31557600 * NSEC_PER_SEC
32+
33+
val multiplers = listOf(
34+
Multiplier( "seconds", USEC_PER_SEC ),
35+
Multiplier( "second", USEC_PER_SEC ),
36+
Multiplier( "sec", USEC_PER_SEC ),
37+
Multiplier( "s", USEC_PER_SEC ),
38+
Multiplier( "minutes", USEC_PER_MINUTE ),
39+
Multiplier( "minute", USEC_PER_MINUTE ),
40+
Multiplier( "min", USEC_PER_MINUTE ),
41+
Multiplier( "months", USEC_PER_MONTH ),
42+
Multiplier( "month", USEC_PER_MONTH ),
43+
Multiplier( "M", USEC_PER_MONTH ),
44+
Multiplier( "msec", USEC_PER_MSEC ),
45+
Multiplier( "ms", USEC_PER_MSEC ),
46+
Multiplier( "m", USEC_PER_MINUTE ),
47+
Multiplier( "hours", USEC_PER_HOUR ),
48+
Multiplier( "hour", USEC_PER_HOUR ),
49+
Multiplier( "hr", USEC_PER_HOUR ),
50+
Multiplier( "h", USEC_PER_HOUR ),
51+
Multiplier( "days", USEC_PER_DAY ),
52+
Multiplier( "day", USEC_PER_DAY ),
53+
Multiplier( "d", USEC_PER_DAY ),
54+
Multiplier( "weeks", USEC_PER_WEEK ),
55+
Multiplier( "week", USEC_PER_WEEK ),
56+
Multiplier( "w", USEC_PER_WEEK ),
57+
Multiplier( "years", USEC_PER_YEAR ),
58+
Multiplier( "year", USEC_PER_YEAR ),
59+
Multiplier( "y", USEC_PER_YEAR ),
60+
Multiplier( "usec", 1 ),
61+
Multiplier( "us", 1 ),
62+
Multiplier( "μs", 1 ), /* U+03bc (aka GREEK SMALL LETTER MU) */
63+
Multiplier( "µs", 1 ),
64+
)
65+
66+
val WHITESPACE = """[ \t\n\r]""".toRegex()
67+
68+
val NUMBER = """^\d*\.?\d+""".toRegex()
69+
70+
fun parseSecs(time: String): ULong {
71+
return parseTime(time, Multiplier( "sec", USEC_PER_SEC ));
72+
}
73+
74+
fun parseTime(time: String, defaultMultiplier : Multiplier): ULong {
75+
76+
var p = time.trim()
77+
78+
var something = false
79+
80+
var usec : ULong = 0u
81+
82+
if (p == "infinity") {
83+
return ULong.MAX_VALUE
84+
}
85+
86+
87+
while(true) {
88+
p = p.trimStart()
89+
90+
if (p.length == 0) {
91+
if (!something) {
92+
throw IllegalArgumentException("Could not parse $p in $time")
93+
} else {
94+
break
95+
}
96+
}
97+
98+
if (p.startsWith('-')) {
99+
throw IllegalArgumentException("Negatives are not allowed in $time")
100+
}
101+
102+
val result = NUMBER.find(p)
103+
104+
val value = result?.groups?.get(0)?.value?.toDouble() ?: throw IllegalArgumentException("Could not parse leading number ($p) in $time")
105+
p = p.substring(result.groups.get(0)?.value!!.length).trim()
106+
107+
var multiplier : Multiplier? = null
108+
for (m in multiplers) {
109+
if (p.startsWith(m.str)) {
110+
multiplier = m
111+
p = p.substring(m.str.length).trim()
112+
break
113+
}
114+
}
115+
116+
if (multiplier == null) {
117+
multiplier = defaultMultiplier
118+
}
119+
// There is something odd with the below that I don't care about right now.
120+
// For some reason k is turned a signed 32-bit integer.
121+
122+
// if (value >= (ULong.MAX_VALUE / multiplier.usecs.toULong()).toDouble()) {
123+
// // This checks that the single term won't cause an overflow.
124+
// throw IllegalArgumentException("Value is too big $p in $time")
125+
// }
126+
//
127+
val k : ULong = (multiplier.usecs.toULong() * value.toULong())
128+
// if (k > ULong.MAX_VALUE - usec) {
129+
// // This checks that the addition of this value won't cause an overflow.
130+
// throw IllegalArgumentException("Total value is too $p in $time")
131+
// }
132+
133+
usec += k
134+
135+
something = true;
136+
137+
}
138+
139+
140+
141+
142+
return usec
143+
}
144+
145+
146+
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections
2+
3+
import junit.framework.TestCase
4+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
5+
6+
class InvalidValidInspectionForConfigParseSecOptionValueTest : AbstractUnitFileTest() {
7+
8+
fun testWeakWarningWhenStringSpecified() {
9+
// Fixture Setup
10+
// language="unit file (systemd)"
11+
val file = """
12+
[Unit]
13+
StartLimitIntervalSec=abc
14+
""".trimIndent()
15+
16+
17+
// Execute SUT
18+
setupFileInEditor("file.service", file)
19+
enableInspection(InvalidValueInspection::class.java)
20+
val highlights = myFixture.doHighlighting()
21+
22+
// Verification
23+
assertSize(1, highlights)
24+
val info = highlights[0]
25+
AbstractUnitFileTest.Companion.assertStringContains("Could not parse leading number", info!!.description)
26+
TestCase.assertEquals("abc", info.text)
27+
}
28+
29+
fun testWeakWarningWhenNegativeSpecified() {
30+
// Fixture Setup
31+
// language="unit file (systemd)"
32+
val file = """
33+
[Unit]
34+
StartLimitIntervalSec=-5
35+
""".trimIndent()
36+
37+
38+
// Execute SUT
39+
setupFileInEditor("file.service", file)
40+
enableInspection(InvalidValueInspection::class.java)
41+
val highlights = myFixture.doHighlighting()
42+
43+
// Verification
44+
assertSize(1, highlights)
45+
val info = highlights[0]
46+
AbstractUnitFileTest.Companion.assertStringContains("Negatives are not allowed", info!!.description)
47+
TestCase.assertEquals("-5", info.text)
48+
}
49+
50+
fun testWeakWarningWhenNegativeSpecifiedInMultipleTerms() {
51+
// Fixture Setup
52+
// language="unit file (systemd)"
53+
val file = """
54+
[Unit]
55+
StartLimitIntervalSec=1 day -5 secs
56+
""".trimIndent()
57+
58+
59+
// Execute SUT
60+
setupFileInEditor("file.service", file)
61+
enableInspection(InvalidValueInspection::class.java)
62+
val highlights = myFixture.doHighlighting()
63+
64+
// Verification
65+
assertSize(1, highlights)
66+
val info = highlights[0]
67+
AbstractUnitFileTest.Companion.assertStringContains("Negatives are not allowed", info!!.description)
68+
TestCase.assertEquals("1 day -5 secs", info.text)
69+
}
70+
71+
fun testNoWarningsWithVariousFormats() {
72+
// Fixture Setup
73+
// language="unit file (systemd)"
74+
val file = """
75+
[Unit]
76+
StartLimitIntervalSec=1 day
77+
78+
[Service]
79+
RestartSec=542
80+
WatchdogSec=1 years
81+
RuntimeMaxSec=1 us 1 y 1d 1 w 1hr 1m 1 ms
82+
83+
""".trimIndent()
84+
85+
86+
// Execute SUT
87+
setupFileInEditor("file.service", file)
88+
enableInspection(InvalidValueInspection::class.java)
89+
val highlights = myFixture.doHighlighting()
90+
91+
// Verification
92+
assertSize(0, highlights)
93+
94+
}
95+
96+
97+
98+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues
2+
3+
import com.intellij.testFramework.UsefulTestCase
4+
import com.intellij.util.ThrowableRunnable
5+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
6+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.TimeHelper.USEC_PER_MSEC
7+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.TimeHelper.USEC_PER_SEC
8+
import org.junit.Test
9+
10+
11+
class TimeHelperTest : AbstractUnitFileTest() {
12+
13+
@Test
14+
fun testThatInfinityParses() {
15+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs("infinity"))
16+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs(" infinity"))
17+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs("infinity "))
18+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs("infinity "))
19+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs("infinity "))
20+
assertEquals(ULong.MAX_VALUE, TimeHelper.parseSecs("infinity "))
21+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("Infinity"))
22+
}
23+
24+
fun testReferenceValuesFromCCode() {
25+
assertEquals(1 * USEC_PER_SEC, TimeHelper.parseSecs("1").toLong())
26+
assertEquals(1 * USEC_PER_SEC, TimeHelper.parseSecs("1s").toLong())
27+
assertEquals(100 * USEC_PER_MSEC, TimeHelper.parseSecs("100ms").toLong())
28+
assertEquals(5 * 60 * USEC_PER_SEC + 20 * USEC_PER_SEC, TimeHelper.parseSecs("5min 20s").toLong())
29+
30+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("-1"))
31+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("10foo"))
32+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("garbage"))
33+
}
34+
35+
fun testOtherIllegalValues() {
36+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("-0"))
37+
38+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("-12.34.56"))
39+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("0.-0"))
40+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.+1"))
41+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.sec"))
42+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.hoge"))
43+
}
44+
45+
fun testOtherValues() {
46+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("-0"))
47+
48+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("-12.34.56"))
49+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("0.-0"))
50+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.+1"))
51+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.sec"))
52+
UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("3.hoge"))
53+
}
54+
55+
// fun testOverflows() {
56+
//
57+
//// assertEquals(584541UL * USEC_PER_YEAR.toULong(), TimeHelper.parseSecs("584541 years").toULong())
58+
//// UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("584542 years"))
59+
//
60+
//
61+
// UsefulTestCase.assertThrows(IllegalArgumentException::class.java, GetRunnable("584541 years 366 days"))
62+
//
63+
//
64+
// }
65+
66+
67+
}
68+
69+
fun GetRunnable(input: String): ThrowableRunnable<IllegalArgumentException> {
70+
71+
return ThrowableRunnable {
72+
TimeHelper.parseSecs(input)
73+
}
74+
}

0 commit comments

Comments
 (0)