Share
Embed
Conference for Kotliners - Kotlin: the Next Frontier in Metaprogramming
In an epic quest using Kotlin and native desktop development to generate UI testing for the TornadoFX framework, we turn to metaprogramming in Kotlin to explore AST language parsing and the philosophy of design-driven UI testing in the pursuit for smarter test-generation.
mvndy_hd
June 07, 2019
Transcript
65 slides
-
Kotlin: The Next Frontier in Metaprogramming Using metaprogramming to formalize
the theory of human error in applications in generative UI testing Amanda Hinchman-Dominguez @hinchman_amanda -
UIs increasingly critical to the success of applications • An
application’s GUI must create a rich, intuitive and pleasant user experience • Validate + guarantee quality assurance of event-driven applications through the active process of exploring an environment -
Black box testing • Tests for the behavior/ functionality of
software White box testing • Tests how the system functions E2E Integration Unit Quality Assurance -
Black box testing • Tests for the behavior/ functionality of
software White box testing • Tests how the system functions E2E Integration Unit Quality Assurance -
E2E Integration Unit Black box testing • Tests for the
behavior/ functionality of software White box testing • Tests how the system functions UI Testing Quality Assurance -
A UI Test goes something like this… Grab a particular
view Perform action on view Check UI to see if it looks as it should -
A UI Test goes something like this… Grab a particular
view Perform action on view Check UI to see if it looks as it should -
• Kastree - a simple wrapper around the Kotlin compiler
• Program Structure Interface (PSI): the layer in IntelliJ parses syntactic and semantic code models • An Abstract Syntax Tree (AST) parser surpasses challenges found in statically-typed metaprogramming: • Access levels • Type casting in recursion Phase 1: Detecting Inputs w/ AST Parsing -
override val root = hbox { form { fieldset("Edit person")
{ field("Owner") { textfield(model.ownerName) { ownerNameField = this } } field("Cat") { textfield(model.catName) { catNameField = this } } field("Time") { textfield(model.time) { timeField = this } } button("Save") { enableWhen(model.dirty) action { save() } } } } } Saving UI Nodes field fieldset form textfield field textfield field field textfield field fieldset form hbox textfield hbox field textfield textfield button button -
A UI Test goes something like this… Grab a particular
view Perform action on view Check UI to see if it looks as it should -
A UI Test goes something like this… Grab a particular
view Perform action on view Check UI to see if it looks as it should -
• Kotlin scripting to write Kotlin test files for node
interactions • TestFX robots to stimulate user interactions, check node states with matchers • Dynamically insert randomized ids for detected nodes at runtime Phase 2: Generating Tests with TestFX -
• Kotlin scripting to write Kotlin test files for node
interactions • TestFX robots to stimulate user interactions, check node states with matchers • Dynamically insert randomized ids for detected nodes at runtime Phase 2: Generating Tests with TestFX -
A UI Test goes something like this… Grab a particular
view Perform action on view Check UI to see if it looks as it should -
How do we know if a node looks as it
should? • Mapping a compositional abstraction for our components can help define properties surrounding actions of our views • When a control event is triggered, apply binary operations against states and check against the contract holistic result of an application’s representative compositional map -
Phase 3: Abstract Compositional Contracting • Check for the state
of the node after an interaction • Map nodes to associated functions • Create function compositions from node interactions • Check for models attached to views • Model support • Scope/Context support -
Phase 3: Abstract Compositional Contracting • Check for the state
of the node after an interaction • Map nodes to associated functions • Create function compositions from node interactions • Check for models attached to views • Model support • Scope/Context support -
Phase 3: Abstract Compositional Contracting • Check for the state
of the node after an interaction • Map nodes to associated functions • Create function compositions from node interactions • Check for models attached to views • Model support • Scope/Context support -
button form hbox override val root = hbox { form
{ fieldset("Edit person") { field("Owner") { textfield(model.ownerName) { ownerNameField = this } } field("Cat") { textfield(model.catName) { catNameField = this } } field("Time") { textfield(model.time) { timeField = this } } button("Save") { enableWhen(model.dirty) action { save() } } } } } fieldset form fieldset field field field field button textfield field textfield textfield textfield field textfield textfield field textfield Post-parsing analysis -
UINode( nodeType = “Button”, uiLevel = 3, nodeChildren = JsonObject(),
valueAssignment = “”, functionComposition = Digraph<UIFunctionComposition>(), associatedFunctions = ArrayList<String>() ) button Post-parsing analysis -
UINode( nodeType = “Button”, uiLevel = 3, nodeChildren = JsonObject(),
valueAssignment = “”, functionComposition = Digraph<UIFunctionComposition>(), associatedFunctions = ArrayList<String>() ) button Post-parsing analysis -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } fun editCatSchedule(catSchedule: CatSchedule) { val catScheduleScope = CatScheduleScope() catScheduleScope.model.item = catSchedule find(Editor::class, scope = catScheduleScope).openModal() } } Associated Functions -
avi class BottomViewController: Controller() { private val view: BottomView by
inject() fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) Associated Functions -
button("Save") { enableWhen(model.dirty) action { save() update(model) } } Functional
composition - what functions are spurned from interacting with a particular node? -
Function Composition button("Save") { enableWhen(model.dirty) action { save() update(model) }
} private fun save() { model.commit() val catSchedule = model.item close() } private fun update(model: CatScheduleModel) { println("Contact updated!") controller.changeCatAvi(model.item) } save save action fun changeCatAvi(catSchedule: CatSchedule) { view.avi.children.clear() val image = StackPane().apply { rectangle { width = 200.0 height = 200.0 fill = c("#AARRGGBB") opacity = 0.01 } imageview(catSchedule.catImage) } view.avi.children.add(image) } changeCatAvi changeCatAvi update update -
Phase 3: Abstract Compositional Contracting • Check for the state
of the node after an interaction • Map nodes to associated functions • Create function compositions from node interactions • Check for models attached to views • Model support • Scope/Context support -
Creating Finite State Machines to predict node behavior Cat Schedule
Model Save Button Dirty Not Dirty Disabled Enabled -
Creating Finite State Machines to predict node behavior Cat Schedule
Model Save Button Dirty Not Dirty Disabled Enabled -
Cat Schedule Model Save Button Dirty Not Dirty Disabled Enabled
enum class Toggle { Enable, Disable; fun isButtonEnabled(): Boolean { return when (this) { Enable -> true Disable -> false } } fun enableOnDirty(model: ModelState): Toggle { return when (model) { ModelState.Dirty -> Enable ModelState.NotDirty -> Disable } } } enum class ModelState { Dirty, NotDirty; fun initModelStateTransition(): ModelState { return when (this) { Dirty -> NotDirty NotDirty -> Dirty } } } -
sealed class EditorFragment { var modelState = ModelState.NotDirty fun commitModel()
{} fun onChangeText() { modelState.initModelStateTransition() } open class OwnerTextField: EditorFragment() {} open class CatTextField: EditorFragment() {} open class TimeTextField: EditorFragment() {} class SaveButton(var toggle: Toggle = Toggle.Disable): EditorFragment() { fun toggleButtonOnDirtyModel(model: ModelState) { toggle.enableOnDirty(model) } fun saveModel() { if (toggle.isButtonEnabled()) commitModel() } } } -
sealed class EditorFragment { var modelState = ModelState.NotDirty fun commitModel()
{} fun onChangeText() { modelState.initModelStateTransition() } open class OwnerTextField: EditorFragment() {} open class CatTextField: EditorFragment() {} open class TimeTextField: EditorFragment() {} class SaveButton(var toggle: Toggle = Toggle.Disable): EditorFragment() { fun toggleButtonOnDirtyModel(model: ModelState) { toggle.enableOnDirty(model) } fun saveModel() { if (toggle.isButtonEnabled()) commitModel() } } } -
Specifying UIs Formally through Metaprogramming Benefits: • Implicit: Clearly expressing
ourselves leads to a better understanding of the system • Explicit: Using formal specs to check properties - does our UI have any dead ends? Can we get locked out of a chunk of the UI? -
Promising discoveries in metaprogramming analysis research • Metaprogramming locator mechanism
is more accurate/less intensive detecting UI components in applications than current AI locator mechanisms • Current AI image classification locator mechanisms suffer from partial view render recognition, detecting custom object interactions -
Promising discoveries in metaprogramming analysis research • Using Kotlin metaprogramming
to further analyze the relationship of UI hierarchies, functional composition, and generated test results • Potential for generating unit testing in the future • Traditional machine learning only offers immediate extrinsic reward analysis; metaprogramming generated relation analysis may provide more in-depth analysis to supplement future deep learning -
Future Plans for TornadoFX-Suite • DSLs for more flexible test
building + abstracted compositional mapping • Make AST parsing stronger + pull out into an independent library • Expand TornadoFX-Suite to analyze other UI frameworks • Collect voluntary data from future users to begin deep learning -
• TornadoFX-Suite: https://github.com/ahinchman1/TornadoFX-Suite • Automata UI research: https://github.com/ahinchman1/Finite-State-Machine-Crash-Course • Data
science research: https://github.com/ahinchman1/Data-Science-Crash-Course • Neighborhood Cat Scheduler: https://github.com/Kotlin-Thursdays/Neighborhood-Cat-Scheduler • TornadoFX-DnD-TilesFX: https://github.com/ahinchman1/TornadoFX-DnD-TilesFX Amanda Hinchman-Dominguez @hinchman_amanda Relevant Links