From cf54fa973dc95189d2fa044347952671e79c3dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCrten?= <sebastian@mobanisto.de> Date: Fri, 1 Oct 2021 14:18:13 +0200 Subject: [PATCH] Create DeterministicTestDataCreator based on TestDataCreator --- .../briar/desktop/BriarDesktopTestApp.kt | 3 + .../briar/desktop/DesktopTestModule.kt | 8 + .../briar/desktop/TestWithTemporaryAccount.kt | 3 +- .../testdata/DeterministicTestDataCreator.kt | 22 ++ .../DeterministicTestDataCreatorImpl.kt | 363 ++++++++++++++++++ 5 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreator.kt create mode 100644 src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt diff --git a/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt b/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt index 17584c5998..dd1f8ca9f4 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.lifecycle.ShutdownManager import org.briarproject.briar.BriarCoreEagerSingletons import org.briarproject.briar.BriarCoreModule import org.briarproject.briar.api.test.TestDataCreator +import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator import java.security.SecureRandom import javax.inject.Singleton @@ -33,4 +34,6 @@ internal interface BriarDesktopTestApp : BrambleCoreEagerSingletons, BriarCoreEa fun getAccountManager(): AccountManager fun getTestDataCreator(): TestDataCreator + + fun getDeterministicTestDataCreator(): DeterministicTestDataCreator } diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt index b0e9afdb24..5fc383a2cf 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt @@ -25,6 +25,8 @@ import org.briarproject.bramble.system.JavaSystemModule import org.briarproject.bramble.util.OsUtils.isLinux import org.briarproject.bramble.util.OsUtils.isMac import org.briarproject.briar.api.test.TestAvatarCreator +import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator +import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl import org.briarproject.briar.test.TestModule import java.io.File import java.util.Collections.emptyList @@ -93,4 +95,10 @@ internal class DesktopTestModule(private val appDir: File) { @Provides internal fun provideTestAvatarCreator() = TestAvatarCreator { null } + + @Provides + @Singleton + internal fun getDeterministicTestDataCreator(testDataCreator: DeterministicTestDataCreatorImpl): DeterministicTestDataCreator { + return testDataCreator + } } diff --git a/src/test/kotlin/org/briarproject/briar/desktop/TestWithTemporaryAccount.kt b/src/test/kotlin/org/briarproject/briar/desktop/TestWithTemporaryAccount.kt index ac5bb5cd3b..90671b19ca 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/TestWithTemporaryAccount.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/TestWithTemporaryAccount.kt @@ -54,7 +54,8 @@ private class TestWithTemporaryAccount { lifecycleManager.startServices(dbKey) lifecycleManager.waitForStartup() - app.getTestDataCreator().createTestData(10, 20, 50, 4, 4, 10) + app.getDeterministicTestDataCreator().createTestData(5, 20, 50) + app.getTestDataCreator().createTestData(5, 20, 50, 4, 4, 10) // Creating test data happens on a background thread. As we do not get notified about updates to the conact // list yet, we need to wait a moment in order for that to finish (hopefully). diff --git a/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreator.kt b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreator.kt new file mode 100644 index 0000000000..af242ba5d3 --- /dev/null +++ b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreator.kt @@ -0,0 +1,22 @@ +package org.briarproject.briar.desktop.testdata + +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.bramble.api.db.DbException +import org.briarproject.bramble.api.lifecycle.IoExecutor + +interface DeterministicTestDataCreator { + /** + * Create fake test data on the IoExecutor + * + * @param numContacts Number of contacts to create. Must be >= 1 + * @param numPrivateMsgs Number of private messages to create for each + * contact. + * @param avatarPercent Percentage of contacts + * that will use a random profile image. Between 0 and 100. + */ + fun createTestData(numContacts: Int, numPrivateMsgs: Int, avatarPercent: Int) + + @IoExecutor + @Throws(DbException::class) + fun addContact(name: String?, alias: Boolean, avatar: Boolean): Contact? +} diff --git a/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt new file mode 100644 index 0000000000..72cdd1d450 --- /dev/null +++ b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt @@ -0,0 +1,363 @@ +package org.briarproject.briar.desktop.testdata + +import org.briarproject.bramble.api.FormatException +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.crypto.SecretKey +import org.briarproject.bramble.api.db.DatabaseComponent +import org.briarproject.bramble.api.db.DbException +import org.briarproject.bramble.api.db.Transaction +import org.briarproject.bramble.api.identity.AuthorFactory +import org.briarproject.bramble.api.identity.AuthorId +import org.briarproject.bramble.api.identity.IdentityManager +import org.briarproject.bramble.api.identity.LocalAuthor +import org.briarproject.bramble.api.lifecycle.IoExecutor +import org.briarproject.bramble.api.plugin.BluetoothConstants +import org.briarproject.bramble.api.plugin.LanTcpConstants +import org.briarproject.bramble.api.plugin.TorConstants +import org.briarproject.bramble.api.plugin.TransportId +import org.briarproject.bramble.api.properties.TransportProperties +import org.briarproject.bramble.api.properties.TransportPropertyManager +import org.briarproject.bramble.api.sync.Group +import org.briarproject.bramble.api.sync.GroupFactory +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.Message +import org.briarproject.bramble.api.system.Clock +import org.briarproject.bramble.util.LogUtils +import org.briarproject.bramble.util.StringUtils +import org.briarproject.briar.api.autodelete.AutoDeleteConstants +import org.briarproject.briar.api.avatar.AvatarManager +import org.briarproject.briar.api.avatar.AvatarMessageEncoder +import org.briarproject.briar.api.messaging.MessagingManager +import org.briarproject.briar.api.messaging.PrivateMessageFactory +import org.briarproject.briar.api.test.TestAvatarCreator +import org.briarproject.briar.test.TestData +import java.io.IOException +import java.io.InputStream +import java.util.Random +import java.util.UUID +import java.util.concurrent.Executor +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class DeterministicTestDataCreatorImpl @Inject internal constructor( + private val authorFactory: AuthorFactory, private val clock: Clock, + private val groupFactory: GroupFactory, + private val privateMessageFactory: PrivateMessageFactory, + private val db: DatabaseComponent, + private val identityManager: IdentityManager, private val contactManager: ContactManager, + private val transportPropertyManager: TransportPropertyManager, + private val messagingManager: MessagingManager, + private val testAvatarCreator: TestAvatarCreator, + private val avatarMessageEncoder: AvatarMessageEncoder, + @field:IoExecutor @param:IoExecutor private val ioExecutor: Executor +) : DeterministicTestDataCreator { + private val LOG = Logger.getLogger(DeterministicTestDataCreatorImpl::class.java.name) + private val random = Random() + private val localAuthors: MutableMap<Contact, LocalAuthor> = HashMap() + override fun createTestData( + numContacts: Int, numPrivateMsgs: Int, + avatarPercent: Int + ) { + require(numContacts != 0) + require(!(avatarPercent < 0 || avatarPercent > 100)) + ioExecutor.execute { + try { + createTestDataOnIoExecutor( + numContacts, numPrivateMsgs, + avatarPercent + ) + } catch (e: DbException) { + LogUtils.logException(LOG, Level.WARNING, e) + } + } + } + + @IoExecutor + @Throws(DbException::class) + private fun createTestDataOnIoExecutor( + numContacts: Int, numPrivateMsgs: Int, + avatarPercent: Int + ) { + val contacts = createContacts(numContacts, avatarPercent) + createPrivateMessages(contacts, numPrivateMsgs) + } + + @Throws(DbException::class) + private fun createContacts(numContacts: Int, avatarPercent: Int): List<Contact> { + val contacts: MutableList<Contact> = ArrayList(numContacts) + val localAuthor = identityManager.localAuthor + for (i in 0 until numContacts) { + val remote = randomAuthor + val contact = addContact( + localAuthor.id, remote, + random.nextBoolean(), avatarPercent + ) + contacts.add(contact) + } + return contacts + } + + @Throws(DbException::class) + private fun addContact( + localAuthorId: AuthorId, remote: LocalAuthor, + alias: Boolean, avatarPercent: Int + ): Contact { + // prepare to add contact + val secretKey = secretKey + val timestamp = clock.currentTimeMillis() + val verified = random.nextBoolean() + + // prepare transport properties + val props = randomTransportProperties + val contact = db.transactionWithResult<Contact, RuntimeException>(false) { txn: Transaction? -> + val contactId = contactManager.addContact( + txn, remote, + localAuthorId, secretKey, timestamp, true, verified, true + ) + if (alias) { + contactManager.setContactAlias( + txn, contactId, + randomAuthorName + ) + } + transportPropertyManager.addRemoteProperties(txn, contactId, props) + db.getContact(txn, contactId) + } + if (random.nextInt(100) + 1 <= avatarPercent) addAvatar(contact) + if (LOG.isLoggable(Level.INFO)) { + LOG.info( + "Added contact " + remote.name + + " with transport properties: " + props + ) + } + localAuthors[contact] = remote + return contact + } + + @Throws(DbException::class) + override fun addContact(name: String?, alias: Boolean, avatar: Boolean): Contact? { + val localAuthor = identityManager.localAuthor + val remote = authorFactory.createLocalAuthor(name) + val avatarPercent = if (avatar) 100 else 0 + return addContact(localAuthor.id, remote, alias, avatarPercent) + } + + private val randomAuthorName: String + private get() { + val i = random.nextInt(TestData.AUTHOR_NAMES.size) + return TestData.AUTHOR_NAMES[i] + } + private val randomAuthor: LocalAuthor + private get() = authorFactory.createLocalAuthor(randomAuthorName) + private val secretKey: SecretKey + private get() { + val b = ByteArray(SecretKey.LENGTH) + random.nextBytes(b) + return SecretKey(b) + } + + // Bluetooth + private val randomTransportProperties: Map<TransportId, TransportProperties> + private get() { + val props: MutableMap<TransportId, TransportProperties> = HashMap() + // Bluetooth + val bt = TransportProperties() + val btAddress = randomBluetoothAddress + val uuid = randomUUID + bt[BluetoothConstants.PROP_ADDRESS] = btAddress + bt[BluetoothConstants.PROP_UUID] = uuid + props[BluetoothConstants.ID] = bt + + // LAN + val lan = TransportProperties() + val sb = StringBuilder() + for (i in 0..3) { + if (sb.length > 0) sb.append(',') + sb.append(randomLanAddress) + } + lan[LanTcpConstants.PROP_IP_PORTS] = sb.toString() + val port = randomPortNumber.toString() + lan[LanTcpConstants.PROP_PORT] = port + props[LanTcpConstants.ID] = lan + + // Tor + val tor = TransportProperties() + val torAddress = randomTorAddress + tor[TorConstants.PROP_ONION_V2] = torAddress + props[TorConstants.ID] = tor + return props + } + private val randomBluetoothAddress: String + private get() { + val mac = ByteArray(6) + random.nextBytes(mac) + val sb = StringBuilder(18) + for (b in mac) { + if (sb.length > 0) sb.append(":") + sb.append(String.format("%02X", b)) + } + return sb.toString() + } + private val randomUUID: String + private get() { + val uuid = ByteArray(BluetoothConstants.UUID_BYTES) + random.nextBytes(uuid) + return UUID.nameUUIDFromBytes(uuid).toString() + } + + // address + // port + private val randomLanAddress: String + private get() { + val sb = StringBuilder() + // address + if (random.nextInt(5) == 0) { + sb.append("10.") + sb.append(random.nextInt(2)).append('.') + } else { + sb.append("192.168.") + } + sb.append(random.nextInt(2)).append('.') + sb.append(random.nextInt(255)) + // port + sb.append(':').append(randomPortNumber) + return sb.toString() + } + private val randomPortNumber: Int + private get() = 32768 + random.nextInt(32768) + + // address + private val randomTorAddress: String + private get() { + val sb = StringBuilder() + // address + for (i in 0..15) { + if (random.nextBoolean()) sb.append(2 + random.nextInt(6)) else sb.append((random.nextInt(26) + 'a'.toInt()).toChar()) + } + return sb.toString() + } + + @Throws(DbException::class) + private fun addAvatar(c: Contact) { + val authorId = c.author.id + val groupId = groupFactory.createGroup( + AvatarManager.CLIENT_ID, + AvatarManager.MAJOR_VERSION, authorId.bytes + ).id + val `is`: InputStream? + `is` = try { + testAvatarCreator.avatarInputStream + } catch (e: IOException) { + LogUtils.logException(LOG, Level.WARNING, e) + return + } + if (`is` == null) return + val m: Message + m = try { + avatarMessageEncoder.encodeUpdateMessage( + groupId, 0, + "image/jpeg", `is` + ).first + } catch (e: IOException) { + throw DbException(e) + } + db.transaction<RuntimeException>(false) { txn: Transaction? -> + // TODO: Do this properly via clients without breaking encapsulation + db.setGroupVisibility(txn, c.id, groupId, Group.Visibility.SHARED) + db.receiveMessage(txn, c.id, m) + } + } + + // TODO: Do this properly via clients without breaking encapsulation + @Throws(DbException::class) + private fun shareGroup(contactId: ContactId, groupId: GroupId) { + db.transaction<RuntimeException>(false) { txn: Transaction? -> + db.setGroupVisibility( + txn, + contactId, + groupId, + Group.Visibility.SHARED + ) + } + } + + @Throws(DbException::class) + private fun createPrivateMessages( + contacts: List<Contact>, + numPrivateMsgs: Int + ) { + for (contact in contacts) { + val group = messagingManager.getContactGroup(contact) + shareGroup(contact.id, group.id) + for (i in 0 until numPrivateMsgs) { + createRandomPrivateMessage(contact.id, group.id, i) + } + } + if (LOG.isLoggable(Level.INFO)) { + LOG.info( + "Created " + numPrivateMsgs + + " private messages per contact." + ) + } + } + + @Throws(DbException::class) + private fun createRandomPrivateMessage( + contactId: ContactId, + groupId: GroupId, num: Int + ) { + val timestamp = clock.currentTimeMillis() - num * 60 * 1000 + val text = randomText + val local = random.nextBoolean() + val autoDelete = random.nextBoolean() + createPrivateMessage( + contactId, groupId, text, timestamp, local, + autoDelete + ) + } + + @Throws(DbException::class) + private fun createPrivateMessage( + contactId: ContactId, groupId: GroupId, + text: String, timestamp: Long, local: Boolean, autoDelete: Boolean + ) { + val timer = + if (autoDelete) AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS else AutoDeleteConstants.NO_AUTO_DELETE_TIMER + try { + val m = privateMessageFactory.createPrivateMessage( + groupId, timestamp, text, emptyList(), timer + ) + if (local) { + messagingManager.addLocalMessage(m) + } else { + db.transaction<RuntimeException>(false) { txn: Transaction? -> + db.receiveMessage( + txn, + contactId, + m.message + ) + } + } + } catch (e: FormatException) { + throw AssertionError(e) + } + } + + private val randomText: String + private get() { + val minLength = 3 + random.nextInt(500) + val maxWordLength = 15 + val sb = StringBuilder() + while (sb.length < minLength) { + if (sb.length > 0) sb.append(' ') + sb.append(StringUtils.getRandomString(random.nextInt(maxWordLength) + 1)) + } + if (random.nextBoolean()) { + sb.append(" \uD83D\uDC96 \uD83E\uDD84 \uD83C\uDF08") + } + return sb.toString() + } +} -- GitLab