Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update current flank flags #88

Merged
merged 13 commits into from May 8, 2020

Conversation

pawelpasterz
Copy link
Contributor

New flags added

  • runTimeout
  • ignoreFailedTests
  • disableSharding
  • smartFlankDisableUpload
  • testRunnerClass
  • localResultsDir
  • numUniformShards
  • clientDetails
  • testTargetsAlwaysRun
  • otherFiles
  • networkProfile

@pawelpasterz
Copy link
Contributor Author

Hey @runningcode , please take a look and let me know what do you think. Thanks!

Copy link
Owner

@runningcode runningcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! thanks for the update. I just had some questions on the documentation but otherwise looks great.
You can also update the CHANGELOG.md if you please and add a link to your contribution and github profile.

assertTrue(properties.contains(" network-profile: LTE"))
}

private fun emptyExtension(block: FlankGradleExtension.() -> Unit) = FlankGradleExtension(project).apply(block)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice extension function! :)

@@ -24,5 +24,16 @@ data class FladleConfigImpl(
override var resultsBucket: String? = null,
override var keepFilePath: Boolean = false,
override var resultsDir: String?,
override var additionalTestApks: Map<String, List<String>> = emptyMap()
override var additionalTestApks: Map<String, List<String>> = emptyMap(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just realized this is bad design on my part that we have to specify these twice :(. i'll see if i can clean this up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about small refactor but decided to keep it separate from each other. I'd like to contribute more into fladle and next PR would be probably refactor of some pieces of code (if I may ofc)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be lovely. and yes, its a good idea to keep these PRs separate.

README.md Outdated
@@ -184,6 +203,38 @@ Keeps the full path of downloaded files from a Google Cloud Storage bucket. Requ
### resultsDir
The name of a unique Google Cloud Storage object within the results bucket where raw test results will be stored. The default is a timestamp with a random suffix.

### runTimeout
The max time this test run can execute before it is cancelled. s (seconds), m (minutes), h (hours) suffixes are acceptable (default: unlimited).
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a little example here? is it like 1h2m2s or 1h 2m 2s? or just 1h?

README.md Outdated Show resolved Hide resolved
Disables sharding. Useful for parameterized tests. (default: false)

### smartFlankDisableUpload
Disables smart flank JUnit XML uploading. Useful for preventing timing data from being updated. (default: false)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "smart flank"? Can we link to docs in flank here?

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
A list of device-path: file-path pairs that indicate the device paths to push files to the device before starting tests, and the paths of files to push. Device paths must be under absolute, whitelisted paths (${EXTERNAL_STORAGE}, or ${ANDROID_DATA}/local/tmp). Source file paths may be in the local filesystem or in Google Cloud Storage (gs://…).

### networkProfile
The name of the network traffic profile, for example LTE, HSPA, etc, which consists of a set of parameters to emulate network conditions when running the test (default: no network shaping; see available profiles listed by the `flank test network-profiles list` command). This feature only works on physical devices.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, i like the examples and the command to fetch other supported values.

@pawelpasterz
Copy link
Contributor Author

Hey @runningcode thanks for review and comments. I implemented changes, please verify. Thanks!

Copy link
Owner

@runningcode runningcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I'm going to wait until an updated version of Flank is released with these features before merging, is that okay? Otherwise people might look at the readme and try to use features that don't work with any released version.

README.md Outdated
@@ -184,6 +203,44 @@ Keeps the full path of downloaded files from a Google Cloud Storage bucket. Requ
### resultsDir
The name of a unique Google Cloud Storage object within the results bucket where raw test results will be stored. The default is a timestamp with a random suffix.

### runTimeout
The max time this test run can execute before it is cancelled. s (seconds), m (minutes), h (hours) suffixes are acceptable, mixes like 1h45m are currently not supported (default: unlimited).
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


### smartFlankDisableUpload
Disables smart flank JUnit XML uploading. Useful for preventing timing data from being updated. (default: false)
[What is Smart Flank?](https://github.com/Flank/flank/blob/master/docs/smart_flank.md)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@pawelpasterz
Copy link
Contributor Author

pawelpasterz commented Apr 16, 2020

Nice work! I'm going to wait until an updated version of Flank is released with these features before merging, is that okay? Otherwise people might look at the readme and try to use features that don't work with any released version.

Definitely, I will keep this PR updated if any new flag will appear in flank

@runningcode
Copy link
Owner

Hey! I was looking in to merging this, but when testing with Flank 8.1.0 i ran in to this issue which caused flank to hang:

com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class ftl.reports.xml.model.JUnitTestSuite] value failed for JSON property hostname due to missing (therefore NULL) value for creator parameter hostname which is a non-nullable type
 at [Source: (byte[])"<?xml version='1.0' encoding='UTF-8' ?>
<testsuites>
  <testsuite name="Nexus5-23-en-portrait" tests="1" failures="0" flakes="0" errors="0" skipped="0" time="2.594" timestamp="2020-04-23T20:40:34">
    <testcase name="seeView" classname="com.osacky.flank.gradle.sample.ExampleInstrumentedTest" time="2.594"/>
  </testsuite>
  <testsuite name="Pixel2-26-en-portrait" tests="1" failures="0" flakes="0" errors="0" skipped="0" time="2.712" timestamp="2020-04-23T20:40:14">
    <testcase name="seeView" cl"[truncated 109 bytes]; line: 5, column: 3] (through reference chain: ftl.reports.xml.model.JUnitTestResult["testsuite"]->java.util.ArrayList[0]->ftl.reports.xml.model.JUnitTestSuite["hostname"])
        at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:107)
        at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:198)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:488)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
        at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:114)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
        at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:417)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
        at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:114)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3266)
        at ftl.reports.xml.JUnitXmlKt.parseAllSuitesXml(JUnitXml.kt:50)
        at ftl.reports.xml.JUnitXmlKt.parseAllSuitesXml(JUnitXml.kt:54)
        at ftl.gc.GcStorage.downloadJunitXml(GcStorage.kt:94)
        at ftl.args.ArgsHelper.calculateShards(ArgsHelper.kt:222)
        at ftl.args.AndroidTestShard.getTestShardChunks(AndroidTestShard.kt:26)
        at ftl.run.AndroidTestRunner$runTests$2.invokeSuspend(AndroidTestRunner.kt:38)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.ResumeModeKt.resumeUninterceptedMode(ResumeMode.kt:45)
        at kotlinx.coroutines.internal.ScopeCoroutine.afterCompletionInternal(Scopes.kt:32)
        at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:310)
        at kotlinx.coroutines.JobSupport.tryFinalizeFinishingState(JobSupport.kt:236)
        at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:849)
        at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:811)
        at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:787)
        at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:111)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:270)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at ftl.cli.firebase.test.android.AndroidRunCommand.run(AndroidRunCommand.kt:49)
        at picocli.CommandLine.executeUserObject(CommandLine.java:1729)
        at picocli.CommandLine.access$900(CommandLine.java:145)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2101)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2068)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:1935)
        at picocli.CommandLine.execute(CommandLine.java:1864)
        at ftl.Main$Companion.main(Main.kt:41)
        at ftl.Main.main(Main.kt)

Are we adding a flag that doesn't work with the old version of flank?

@@ -25,5 +25,16 @@ data class FladleConfigImpl(
override var resultsBucket: String?,
override var keepFilePath: Boolean,
override var resultsDir: String?,
override var additionalTestApks: Map<String, List<String>>
override var additionalTestApks: Map<String, List<String>>,
override var runTimeout: String? = null,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, i updated the rest of this class, but you can remove the redundant default specification from this file!

@pawelpasterz
Copy link
Contributor Author

Hey! I was looking in to merging this, but when testing with Flank 8.1.0 i ran in to this issue which caused flank to hang:

com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class ftl.reports.xml.model.JUnitTestSuite] value failed for JSON property hostname due to missing (therefore NULL) value for creator parameter hostname which is a non-nullable type
 at [Source: (byte[])"<?xml version='1.0' encoding='UTF-8' ?>
<testsuites>
  <testsuite name="Nexus5-23-en-portrait" tests="1" failures="0" flakes="0" errors="0" skipped="0" time="2.594" timestamp="2020-04-23T20:40:34">
    <testcase name="seeView" classname="com.osacky.flank.gradle.sample.ExampleInstrumentedTest" time="2.594"/>
  </testsuite>
  <testsuite name="Pixel2-26-en-portrait" tests="1" failures="0" flakes="0" errors="0" skipped="0" time="2.712" timestamp="2020-04-23T20:40:14">
    <testcase name="seeView" cl"[truncated 109 bytes]; line: 5, column: 3] (through reference chain: ftl.reports.xml.model.JUnitTestResult["testsuite"]->java.util.ArrayList[0]->ftl.reports.xml.model.JUnitTestSuite["hostname"])
        at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:107)
        at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:198)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:488)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
        at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:114)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
        at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:417)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
        at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:114)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3266)
        at ftl.reports.xml.JUnitXmlKt.parseAllSuitesXml(JUnitXml.kt:50)
        at ftl.reports.xml.JUnitXmlKt.parseAllSuitesXml(JUnitXml.kt:54)
        at ftl.gc.GcStorage.downloadJunitXml(GcStorage.kt:94)
        at ftl.args.ArgsHelper.calculateShards(ArgsHelper.kt:222)
        at ftl.args.AndroidTestShard.getTestShardChunks(AndroidTestShard.kt:26)
        at ftl.run.AndroidTestRunner$runTests$2.invokeSuspend(AndroidTestRunner.kt:38)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.ResumeModeKt.resumeUninterceptedMode(ResumeMode.kt:45)
        at kotlinx.coroutines.internal.ScopeCoroutine.afterCompletionInternal(Scopes.kt:32)
        at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:310)
        at kotlinx.coroutines.JobSupport.tryFinalizeFinishingState(JobSupport.kt:236)
        at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:849)
        at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:811)
        at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:787)
        at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:111)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:270)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at ftl.cli.firebase.test.android.AndroidRunCommand.run(AndroidRunCommand.kt:49)
        at picocli.CommandLine.executeUserObject(CommandLine.java:1729)
        at picocli.CommandLine.access$900(CommandLine.java:145)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2101)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2068)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:1935)
        at picocli.CommandLine.execute(CommandLine.java:1864)
        at ftl.Main$Companion.main(Main.kt:41)
        at ftl.Main.main(Main.kt)

Are we adding a flag that doesn't work with the old version of flank?

Yes, there are few flags there are not supported in flank 8.1.0. I'll update README with that information

Pawel Pasterz and others added 10 commits April 28, 2020 23:05
Co-Authored-By: Nelson Osacky <nelson@osacky.com>
Co-Authored-By: Nelson Osacky <nelson@osacky.com>
Co-Authored-By: Nelson Osacky <nelson@osacky.com>
Co-Authored-By: Nelson Osacky <nelson@osacky.com>
Co-Authored-By: Nelson Osacky <nelson@osacky.com>
* Each key should be the Android resource name of a target UI element and each value should be the text input for that element.
* Values are only permitted for text type elements, so no value should be specified for click and ignore type elements.
*/
var roboDirectives: List<List<String>>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh. do we want this to be a List<Triple>?
Or is it better represented as Map<String, List<String>>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that but I wanted to allow user to skip 3rd parameter.

roboDirectives = [
      ["click", "button1", ""],
      ["ignore", "button2"],  <-- 3rd skipped
      ["text", "field1", "my text"],
    ]

Triple will force user to always provide three strings. But I am open to suggestions

if (config.roboDirectives.isNotEmpty()) {
appendln(" robo-directives:")
config.roboDirectives.forEach {
val value = it.getOrNull(2).let { stringValue -> if (stringValue.isNullOrBlank()) "\"\"" else stringValue }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it.getOrNull(2) returns null, does it crash on .let? should it be ?.let?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting fact it worked! But I think you are right. I'll change it to be more robust

@runningcode
Copy link
Owner

Looking good. I'll test this tomorrow now that I figured out the issue that was causing the crash for me!

Also, I won't block this PR since it already is quite large, but I just wanted to also give you a heads up: From now on, I would like new extensions in fladle to use Gradle's lazy property API. This will allow for lazy evaluation and faster configuration times.

I started doing some migration here: f92bd12#diff-670e340c5b646cd261359b8466643022R40

Anyways, don't worry about it. I'll do the migration for you once I merge this PR.

Let's figure out that robo directives api first!

@bootstraponline
Copy link

@pawelpasterz now that flank is released, let's double check all the flags are supported.

I think additional-apks might be missing? Flank/flank#695

@runningcode
Copy link
Owner

Hey, I just tested this out. I'm going to merge to master and release a snapshot! Thanks!

@runningcode runningcode merged commit 684917f into runningcode:master May 8, 2020
@andrey-bolduzev
Copy link

Hey @runningcode, have you had a chance to push these exciting changes to a snapshot release? I don't think 0.9.2-SNAPSHOT includes them. Can't wait to try! Thank you.

@runningcode
Copy link
Owner

runningcode commented May 11, 2020

There may be an older 0.9.2-SNAPSHOT being pulled from the Gradle Plugin Repository overriding the one from the sonatype snaphots repo. Can you put the sonatype snapshots repo above the gradle plugin repository? Or add the following beneath the snapshots repository declaration?

mavenContent {
  snapshotsOnly()
}

More info here: https://docs.gradle.org/current/userguide/declaring_repositories.html#maven_repository_filtering

@runningcode
Copy link
Owner

Hey, I've deleted the 0.9.2-SNAPSHOT from the Gradle Plugins portal. Please re-run using --refresh-dependencies and it should work. Let me know if you run in to any trouble.

@andrey-bolduzev
Copy link

andrey-bolduzev commented May 11, 2020

Hey, I've deleted the 0.9.2-SNAPSHOT from the Gradle Plugins portal. Please re-run using --refresh-dependencies and it should work. Let me know if you run in to any trouble.

To be honest I'm having trouble figuring out the correct coordinates for the sonatype snapshots repository. I think it's https://oss.sonatype.org/service/local/repositories/snapshots/content/ which I added to the settings.gradle.kts's pluginManagement (because the project is using the new plugins API + Kotlin DSL), but then the plugin with this id cannot be found there, so I suspect there's some extra mapping needed to be done. Would it be possible to upload the latest snapshot to the main gradle plugin repository? Or I could wait until the next regular release.

@runningcode
Copy link
Owner

I believe the URL is this: https://oss.sonatype.org/content/repositories/snapshots

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants