Skip to content

Commit a3f4a6c

Browse files
ia-keatonbrianeray
andauthored
Added ability to support "required" flag in module dependencies of module.xml (#48)
Supports IGN-9137 Co-authored-by: Brian Ray <brianeray@users.noreply.github.com>
1 parent 579957f commit a3f4a6c

11 files changed

Lines changed: 386 additions & 15 deletions

File tree

generator/generator-core/build.gradle.kts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ tasks {
4747
named<KotlinCompile>("compileTestKotlin") {
4848
// don't try compiling resources that somehow end up in the test compilation path when we add the integration
4949
// test suite
50-
sourceSets {
51-
exclude("**/resources/**/*.groovy")
52-
exclude("**/resources/**/*.kts")
53-
}
50+
exclude("**/resources/**/*.kts")
5451
}
5552
}
5653

generator/generator-core/src/main/resources/templates/config/modlPluginConfig.groovy

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,34 @@ ignitionModule {
3838
*
3939
* Example Value:
4040
* moduleDependencies = [
41-
"com.inductiveautomation.vision": "CD",
42-
"com.inductiveautomation.opcua": "G"
41+
* "com.inductiveautomation.vision": "CD",
42+
* "com.inductiveautomation.opcua": "G"
4343
* ]
4444
*/
4545
moduleDependencies = [ : ]
4646

47+
/*
48+
* Add required module dependencies here, following the examples, with scope being one or more of G, C or D,
49+
* for (G)ateway, (D)esigner, Vision (C)lient.
50+
*
51+
* Example:
52+
* moduleDependencySpecs {
53+
* register("com.inductiveautomation.vision") {
54+
* scope = "GCD"
55+
* required = true
56+
* }
57+
* // register("com.another.mod") { ...
58+
* }
59+
*
60+
* If any of module's required module dependencies are not present, the
61+
* gateway will fault on loading the module.
62+
*
63+
* NOTE: For modules targeting Ignition 8.3 and later. Use `moduleDependencies` for 8.1 and earlier.
64+
* This property will only add the "required" flag if {requiredIgnitionVersion} is at least 8.3
65+
*
66+
*/
67+
moduleDependencySpecs { }
68+
4769
/*
4870
* Map of fully qualified hook class to the shorthand scope. Only one scope per hook class.
4971
*

generator/generator-core/src/main/resources/templates/config/modlPluginConfig.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ ignitionModule {
4646
*/
4747
moduleDependencies.set(mapOf<String, String>())
4848

49+
/*
50+
* Add required module dependencies here, following the examples, with scope being one or more of G, C or D,
51+
* for (G)ateway, (D)esigner, Vision (C)lient.
52+
*
53+
* Example:
54+
* moduleDependencySpecs {
55+
* register("com.inductiveautomation.vision") {
56+
* scope = "GCD"
57+
* required = true
58+
* }
59+
* // register("com.another.mod") { ...
60+
* }
61+
*
62+
* If any of module's required module dependencies are not present, the
63+
* gateway will fault on loading the module.
64+
*
65+
* NOTE: For modules targeting Ignition 8.3 and later. Use `moduleDependencies` for 8.1 and earlier.
66+
* This property will only add the "required" flag if {requiredIgnitionVersion} is at least 8.3
67+
*
68+
*/
69+
moduleDependencySpecs { }
70+
4971
/*
5072
* Map of fully qualified hook class to the shorthand scope. Only one scope may apply to a class, and each scope
5173
* must have no more than single class registered. You may omit scope registrations if they do not apply.

gradle-module-plugin/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ ignitionModule {
110110
*
111111
* Example Value:
112112
* moduleDependencies = [
113-
"com.inductiveautomation.opcua": "G"
113+
* "com.inductiveautomation.opcua": "G"
114114
* ]
115115
*/
116116
moduleDependencies = [ : ] // syntax for initializing an empty map in groovy
@@ -187,6 +187,33 @@ ignitionModule {
187187
"com.inductiveautomation.opcua" to "G"
188188
))
189189

190+
/*
191+
* Add required module dependencies here, following the examples, with scope being one or more of G, C or D,
192+
* for (G)ateway, (D)esigner, Vision (C)lient.
193+
*
194+
* Example:
195+
* moduleDependencySpecs {
196+
* register("com.inductiveautomation.vision") {
197+
* scope = "GCD"
198+
* required = true
199+
* }
200+
* // register("com.another.mod") { ...
201+
* }
202+
*
203+
* If any of module's required module dependencies are not present, the
204+
* gateway will fault on loading the module.
205+
*
206+
* NOTE: For modules targeting Ignition 8.3 and later. Use `moduleDependencies` for 8.1 and earlier.
207+
* This property will only add the "required" flag if {requiredIgnitionVersion} is at least 8.3
208+
*
209+
*/
210+
moduleDependencySpecs {
211+
register("com.inductiveautomation.vision") {
212+
scope = "CD"
213+
required = true
214+
}
215+
}
216+
190217
/*
191218
* Map of fully qualified hook class to the shorthand scope. Only one scope may apply to a class, and each scope
192219
* must have no more than single class registered. You may omit scope registrations if they do not apply.

gradle-module-plugin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ repositories {
2020
}
2121

2222
group = "io.ia.sdk"
23-
version = "0.2.0"
23+
version = "0.3.0"
2424

2525
configurations {
2626
val functionalTestImplementation by registering {
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package io.ia.sdk.gradle.modl.task
2+
3+
import io.ia.ignition.module.generator.ModuleGenerator
4+
import io.ia.ignition.module.generator.api.GeneratorConfigBuilder
5+
import io.ia.ignition.module.generator.api.GradleDsl
6+
import io.ia.sdk.gradle.modl.BaseTest
7+
import io.ia.sdk.gradle.modl.util.collapseXmlToOneLine
8+
import org.gradle.testkit.runner.BuildResult
9+
import org.gradle.testkit.runner.TaskOutcome
10+
import java.io.File
11+
import java.nio.file.Path
12+
import kotlin.io.path.readText
13+
import kotlin.test.Test
14+
import kotlin.test.assertContains
15+
import kotlin.test.assertEquals
16+
17+
class WriteModuleXmlTest : BaseTest() {
18+
companion object {
19+
const val MODULE_NAME = "ModuleXmlTest"
20+
const val PACKAGE_NAME = "module.xml.test"
21+
const val DEPENDS = "<depends"
22+
}
23+
24+
@Test
25+
// @Tag("IGN-9137")
26+
fun `single module dependency marked as not required`() {
27+
val dirName = currentMethodName()
28+
val replacements = mapOf(
29+
"moduleDependencySpecs { }" to
30+
"""
31+
moduleDependencySpecs {
32+
register("io.ia.modl") {
33+
scope = "GCD"
34+
required = false
35+
}
36+
}
37+
""",
38+
"requiredIgnitionVersion = \"8.0.10\"" to
39+
"requiredIgnitionVersion = \"8.3.0\""
40+
)
41+
42+
val oneLineXml = generateXml(dirName, replacements)
43+
44+
assertContains(
45+
oneLineXml,
46+
"""<depends scope="GCD" required="false">io.ia.modl</depends>"""
47+
)
48+
assertEquals(
49+
Regex(DEPENDS).findAll(oneLineXml).toList().size,
50+
1
51+
)
52+
}
53+
54+
@Test
55+
// @Tag("IGN-9137")
56+
fun `multiple module dependencies marked as required`() {
57+
val dirName = currentMethodName()
58+
val replacements = mapOf(
59+
"moduleDependencySpecs { }" to
60+
"""
61+
moduleDependencySpecs {
62+
register("io.ia.modl") {
63+
scope = "GCD"
64+
required = true
65+
}
66+
register("io.ia.otherModl") {
67+
scope = "G"
68+
required = true
69+
}
70+
}
71+
""",
72+
"requiredIgnitionVersion = \"8.0.10\"" to
73+
"requiredIgnitionVersion = \"8.3.0\""
74+
)
75+
76+
val oneLineXml = generateXml(dirName, replacements)
77+
78+
assertContains(
79+
oneLineXml,
80+
"""<depends scope="GCD" required="true">io.ia.modl</depends>"""
81+
)
82+
assertContains(
83+
oneLineXml,
84+
"""<depends scope="G" required="true">io.ia.otherModl</depends>"""
85+
)
86+
assertEquals(
87+
Regex(DEPENDS).findAll(oneLineXml).toList().size,
88+
2
89+
)
90+
}
91+
92+
@Test
93+
// @Tag("IGN-9137")
94+
fun `module dependencies via compact, eager DSL`() {
95+
val dirName = currentMethodName()
96+
97+
// This allows for streamlined, magical build scripts but there is a
98+
// slight performance hit as the ModuleDependencySpecs are eagerly
99+
// created during build script configuration as opposed to registered
100+
// for lazy configuration only on demand. With `register` as in other
101+
// tests here and per our guidance in the doc that _should_ only be
102+
// when `writeModuleXml` task is fired. One can imagine use cases where
103+
// that task is not fired and this eager instance creation is an
104+
// unnecessary waste of CPU cycles.
105+
val replacements = mapOf(
106+
"moduleDependencySpecs { }" to
107+
"""
108+
moduleDependencySpecs {
109+
"io.ia.modl" {
110+
scope = "GCD"
111+
required = true
112+
}
113+
"io.ia.otherModl" {
114+
scope = "G"
115+
required = true
116+
}
117+
}
118+
""",
119+
"requiredIgnitionVersion = \"8.0.10\"" to
120+
"requiredIgnitionVersion = \"8.3.0\""
121+
)
122+
123+
val oneLineXml = generateXml(
124+
dirName,
125+
replacements,
126+
// true,
127+
)
128+
129+
assertContains(
130+
oneLineXml,
131+
"""<depends scope="GCD" required="true">io.ia.modl</depends>"""
132+
)
133+
assertContains(
134+
oneLineXml,
135+
"""<depends scope="G" required="true">io.ia.otherModl</depends>"""
136+
)
137+
assertEquals(
138+
Regex(DEPENDS).findAll(oneLineXml).toList().size,
139+
2
140+
)
141+
}
142+
143+
@Test
144+
// @Tag("IGN-9137")
145+
fun `legacy module dependencies not marked at all for requiredness`() {
146+
val dirName = currentMethodName()
147+
148+
val replacements = mapOf(
149+
"moduleDependencies = [ : ]" to
150+
"moduleDependencies = ['io.ia.modl': 'GCD']"
151+
)
152+
153+
val oneLineXml = generateXml(dirName, replacements)
154+
155+
assertContains(
156+
oneLineXml,
157+
"""<depends scope="GCD">io.ia.modl</depends>"""
158+
)
159+
assertEquals(
160+
Regex(DEPENDS).findAll(oneLineXml).toList().size,
161+
1
162+
)
163+
}
164+
165+
private fun generateModule(
166+
projDir: File,
167+
replacements: Map<String, String> = mapOf(),
168+
): Path {
169+
val config = GeneratorConfigBuilder()
170+
.moduleName(MODULE_NAME)
171+
.scopes("GCD")
172+
.packageName(PACKAGE_NAME)
173+
.parentDir(projDir.toPath())
174+
.customReplacements(replacements)
175+
.debugPluginConfig(true)
176+
.allowUnsignedModules(true)
177+
.settingsDsl(GradleDsl.GROOVY)
178+
.rootPluginConfig(
179+
"""
180+
id("io.ia.sdk.modl")
181+
""".trimIndent()
182+
)
183+
.build()
184+
185+
return ModuleGenerator.generate(config)
186+
}
187+
188+
private fun generateXml(
189+
dirName: String,
190+
replacements: Map<String, String> = mapOf(),
191+
dumpBuildScript: Boolean = false,
192+
): String {
193+
val projectDir = generateModule(
194+
tempFolder.newFolder(dirName),
195+
replacements,
196+
)
197+
198+
if (dumpBuildScript) {
199+
println("build script:")
200+
println(projectDir.resolve("build.gradle").readText())
201+
}
202+
203+
val result: BuildResult = runTask(
204+
projectDir.toFile(),
205+
listOf(
206+
"writeModuleXml",
207+
"--stacktrace",
208+
)
209+
)
210+
211+
val task = result.task(":writeModuleXml")
212+
assertEquals(task?.outcome, TaskOutcome.SUCCESS)
213+
214+
// We could do real XML parsing here but this is just a test,
215+
// quick-and-dirty should be fine.
216+
return collapseXmlToOneLine(
217+
projectDir.resolve("build/moduleContent/module.xml").readText()
218+
)
219+
}
220+
}

gradle-module-plugin/src/functionalTest/kotlin/io/ia/sdk/gradle/modl/util/utils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ fun signedModuleName(humanModuleName: String): String {
1111
fun nameToDirName(moduleName: String): String {
1212
return moduleName.split(" ").joinToString("-") { it.lowercase() }
1313
}
14+
15+
// For when you don't need full-blown XML parsing just to test. Smoosh all
16+
// tags together in one long line by knocking out indentation and newlines.
17+
fun collapseXmlToOneLine(xml: String): String =
18+
xml.replace(Regex("""^\s+"""), "").replace(Regex("""\R"""), "")

gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/IgnitionModlPlugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class IgnitionModlPlugin : Plugin<Project> {
158158
xmlTask.moduleName.set(settings.name)
159159
xmlTask.moduleVersion.set(settings.moduleVersion)
160160
xmlTask.moduleDependencies.set(settings.moduleDependencies)
161+
xmlTask.moduleDependencySpecs.set(settings.moduleDependencySpecs.toSet())
161162
xmlTask.requiredIgnitionVersion.set(settings.requiredIgnitionVersion)
162163
xmlTask.requiredFrameworkVersion.set(settings.requiredFrameworkVersion)
163164
xmlTask.requireFromPlatform.set(settings.requireFromPlatform)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.ia.sdk.gradle.modl.extension
2+
3+
import org.gradle.api.Named
4+
import org.gradle.api.tasks.Input
5+
import java.io.Serializable
6+
7+
abstract class ModuleDependencySpec : Named, Serializable {
8+
@get:Input
9+
var scope: String = ""
10+
11+
@get:Input
12+
var required: Boolean = false
13+
}

0 commit comments

Comments
 (0)