forked from upstream-mirrors/quik
Gate APK releases on worker factory and Matrix regression tests
Build and Release APK / Build APK (push) Failing after 2m46s
Build and Release APK / Build APK (push) Failing after 2m46s
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Require the critical runtime regression suites before publishing APKs."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKERS = 'dev.octoshrimpy.quik.worker.InjectionWorkerFactoryTest'
|
||||
REQUIRED = {
|
||||
WORKERS: {
|
||||
'matrixCoroutineWorkerIsCreatedByTheApplicationFactory',
|
||||
'matrixWatcherStartsTheExpectedService',
|
||||
'housekeepingReceivesItsRepository',
|
||||
'smsWorkerReceivesEveryRequiredDependency',
|
||||
'mmsWorkerReceivesEveryRequiredDependency',
|
||||
'eachRequestCreatesANewWorkerInstance',
|
||||
},
|
||||
'ch.skew.remotrix.bridge.BackfillRunnerTest': set(),
|
||||
'ch.skew.remotrix.bridge.RoomResolverTest': set(),
|
||||
'ch.skew.remotrix.bridge.CloseMatrixClientTest': set(),
|
||||
'ch.skew.remotrix.bridge.BridgeResetTest': set(),
|
||||
}
|
||||
|
||||
|
||||
def check(report_dirs):
|
||||
passed = {}
|
||||
count = 0
|
||||
for directory in report_dirs:
|
||||
for path in Path(directory).glob('TEST-*.xml'):
|
||||
suite = ET.parse(path).getroot()
|
||||
for case in suite.iter('testcase'):
|
||||
name = case.get('name', '')
|
||||
classname = case.get('classname', suite.get('name', ''))
|
||||
if case.find('failure') is not None or case.find('error') is not None:
|
||||
raise ValueError(f'Failed test: {classname}.{name}')
|
||||
if case.find('skipped') is not None:
|
||||
if classname in REQUIRED:
|
||||
raise ValueError(f'Skipped critical test: {classname}.{name}')
|
||||
continue
|
||||
passed.setdefault(classname, set()).add(name)
|
||||
count += 1
|
||||
for classname, names in REQUIRED.items():
|
||||
if not passed.get(classname):
|
||||
raise ValueError(f'Missing critical test suite: {classname}')
|
||||
missing = names - passed[classname]
|
||||
if missing:
|
||||
raise ValueError(f'Missing critical tests in {classname}: {sorted(missing)}')
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
total = check([
|
||||
ROOT / 'presentation/build/test-results/testDebugUnitTest',
|
||||
ROOT / 'remotrix-sync/build/test-results/testDebugUnitTest',
|
||||
])
|
||||
except (OSError, ValueError, ET.ParseError) as error:
|
||||
print(f'Release test gate failed: {error}', file=sys.stderr)
|
||||
return 1
|
||||
print(f'Release test gate passed: {total} tests, all critical suites present.')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'test_results_gate', Path(__file__).parents[1] / 'scripts/check_test_results.py')
|
||||
gate = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(gate)
|
||||
|
||||
|
||||
class ReleaseTestGateTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.directory = Path(self.temp.name)
|
||||
for classname, names in gate.REQUIRED.items():
|
||||
suite = ET.Element('testsuite', name=classname)
|
||||
for name in names or {'regressionTest'}:
|
||||
ET.SubElement(suite, 'testcase', classname=classname, name=name)
|
||||
ET.ElementTree(suite).write(self.directory / f'TEST-{classname}.xml')
|
||||
|
||||
def change_worker_report(self, change):
|
||||
path = self.directory / f'TEST-{gate.WORKERS}.xml'
|
||||
tree = ET.parse(path)
|
||||
change(tree.getroot())
|
||||
tree.write(path)
|
||||
|
||||
def test_all_required_tests_pass(self):
|
||||
self.assertEqual(gate.check([self.directory]), 10)
|
||||
|
||||
def test_empty_report_directory_blocks_release(self):
|
||||
with self.assertRaisesRegex(ValueError, 'Missing critical test suite'):
|
||||
gate.check([self.directory / 'missing'])
|
||||
|
||||
def test_failed_worker_test_blocks_release(self):
|
||||
self.change_worker_report(lambda r: ET.SubElement(r[0], 'failure'))
|
||||
with self.assertRaisesRegex(ValueError, 'Failed test'):
|
||||
gate.check([self.directory])
|
||||
|
||||
def test_skipped_worker_test_blocks_release(self):
|
||||
self.change_worker_report(lambda r: ET.SubElement(r[0], 'skipped'))
|
||||
with self.assertRaisesRegex(ValueError, 'Skipped critical test'):
|
||||
gate.check([self.directory])
|
||||
|
||||
def test_removed_coroutine_worker_test_blocks_release(self):
|
||||
def remove(root):
|
||||
for case in list(root):
|
||||
if case.get('name') == 'matrixCoroutineWorkerIsCreatedByTheApplicationFactory':
|
||||
root.remove(case)
|
||||
self.change_worker_report(remove)
|
||||
with self.assertRaisesRegex(ValueError, 'Missing critical tests'):
|
||||
gate.check([self.directory])
|
||||
@@ -63,8 +63,11 @@ jobs:
|
||||
env:
|
||||
BUILD_NUMBER: ${{ github.run_number }}
|
||||
|
||||
- name: Test Matrix bridge
|
||||
run: ./gradlew :remotrix-sync:testDebugUnitTest
|
||||
- name: Test worker factory and Matrix bridge
|
||||
run: |
|
||||
set -eu
|
||||
./gradlew :presentation:cleanTestDebugUnitTest :remotrix-sync:cleanTestDebugUnitTest :presentation:testDebugUnitTest :remotrix-sync:testDebugUnitTest
|
||||
python3 .gitea/scripts/check_test_results.py
|
||||
|
||||
- name: Build Debug APK
|
||||
run: |
|
||||
|
||||
@@ -186,3 +186,23 @@ QUIK is developed and maintained by [Marcos Jones](https://github.com/octoshrimp
|
||||
## License
|
||||
|
||||
QUIK is released under the **The GNU General Public License v3.0 (GPLv3)**, which can be found in the [LICENSE](LICENSE) file in the root of this project.
|
||||
|
||||
## Automated release checks
|
||||
|
||||
Before building or publishing APKs, Gitea Actions runs Robolectric tests against the actual
|
||||
`InjectionWorkerFactory`. They construct every worker type used by Quik and the Matrix bridge,
|
||||
verify SMS/MMS dependency injection, and exercise the Matrix watcher's service-start intent.
|
||||
These tests use simulated Android services and mocked repositories, without sending SMS or
|
||||
connecting to Matrix. Matrix room creation, client shutdown, reset and history import also have
|
||||
regression tests. Signing checks run separately.
|
||||
|
||||
Run the same runtime gate locally with JDK 17 and the Android SDK:
|
||||
|
||||
```sh
|
||||
./gradlew :presentation:cleanTestDebugUnitTest :remotrix-sync:cleanTestDebugUnitTest :presentation:testDebugUnitTest :remotrix-sync:testDebugUnitTest
|
||||
python3 .gitea/scripts/check_test_results.py
|
||||
```
|
||||
|
||||
The gate rejects failures, missing required suites, and skipped critical tests. A failed gate
|
||||
stops the workflow before publication. These tests cover known regression scenarios; they do
|
||||
not replace testing the installed Release APK on Android devices.
|
||||
|
||||
@@ -193,7 +193,9 @@ dependencies {
|
||||
androidTestImplementation "org.mockito:mockito-android:$mockito_version"
|
||||
testImplementation "androidx.test:runner:$androidx_testrunner_version"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testImplementation "org.mockito:mockito-core:$mockito_version"
|
||||
testImplementation "org.mockito:mockito-inline:4.11.0"
|
||||
testImplementation "org.robolectric:robolectric:4.11.1"
|
||||
testImplementation "androidx.work:work-testing:$androidx_work_version"
|
||||
|
||||
// moshi
|
||||
implementation "com.squareup.moshi:moshi:$moshi_version"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package dev.octoshrimpy.quik.worker
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.testing.TestListenableWorkerBuilder
|
||||
import ch.skew.remotrix.background.CommandService
|
||||
import ch.skew.remotrix.background.ServiceWatcher
|
||||
import dev.octoshrimpy.quik.blocking.BlockingClient
|
||||
import dev.octoshrimpy.quik.interactor.UpdateBadge
|
||||
import dev.octoshrimpy.quik.manager.ActiveConversationManager
|
||||
import dev.octoshrimpy.quik.manager.NotificationManager
|
||||
import dev.octoshrimpy.quik.manager.ShortcutManager
|
||||
import dev.octoshrimpy.quik.repository.ContactRepository
|
||||
import dev.octoshrimpy.quik.repository.ConversationRepository
|
||||
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
|
||||
import dev.octoshrimpy.quik.repository.MessageRepository
|
||||
import dev.octoshrimpy.quik.repository.ScheduledMessageRepository
|
||||
import dev.octoshrimpy.quik.repository.SyncRepository
|
||||
import dev.octoshrimpy.quik.util.Preferences
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.mock
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Real WorkManager construction and Android service intents; no SMS or Matrix network calls. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE, application = Application::class)
|
||||
class InjectionWorkerFactoryTest {
|
||||
private val conversationRepo = mock(ConversationRepository::class.java)
|
||||
private val blockingClient = mock(BlockingClient::class.java)
|
||||
private val prefs = mock(Preferences::class.java)
|
||||
private val messageRepo = mock(MessageRepository::class.java)
|
||||
private val updateBadge = mock(UpdateBadge::class.java)
|
||||
private val shortcutManager = mock(ShortcutManager::class.java)
|
||||
private val scheduledMessages = mock(ScheduledMessageRepository::class.java)
|
||||
private val notifications = mock(NotificationManager::class.java)
|
||||
private val activeConversation = mock(ActiveConversationManager::class.java)
|
||||
private val syncRepo = mock(SyncRepository::class.java)
|
||||
private val filters = mock(MessageContentFilterRepository::class.java)
|
||||
private val contacts = mock(ContactRepository::class.java)
|
||||
private val factory = InjectionWorkerFactory(
|
||||
conversationRepo, blockingClient, prefs, messageRepo, updateBadge, shortcutManager,
|
||||
scheduledMessages, notifications, activeConversation, syncRepo, filters, contacts
|
||||
)
|
||||
private val application: Application get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private inline fun <reified W : ListenableWorker> build(): W =
|
||||
TestListenableWorkerBuilder<W>(application).setWorkerFactory(factory).build()
|
||||
|
||||
@Test
|
||||
fun matrixCoroutineWorkerIsCreatedByTheApplicationFactory() {
|
||||
assertEquals(ServiceWatcher::class.java, build<ServiceWatcher>().javaClass)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matrixWatcherStartsTheExpectedService() = runBlocking {
|
||||
val worker = build<ServiceWatcher>()
|
||||
assertEquals(ListenableWorker.Result.success(), worker.doWork())
|
||||
val intent = shadowOf(application).nextStartedService
|
||||
assertNotNull(intent)
|
||||
assertEquals(CommandService.START_ALL, intent.action)
|
||||
assertEquals(ComponentName(application, CommandService::class.java), intent.component)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun housekeepingReceivesItsRepository() {
|
||||
assertSame(scheduledMessages, build<HousekeepingWorker>().scheduledMessageRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun smsWorkerReceivesEveryRequiredDependency() {
|
||||
val worker = build<ReceiveSmsWorker>()
|
||||
assertSame(conversationRepo, worker.conversationRepo)
|
||||
assertSame(blockingClient, worker.blockingClient)
|
||||
assertSame(prefs, worker.prefs)
|
||||
assertSame(messageRepo, worker.messageRepo)
|
||||
assertSame(notifications, worker.notificationManager)
|
||||
assertSame(updateBadge, worker.updateBadge)
|
||||
assertSame(shortcutManager, worker.shortcutManager)
|
||||
assertSame(filters, worker.filterRepo)
|
||||
assertSame(contacts, worker.contactsRepo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mmsWorkerReceivesEveryRequiredDependency() {
|
||||
val worker = build<ReceiveMmsWorker>()
|
||||
assertSame(activeConversation, worker.activeConversationManager)
|
||||
assertSame(conversationRepo, worker.conversationRepo)
|
||||
assertSame(blockingClient, worker.blockingClient)
|
||||
assertSame(prefs, worker.prefs)
|
||||
assertSame(syncRepo, worker.syncRepo)
|
||||
assertSame(messageRepo, worker.messageRepo)
|
||||
assertSame(notifications, worker.notificationManager)
|
||||
assertSame(updateBadge, worker.updateBadge)
|
||||
assertSame(shortcutManager, worker.shortcutManager)
|
||||
assertSame(filters, worker.filterRepo)
|
||||
assertSame(contacts, worker.contactsRepo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eachRequestCreatesANewWorkerInstance() {
|
||||
assertNotSame(build<ServiceWatcher>(), build<ServiceWatcher>())
|
||||
assertNotSame(build<ReceiveSmsWorker>(), build<ReceiveSmsWorker>())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user