Skip to content

Commit 46e5cce

Browse files
authored
Adding UI tests for Dynamo DB feature (#2788)
* Changing dynamo feature to be controlled by a registry key * Adding UI tests for Dynamo DB
1 parent 6e9279a commit 46e5cce

File tree

9 files changed

+234
-4
lines changed

9 files changed

+234
-4
lines changed

buildSrc/src/main/kotlin/toolkit-intellij-subplugin.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ tasks.withType<RunIdeForUiTestTask>().all {
133133

134134
// These are experiments to enable for UI tests
135135
systemProperty("aws.feature.connectedLocalTerminal", true)
136+
systemProperty("aws.feature.dynamoDb", true)
136137
ciOnly {
137138
systemProperty("aws.sharedCredentialsFile", "/tmp/.aws/credentials")
138139
}
@@ -144,6 +145,8 @@ tasks.withType<RunIdeForUiTestTask>().all {
144145

145146
configure<JacocoTaskExtension> {
146147
includes = listOf("software.aws.toolkits.*")
147-
output = Output.TCP_CLIENT // Dump to our jacoco server instead of to a file
148+
ciOnly {
149+
output = Output.TCP_CLIENT // Dump to our jacoco server instead of to a file
150+
}
148151
}
149152
}

jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ with what features/services are supported.
235235
<registryKey key="aws.debuggerAttach.timeout" description="Time allowed for debuggers to attach before timing out (ms)" restartRequired="false" defaultValue="60000"/>
236236
<registryKey key="aws.feature.ecsCloudDebug" description="Enables the Cloud Debug for ECS set of features" restartRequired="false" defaultValue="true"/>
237237
<registryKey key="aws.feature.ecsExec" description="Enables the features and actions associated with ECS Execute Command" restartRequired="false" defaultValue="false"/>
238+
<registryKey key="aws.feature.dynamoDb" description="Enables the features and actions associated with DynamoDB" restartRequired="false" defaultValue="false"/>
238239
</extensions>
239240

240241
<extensions defaultExtensionNs="JavaScript.JsonSchema">
@@ -258,7 +259,7 @@ with what features/services are supported.
258259
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.AppRunnerExplorerRootNode"/>
259260
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.CloudFormationExplorerRootNode"/>
260261
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.CloudWatchRootNode"/>
261-
<!-- <explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.DynamoDbExplorerRootNode"/> -->
262+
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.DynamoDbExplorerRootNode"/>
262263
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.LambdaExplorerRootNode"/>
263264
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.S3ExplorerRootNode"/>
264265
<explorer.serviceNode implementation="software.aws.toolkits.jetbrains.core.explorer.nodes.EcrExplorerRootNode"/>

jetbrains-core/src/software/aws/toolkits/jetbrains/AwsToolkit.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ object AwsToolkit {
1717
fun isCloudDebugEnabled() = Registry.`is`("aws.feature.ecsCloudDebug")
1818

1919
fun isEcsExecEnabled() = Registry.`is`("aws.feature.ecsExec")
20+
21+
fun isDynamoDbEnabled() = Registry.`is`("aws.feature.dynamoDb")
2022
}

jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/nodes/AwsExplorerRootNode.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class AwsExplorerRootNode(private val nodeProject: Project) : AbstractTreeNode<A
2020
val regionProvider = AwsRegionProvider.getInstance()
2121

2222
return EP_NAME.extensionList
23-
.filter { regionProvider.isServiceSupported(region, it.serviceId) }
23+
.filter { it.enabled() && regionProvider.isServiceSupported(region, it.serviceId) }
2424
.map { it.buildServiceRootNode(nodeProject) }
2525
}
2626

jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/nodes/AwsExplorerServiceNode.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ import com.intellij.openapi.project.Project
88
interface AwsExplorerServiceNode {
99
val serviceId: String
1010
fun buildServiceRootNode(project: Project): AwsExplorerServiceRootNode
11+
fun enabled(): Boolean = true
1112
}

jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/nodes/DynamoDbExplorerRootNode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package software.aws.toolkits.jetbrains.core.explorer.nodes
55

66
import com.intellij.openapi.project.Project
77
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
8+
import software.aws.toolkits.jetbrains.AwsToolkit
89
import software.aws.toolkits.jetbrains.services.dynamodb.explorer.DynamoDbServiceNode
910

1011
class DynamoDbExplorerRootNode : AwsExplorerServiceNode {
1112
override val serviceId: String = DynamoDbClient.SERVICE_NAME
1213
override fun buildServiceRootNode(project: Project) = DynamoDbServiceNode(project, this)
14+
override fun enabled() = AwsToolkit.isDynamoDbEnabled()
1315
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/dynamodb/editor/TableResults.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import javax.swing.table.DefaultTableCellRenderer
2323

2424
class TableResults : JBTable(TableModel(BidirectionalMap(), emptyList())) {
2525
init {
26-
autoResizeMode = AUTO_RESIZE_OFF
26+
autoResizeMode = AUTO_RESIZE_ALL_COLUMNS
2727
cellSelectionEnabled = true
2828

2929
font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN)

ui-tests/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ plugins {
2323
dependencies {
2424
testImplementation(gradleApi())
2525
testImplementation(project(":core"))
26+
testImplementation(project(path = ":core", configuration = "testArtifacts"))
2627
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
2728
testImplementation(project(":resources"))
2829
testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version")
@@ -32,6 +33,7 @@ dependencies {
3233
testImplementation("software.amazon.awssdk:cloudformation:$awsSdkVersion")
3334
testImplementation("software.amazon.awssdk:cloudwatchlogs:$awsSdkVersion")
3435
testImplementation("software.amazon.awssdk:s3:$awsSdkVersion")
36+
testImplementation("software.amazon.awssdk:dynamodb:$awsSdkVersion")
3537
testImplementation("software.amazon.awssdk:sns:$awsSdkVersion")
3638
testImplementation("software.amazon.awssdk:sqs:$awsSdkVersion")
3739

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.uitests.tests
5+
6+
import com.intellij.remoterobot.fixtures.ComboBoxFixture
7+
import com.intellij.remoterobot.search.locators.byXpath
8+
import com.intellij.remoterobot.stepsProcessing.log
9+
import com.intellij.remoterobot.stepsProcessing.step
10+
import com.intellij.remoterobot.utils.waitFor
11+
import com.intellij.remoterobot.utils.waitForIgnoringError
12+
import org.assertj.core.api.Assertions.assertThat
13+
import org.junit.jupiter.api.AfterAll
14+
import org.junit.jupiter.api.BeforeAll
15+
import org.junit.jupiter.api.Test
16+
import org.junit.jupiter.api.TestInstance
17+
import org.junit.jupiter.api.io.TempDir
18+
import software.amazon.awssdk.regions.Region
19+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
20+
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition
21+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
22+
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement
23+
import software.amazon.awssdk.services.dynamodb.model.KeyType
24+
import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex
25+
import software.amazon.awssdk.services.dynamodb.model.ProjectionType
26+
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType
27+
import software.aws.toolkits.core.utils.test.aString
28+
import software.aws.toolkits.jetbrains.uitests.CoreTest
29+
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
30+
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
31+
import software.aws.toolkits.jetbrains.uitests.fixtures.findByXpath
32+
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
33+
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
34+
import java.nio.file.Path
35+
import java.time.LocalDate
36+
import java.time.format.DateTimeFormatter
37+
import java.util.UUID
38+
import kotlin.random.Random
39+
40+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
41+
class DynamoDbTest {
42+
43+
private val date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
44+
private val tableName = "uitest-$date-${UUID.randomUUID()}"
45+
private val dynamo = "DynamoDB"
46+
private val otherIndex = "OtherIndex"
47+
48+
private lateinit var client: DynamoDbClient
49+
private lateinit var items: List<AllTypesType>
50+
51+
@TempDir
52+
lateinit var tempDir: Path
53+
54+
@BeforeAll
55+
fun setUp() {
56+
log.info("Creating table $tableName")
57+
client = DynamoDbClient.builder().region(Region.US_WEST_2).build()
58+
createAndPopulateTable()
59+
log.info("Table created")
60+
}
61+
62+
@AfterAll
63+
fun cleanup() {
64+
log.info("Running final cleanup")
65+
client.deleteTable {
66+
it.tableName(tableName)
67+
}
68+
client.waiter().waitUntilTableNotExists { it.tableName(tableName) }
69+
log.info("Deleted table $tableName")
70+
}
71+
72+
@Test
73+
@CoreTest
74+
fun testDynamoDbTableViewer() = uiTest {
75+
welcomeFrame {
76+
openFolder(tempDir)
77+
}
78+
79+
idea {
80+
waitForBackgroundTasks()
81+
82+
step("Open table") {
83+
awsExplorer {
84+
expandExplorerNode(dynamo)
85+
doubleClickExplorer(dynamo, tableName)
86+
}
87+
88+
step("Check all data loaded") {
89+
step("Wait for data to load") {
90+
waitFor { findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }.isNotEmpty() }
91+
}
92+
step("Check full table scan (default index)") {
93+
val columnCount = step("Check columns") {
94+
val cols = findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }
95+
// Can't do a straight-up string match because the column headers may be truncated for space
96+
assertThat(cols).satisfiesExactlyInAnyOrder(
97+
{ assertThat(it).startsWith("Num") },
98+
{ assertThat(it).startsWith("Str") },
99+
{ assertThat(it).startsWith("Ter") },
100+
{ assertThat(it).startsWith("Boo") },
101+
{ assertThat(it).startsWith("Int") },
102+
{ assertThat(it).startsWith("Str") },
103+
{ assertThat(it).startsWith("Num") },
104+
{ assertThat(it).startsWith("Map") },
105+
{ assertThat(it).startsWith("Str") },
106+
{ assertThat(it).startsWith("Num") }
107+
)
108+
cols.size
109+
}
110+
step("Check data") {
111+
val table = findByXpath("//div[@class='TableResults']").retrieveData().textDataList
112+
val rowCount = table.size / columnCount
113+
assertThat(rowCount).isEqualTo(items.size)
114+
}
115+
}
116+
step("Select secondary local index $otherIndex") {
117+
step("Expand scan panel") {
118+
jLabel("Scan").click()
119+
}
120+
step("Select and run $otherIndex") {
121+
find<ComboBoxFixture>(byXpath("//div[@class='ComboBox']")).selectItemContains(otherIndex)
122+
button("Run").click()
123+
}
124+
}
125+
step("Check only cols in $otherIndex") {
126+
waitForIgnoringError {
127+
assertThat(findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }).hasSize(3)
128+
true
129+
}
130+
}
131+
}
132+
}
133+
}
134+
}
135+
136+
private fun createAndPopulateTable() {
137+
client.createTable { table ->
138+
table.tableName(tableName)
139+
table.attributeDefinitions(
140+
AttributeDefinition.builder().attributeName("NumericId").attributeType(ScalarAttributeType.N).build(),
141+
AttributeDefinition.builder().attributeName("StringSecondary").attributeType(ScalarAttributeType.S)
142+
.build(),
143+
AttributeDefinition.builder().attributeName("TertiaryColumn").attributeType(ScalarAttributeType.S)
144+
.build(),
145+
)
146+
table.keySchema(
147+
KeySchemaElement.builder().attributeName("NumericId").keyType(KeyType.HASH).build(),
148+
KeySchemaElement.builder().attributeName("StringSecondary").keyType(KeyType.RANGE).build()
149+
)
150+
table.localSecondaryIndexes(
151+
LocalSecondaryIndex.builder().indexName(otherIndex).keySchema(
152+
KeySchemaElement.builder().attributeName("NumericId").keyType(KeyType.HASH).build(),
153+
KeySchemaElement.builder().attributeName("TertiaryColumn").keyType(KeyType.RANGE).build()
154+
).projection {
155+
it.projectionType(ProjectionType.KEYS_ONLY)
156+
}.build()
157+
)
158+
table.provisionedThroughput {
159+
it.readCapacityUnits(10)
160+
it.writeCapacityUnits(5)
161+
}
162+
}
163+
164+
client.waiter().waitUntilTableExists { it.tableName(tableName) }
165+
166+
items = randomRange().map { AllTypesType() }
167+
168+
items.forEach { item ->
169+
client.putItem {
170+
it.tableName(tableName)
171+
it.item(
172+
mapOf(
173+
"NumericId" to attr { n(item.id.toString()) },
174+
"StringSecondary" to attr { s(item.secondary) },
175+
"TertiaryColumn" to attr { s(item.tertiary) },
176+
"Bool" to attr { bool(item.booleanProperty) },
177+
"Int" to attr { n(item.intProperty.toString()) },
178+
"StringMap" to attr { m(item.stringMapProperty.mapValues { v -> attr { s(v.value) } }) },
179+
"NumberMap" to attr { m(item.numberMapProperty.mapValues { v -> attr { n(v.value.toString()) } }) },
180+
"MapOfMap" to attr {
181+
m(
182+
item.mapOfMapProperty.mapValues { v ->
183+
attr {
184+
m(
185+
v.value.mapValues { sv ->
186+
attr {
187+
s(sv.value)
188+
}
189+
}
190+
)
191+
}
192+
}
193+
)
194+
},
195+
"StringList" to attr { ss(item.stringList) },
196+
"NumberList" to attr { ns(item.numberList.map { n -> n.toString() }) }
197+
)
198+
)
199+
}
200+
}
201+
}
202+
203+
private fun attr(block: AttributeValue.Builder.() -> Unit): AttributeValue = AttributeValue.builder().apply(block).build()
204+
205+
private data class AllTypesType(
206+
val id: Long = Random.nextLong(0, Long.MAX_VALUE),
207+
val secondary: String = aString(),
208+
val tertiary: String = aString(),
209+
val booleanProperty: Boolean = Random.nextBoolean(),
210+
val intProperty: Int = Random.nextInt(),
211+
val stringMapProperty: Map<String, String> = randomRange().associate { aString() to aString() },
212+
val numberMapProperty: Map<String, Long> = randomRange().associate { aString() to Random.nextLong() },
213+
val mapOfMapProperty: Map<String, Map<String, String>> = randomRange().associate { aString() to randomRange().associate { aString() to aString() } },
214+
val stringList: List<String> = randomRange().map { aString() },
215+
val numberList: List<Long> = randomRange().map { Random.nextLong() }
216+
)
217+
}
218+
219+
private fun randomRange() = 0..Random.nextInt(1, 10)

0 commit comments

Comments
 (0)