Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 25 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ name: CI

on:
push:
branches: [ main, develop ]
branches: [ "main" ]
pull_request:
types: [opened, synchronize, reopened]
branches: [ main, develop ]
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
branches: [ "main" ]

jobs:
lint:
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
permissions:
contents: read
checks: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Checkout Code
uses: actions/checkout@v6

- name: Set up JDK 17
uses: actions/setup-java@v4
Expand All @@ -28,27 +25,23 @@ jobs:
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Run ktlintCheck
- name: Run Lint Check
run: ./gradlew ktlintCheck

build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Run Unit Tests
run: ./gradlew test --stacktrace

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build App (Main Branch Only)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./gradlew :app:assembleDebug

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build
run: ./gradlew build
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v6
with:
name: test-results
path: '**/build/test-results/test/TEST-*.xml'
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
val javaVersion = JavaVersion.toVersion(libs.versions.jvmTarget.get())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
}

Expand Down
6 changes: 4 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
import org.jlleitschuh.gradle.ktlint.KtlintExtension

plugins {
Expand Down Expand Up @@ -35,8 +37,8 @@ subprojects {
if (name != "app") {
apply(plugin = "com.vanniktech.maven.publish")

extensions.configure<com.vanniktech.maven.publish.MavenPublishBaseExtension> {
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL)
extensions.configure<MavenPublishBaseExtension> {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()

coordinates("io.github.angrypodo", name, "0.1.0")
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
Expand Down
24 changes: 17 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
[versions]
# Build & Publish
androidGradlePlugin = "8.13.0"
androidGradlePlugin = "8.13.2"
vanniktechMavenPublish = "0.30.0"
ksp = "2.3.0"
ksp = "2.3.4"
ktlint = "11.5.1"

# Kotlin
kotlin = "2.2.21"
kotlin = "2.3.0"
kotlinxSerialization = "1.9.0"
kotlinpoet = "2.2.0"

# Android SDK
compileSdk = "36"
minSdk = "28"
jvmTarget = "11"
jvmTarget = "17"

# AndroidX & Compose
coreKtx = "1.17.0"
appcompat = "1.7.1"
activityCompose = "1.11.0"
composeBom = "2025.10.01"
composeNavigation = "2.9.5"
activityCompose = "1.12.2"
composeBom = "2025.12.01"
composeNavigation = "2.9.6"
material = "1.13.0"

# Test
junit = "6.0.1"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
robolectric = "4.16"
junit4 = "4.13.2"
mockito = "5.21.0"
mockitoKotlin = "6.1.0"

[libraries]
# --- Build Tools & Plugins ---
Expand Down Expand Up @@ -67,6 +71,12 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }

# --- Unit Test Libraries ---
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
junit4 = { module = "junit:junit", version.ref = "junit4" }

[plugins]
# Android
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Expand Down
4 changes: 4 additions & 0 deletions wisp-annotations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}

kotlin {
jvmToolchain(libs.versions.jvmTarget.get().toInt())
}
7 changes: 7 additions & 0 deletions wisp-processor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}

kotlin {
jvmToolchain(libs.versions.jvmTarget.get().toInt())
}

dependencies {
implementation(project(":wisp-annotations"))
implementation(libs.ksp.api)
Expand All @@ -11,4 +15,7 @@ dependencies {
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
testRuntimeOnly(libs.junit.platform.launcher)

testImplementation(libs.mockito.core)
testImplementation(libs.mockito.kotlin)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.angrypodo.wisp.mapper

import com.angrypodo.wisp.model.ClassRouteInfo
import com.angrypodo.wisp.model.ObjectRouteInfo
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSName
import com.google.devtools.ksp.symbol.KSValueArgument
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.kotlin.whenever

class RouteInfoMapperTest {

@Test
fun `toRouteInfo returns null if @Wisp annotation is missing`() {
val classDeclaration = mock<KSClassDeclaration>()
whenever(classDeclaration.annotations).thenReturn(emptySequence())

val result = classDeclaration.toRouteInfo()

assertNull(result)
}

@Test
fun `toRouteInfo returns ObjectRouteInfo for object declaration`() {
val mockPackageName = mock<KSName>()
whenever(mockPackageName.asString()).thenReturn("com.example")

val mockSimpleName = mock<KSName>()
whenever(mockSimpleName.asString()).thenReturn("HomeScreen")

val mockQualifiedName = mock<KSName>()
whenever(mockQualifiedName.asString()).thenReturn("com.example.HomeScreen")

val classDeclaration = mock<KSClassDeclaration>()
whenever(classDeclaration.classKind).thenReturn(ClassKind.OBJECT)
whenever(classDeclaration.packageName).thenReturn(mockPackageName)
whenever(classDeclaration.simpleName).thenReturn(mockSimpleName)
whenever(classDeclaration.qualifiedName).thenReturn(mockQualifiedName)

val mockAnnotation = createMockWispAnnotation("home")
whenever(classDeclaration.annotations).thenReturn(sequenceOf(mockAnnotation))

val result = classDeclaration.toRouteInfo()

assertNotNull(result)
assertTrue(result is ObjectRouteInfo)
assertEquals("home", result?.wispPath)
}

@Test
fun `toRouteInfo returns ClassRouteInfo for class declaration`() {
val mockPackageName = mock<KSName>()
whenever(mockPackageName.asString()).thenReturn("com.example")

val mockSimpleName = mock<KSName>()
whenever(mockSimpleName.asString()).thenReturn("ProfileScreen")

val mockQualifiedName = mock<KSName>()
whenever(mockQualifiedName.asString()).thenReturn("com.example.ProfileScreen")

val classDeclaration = mock<KSClassDeclaration>()
whenever(classDeclaration.classKind).thenReturn(ClassKind.CLASS)
whenever(classDeclaration.packageName).thenReturn(mockPackageName)
whenever(classDeclaration.simpleName).thenReturn(mockSimpleName)
whenever(classDeclaration.qualifiedName).thenReturn(mockQualifiedName)
whenever(classDeclaration.primaryConstructor).thenReturn(null)

val mockAnnotation = createMockWispAnnotation("profile")
whenever(classDeclaration.annotations).thenReturn(sequenceOf(mockAnnotation))

val result = classDeclaration.toRouteInfo()

assertNotNull(result)
assertTrue(result is ClassRouteInfo)
}

private fun createMockWispAnnotation(path: String): KSAnnotation {
val annotation = mock<KSAnnotation>()

val shortName = mock<KSName>()
whenever(shortName.asString()).thenReturn("Wisp")
whenever(annotation.shortName).thenReturn(shortName)

val arg = mock<KSValueArgument>()
val argName = mock<KSName>()
whenever(argName.asString()).thenReturn("path")
whenever(arg.name).thenReturn(argName)
whenever(arg.value).thenReturn(path)

whenever(annotation.arguments).thenReturn(listOf(arg))

return annotation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.angrypodo.wisp.validation

import com.angrypodo.wisp.model.ObjectRouteInfo
import com.squareup.kotlinpoet.ClassName
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class WispValidatorTest {

@Test
fun `validateDuplicatePaths returns Success when there are no duplicates`() {
val routes = listOf(
createMockRouteInfo("home", "HomeScreen"),
createMockRouteInfo("profile", "ProfileScreen"),
createMockRouteInfo("settings", "SettingsScreen")
)

val result = WispValidator.validateDuplicatePaths(routes)

assertTrue(result is WispValidator.ValidationResult.Success)
}

@Test
fun `validateDuplicatePaths returns Failure when there are duplicates`() {
val routes = listOf(
createMockRouteInfo("home", "HomeScreen"),
createMockRouteInfo("home", "AnotherHomeScreen")
)

val result = WispValidator.validateDuplicatePaths(routes)

assertTrue(result is WispValidator.ValidationResult.Failure)
val failure = result as WispValidator.ValidationResult.Failure
assertEquals(1, failure.errors.size)
assertTrue(failure.errors[0].contains("path 'home' is already used by multiple routes"))
}

private fun createMockRouteInfo(path: String, className: String): ObjectRouteInfo {
return ObjectRouteInfo(
routeClassName = ClassName("com.example", className),
factoryClassName = ClassName("com.example", "${className}Factory"),
wispPath = path
)
}
}
9 changes: 7 additions & 2 deletions wisp-runtime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
val javaVersion = JavaVersion.toVersion(libs.versions.jvmTarget.get())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
}

Expand All @@ -51,4 +52,8 @@ dependencies {
testRuntimeOnly(libs.junit.platform.launcher)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

testImplementation(libs.robolectric)
testImplementation(libs.junit4)
testImplementation(libs.androidx.junit)
}
Loading