Compare commits

..
Author SHA1 Message Date
vantablackTea 1f58466d25 fix: removed unnecessary text from pdf test file
Unit Tests / unit-tests (push) Failing after 47s
Unit Tests / unit-tests (pull_request) Successful in 4m55s
2026-09-18 08:53:28 +03:00
vantablackTea c6f280ff72 tests: pdf parser test based on prepared files
Unit Tests / unit-tests (push) Successful in 6m6s
Unit Tests / unit-tests (pull_request) Successful in 5m43s
2026-09-17 14:30:33 +03:00
vantablackTea f8b4e8cfd3 fix: added back deleted libraries for testing, assumed they weren't used
Unit Tests / unit-tests (push) Successful in 7m4s
Unit Tests / unit-tests (pull_request) Successful in 7m39s
2026-09-16 21:07:51 +03:00
vantablackTea 1fa2f53d35 fix: slight tests optimisation
Unit Tests / unit-tests (push) Successful in 6m37s
Unit Tests / unit-tests (pull_request) Successful in 6m50s
2026-09-16 20:45:17 +03:00
vantablackTea 5e22c3a391 fix: changed incompatibility with old gitea runner
Unit Tests / unit-tests (push) Successful in 4m49s
Unit Tests / unit-tests (pull_request) Successful in 4m37s
2026-09-16 00:49:42 +03:00
vantablackTea 8df7f7c74c fix: version control fix, could work
Unit Tests / unit-tests (push) Failing after 5m2s
2026-09-16 00:37:14 +03:00
vantablackTea 45139640eb ci: added mandatory tests to build release
Unit Tests / unit-tests (push) Failing after 5m24s
2026-09-16 00:32:47 +03:00
vantablackTea aa568cb628 tests: added last unit tests, removed unused ui test dependencies 2026-09-16 00:15:33 +03:00
vantablackTea 4160a79574 tests: added ConvertersTest and LessonTypeTest 2026-09-15 23:50:41 +03:00
vantablackTea 8d04c10f48 tests: added GroupsParserTests 2026-09-14 16:33:39 +03:00
vantablackTea 02bfe60e8b tests: added pdf example, transcription, basic 11 tests 2026-09-14 15:41:42 +03:00
vantablackTea 4ff7b1a5ea tests: removed blanks, removed dependency 2026-09-14 14:23:48 +03:00
20 changed files with 820 additions and 192 deletions
+3 -3
View File
@@ -74,9 +74,9 @@ jobs:
SUFFIX=$((SUFFIX + 1)) SUFFIX=$((SUFFIX + 1))
TAG="v${BASE}.${SUFFIX}" TAG="v${BASE}.${SUFFIX}"
done done
echo "version_name=$TAG" >> "$GITHUB_OUTPUT" echo "version_name=${TAG#v}" >> "$GITHUB_OUTPUT"
echo "version_tag=$TAG" >> "$GITHUB_OUTPUT" echo "version_tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Generated version: $BASE" echo "Generated version: ${TAG#v} (tag: $TAG)"
- name: Restore keystore from secret - name: Restore keystore from secret
# Ключ подписи передаётся как base64-секрет и декодируется в файл. # Ключ подписи передаётся как base64-секрет и декодируется в файл.
@@ -102,7 +102,7 @@ jobs:
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ORG_GRADLE_JVMARGS: '-Xmx2048m' ORG_GRADLE_JVMARGS: '-Xmx2048m'
GRADLE_OPTS: '-Dorg.gradle.daemon=false' GRADLE_OPTS: '-Dorg.gradle.daemon=true'
- name: Determine APK path - name: Determine APK path
id: apk id: apk
+51
View File
@@ -0,0 +1,51 @@
name: Unit Tests
on:
push:
pull_request:
workflow_dispatch:
jobs:
unit-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
run: |
set -euxo pipefail
cd /tmp
wget -q https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.12%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz
tar -xzf OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz -C /opt
mv /opt/jdk-17.0.12+7 /opt/java17
export JAVA_HOME=/opt/java17
export PATH=$JAVA_HOME/bin:$PATH
echo "JAVA_HOME=/opt/java17" >> $GITHUB_ENV
echo "$JAVA_HOME/bin" >> $GITHUB_PATH
- name: Install Android SDK
run: |
set -euxo pipefail
echo "ANDROID_HOME=/opt/android-sdk" >> "$GITHUB_ENV"
mkdir -p /opt/android-sdk/cmdline-tools
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/clt.zip
unzip -q /tmp/clt.zip -d /tmp/clt
mv /tmp/clt/cmdline-tools /opt/android-sdk/cmdline-tools/latest
yes | /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses >/dev/null 2>&1 || true
/opt/android-sdk/cmdline-tools/latest/bin/sdkmanager \
"platform-tools" \
"platforms;android-36.1" \
"build-tools;36.1.0"
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run JVM unit tests
run: ./gradlew :app:testDebugUnitTest --console=plain
env:
ANDROID_HOME: /opt/android-sdk
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
+2 -15
View File
@@ -40,8 +40,7 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = true isMinifyEnabled = false
isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -67,14 +66,6 @@ android {
compose = true compose = true
buildConfig = true buildConfig = true
} }
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = false
}
}
} }
ksp { ksp {
@@ -118,11 +109,7 @@ dependencies {
implementation(libs.pdfbox.android) implementation(libs.pdfbox.android)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation("org.apache.pdfbox:pdfbox:3.0.4")
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
} }
-9
View File
@@ -19,12 +19,3 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
# Игнорировать отсутствие класса декодера JP2 в PdfBox-Android
-dontwarn com.gemalto.jp2.JP2Decoder
# На всякий случай, если PdfBox ссылается на другие отсутствующие библиотеки (например, ICU для текста)
-dontwarn com.tom_roush.pdfbox.**
# Запретить R8 удалять и переименовывать внутренности PdfBox-Android
-keep class com.tom_roush.pdfbox.** { *; }
-keep interface com.tom_roush.pdfbox.** { *; }
+95
View File
@@ -0,0 +1,95 @@
Факультет автоматики и вычислительной техники (ОРУ)
Группа: ИВТб-3304-05-00
Понедельник (14.09.26): <пусто, пар нет>
Вторник (15.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - ИВТб-3304-05-00 Объектно-ориентированный анализ и проектирование Лекция Преподаватель -.П. 2-202; ИВТб-3304-05-00 Разработка Web-приложений Лекция Преподаватель -.П. 1-236
11:45-13:15 - ИВТб-3304-05-00 Инструментальные средства проектирования компонентов вычислительной техники Лекция Семеновых В.И. 2-201; ИВТб-3304-05-00 Предметно-ориентированные автоматизированные информационные системы Лекция Прозорова Ю.А. 1-313; ИВТб-3304-05-00 Сценарное и функциональное программирование Лекция Преподаватель -.П. 1-311
14:00-15:30 - ИВТб-3304-05-00 Инструментальные средства проектирования компонентов вычислительной техники Лабораторная работа Семеновых В.И. 2-201; ИВТб-3304-05-00 Предметно-ориентированные автоматизированные информационные системы Лабораторная работа Прозорова Ю.А. 1-236; ИВТб-3304-05-00 Сценарное и функциональное программирование Лабораторная работа Преподаватель -.П. 1-114
15:45-17:15 - ИВТб-3304-05-00 Инструментальные средства проектирования компонентов вычислительной техники Лабораторная работа Семеновых В.И. 2-201; ИВТб-3304-05-00 Предметно-ориентированные автоматизированные информационные системы Лабораторная работа Прозорова Ю.А. 1-236; ИВТб-3304-05-00 Сценарное и функциональное программирование Лабораторная работа Преподаватель -.П. 1-114
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Среда (16.09.26): 08:20-09:50 - Введение в архитектуру ЭВМ Лекция Страбыкин Д.А. 1-242
10:00-11:30 - Теория компиляторов и формальные грамматики Лекция Мельцов В.Ю. 1-242
11:45-13:15 - <пусто>
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Четверг (17.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 131) Практическое занятие Трушкова Е.Л. 9-115; ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 039) Практическое занятие Салопин О.М. 9-301 ИВТб-3304-05-00, 02 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 039) Практическое занятие Салопин О.М. 9-301; ИВТб-3304-05-00, 02 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (21 285) Практическое занятие Хвостенко С.Ю. 9-209.; ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 124) Практическое занятие Пластинина В.Б. 9-208
14:00-15:30 - Архитектура операционных систем и сервисное программное обеспечение Лекция Преподаватель -.П. 1-439
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Пятница (18.09.26): 08:20-09:50 - ИВТб-3304-05-00, 01 подгруппа Введение в архитектуру ЭВМ Лабораторная работа Страбыкин Д.А. 1-117; ИВТб-3304-05-00, 02 подгруппа Введение в архитектуру ЭВМ Лабораторная работа Страбыкин Д.А. 1-117_
10:00-11:30 - ИВТб-3304-05-00, 01 подгруппа Введение в архитектуру ЭВМ Лабораторная работа Страбыкин Д.А. 1-117; ИВТб-3304-05-00, 02 подгруппа Введение в архитектуру ЭВМ Лабораторная работа Страбыкин Д.А. 1-117_
11:45-13:15 - Введение в архитектуру ЭВМ Практическое занятие Страбыкин Д.А. 2-207
14:00-15:30 - Философия Практическое занятие Коротышева Н.Н. 2-407
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Суббота (19.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - <пусто>
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Воскресенье (20.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - <пусто>
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Понедельник (21.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - ИВТб-3304-05-00, 01 подгруппа Теория компиляторов и формальные грамматики Лабораторная работа Караваева О.В. 2-308; ИВТб-3304-05-00, 02 подгруппа Теория компиляторов и формальные грамматики Лабораторная работа Караваева О.В. 2-308
11:45-13:15 - ИВТб-3304-05-00, 01 подгруппа Теория компиляторов и формальные грамматики Лабораторная работа Караваева О.В. 2-308; ИВТб-3304-05-00, 02 подгруппа Теория компиляторов и формальные грамматики Лабораторная работа Караваева О.В. 2-308_
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Вторник (22.09.26): 08:20-09:50 - Объектно-ориентированный анализ и проектирование Практическое занятие Шмакова Н.А. 2-202
10:00-11:30 - ИВТб-3304-05-00 Инструментальные средства проектирования компонентов вычислительной техники Лекция Семеновых В.И. 2-201; ИВТб-3304-05-00 Предметно-ориентированные автоматизированные информационные системы Лекция Прозорова Ю.А. 1-313; ИВТб-3304-05-00 Сценарное и функциональное программирование Лекция Преподаватель -.П. 1-311
11:45-13:15 - ИВТб-3304-05-00 Объектно-ориентированный анализ и проектирование Лекция Преподаватель -.П. 2-202; ИВТб-3304-05-00 Проектирование систем на ПЛИС Лекция Крутиков А.К. 1-313; ИВТб-3304-05-00 Разработка Web-приложений Лекция Преподаватель -.П. 1-236
14:00-15:30 - ИВТб-3304-05-00 Проектирование систем на ПЛИС Практическое занятие Крутиков А.К. 1-109; ИВТб-3304-05-00 Разработка Web-приложений Практическое занятие Пащенко Д.Э. 1-116
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Среда (23.09.26): 08:20-09:50 - Теория компиляторов и формальные грамматики Практическое занятие Караваева О.В. 2-201
10:00-11:30 - Введение в архитектуру ЭВМ Лекция Страбыкин Д.А. 1-242
11:45-13:15 - Архитектура операционных систем и сервисное программное обеспечение Лекция Преподаватель -.П. 1-242
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Четверг (24.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 131) Практическое занятие Трушкова Е.Л. 9-115; ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 039) Практическое занятие Салопин О.М. 9-301 ИВТб-3304-05-00, 02 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 039) Практическое занятие Салопин О.М. 9-301; ИВТб-3304-05-00, 02 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (21 285) Практическое занятие Хвостенко С.Ю. 9-101; ИВТб-3304-05-00, 01 подгруппа Элективные дисциплины (модули) по физической культуре и спорту Практическое занятие (11 124) Практическое занятие Пластинина В.Б. 9-208
14:00-15:30 - ИВТб-3304-05-00 Объектно-ориентированный анализ и проектирование Лабораторная работа Шмакова Н.А. 2-202; ИВТб-3304-05-00 Проектирование систем на ПЛИС Лабораторная работа Крутиков А.К. 1-239; ИВТб-3304-05-00 Разработка Web-приложений Лабораторная работа Пащенко Д.Э. 1-109
15:45-17:15 - ИВТб-3304-05-00 Объектно-ориентированный анализ и проектирование Лабораторная работа Шмакова Н.А. 2-202; ИВТб-3304-05-00 Проектирование систем на ПЛИС Лабораторная работа Крутиков А.К. 1-239; ИВТб-3304-05-00 Разработка Web-приложений Лабораторная работа Пащенко Д.Э. 1-109
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Пятница (25.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - <пусто>
14:00-15:30 - Теория компиляторов и формальные грамматики Лекция Мельцов В.Ю. 1-242
15:45-17:15 - Архитектура операционных систем и сервисное программное обеспечение Лабораторная работа Караваева О.В. 2-201
17:20-18:50 - Архитектура операционных систем и сервисное программное обеспечение Лабораторная работа Караваева О.В. 2-201
18:55-20:25 - <пусто>
Суббота (26.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - <пусто>
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Воскресенье (27.09.26): 08:20-09:50 - <пусто>
10:00-11:30 - <пусто>
11:45-13:15 - <пусто>
14:00-15:30 - <пусто>
15:45-17:15 - <пусто>
17:20-18:50 - <пусто>
18:55-20:25 - <пусто>
Binary file not shown.
@@ -1,24 +0,0 @@
package agzam.vyatsu
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("agzam.vyatsu", appContext.packageName)
}
}
@@ -0,0 +1,21 @@
package agzam.vyatsu.data.remote.pdf
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PdfDumpTest {
@Test
fun dumpParsedSchedule() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val bytes = instrumentation.context.assets.open("schedule.pdf").use { it.readBytes() }
val days = PdfScheduleParser.parse(bytes)
val target = instrumentation.targetContext.getExternalFilesDir(null)!!.resolve("parsed.txt")
target.writeText(days.joinToString(separator = "\n"))
}
}
@@ -0,0 +1,164 @@
package agzam.vyatsu.data.remote.pdf
import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.Lesson
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import java.time.LocalDate
import java.time.LocalTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PdfScheduleParserTest {
companion object {
private val days: List<Day> by lazy {
val bytes = InstrumentationRegistry.getInstrumentation().context.assets
.open("schedule.pdf")
.use { it.readBytes() }
PdfScheduleParser.parse(bytes)
}
}
private fun day(date: String): Day = days.first { it.date == LocalDate.parse(date) }
private fun lessons(date: String, start: String): List<Lesson> =
day(date).intervals
.first { it.start == LocalTime.parse(start) }
.lessons
.filter { it.raw.isNotBlank() }
private fun allLessons(date: String): List<Lesson> =
day(date).intervals.flatMap { it.lessons }.filter { it.raw.isNotBlank() }
private fun Lesson.canonical(): String =
listOf(subject, type.name, room, teacher, subgroup).joinToString("|")
@Test
fun parsesWholeTwoWeekPeriod() {
val expectedDates = (0..13L).map { LocalDate.of(2026, 9, 14).plusDays(it) }
assertEquals(expectedDates, days.map { it.date })
}
@Test
fun everyDayHasSevenStandardIntervals() {
val expectedTimes = listOf(
LocalTime.of(8, 20) to LocalTime.of(9, 50),
LocalTime.of(10, 0) to LocalTime.of(11, 30),
LocalTime.of(11, 45) to LocalTime.of(13, 15),
LocalTime.of(14, 0) to LocalTime.of(15, 30),
LocalTime.of(15, 45) to LocalTime.of(17, 15),
LocalTime.of(17, 20) to LocalTime.of(18, 50),
LocalTime.of(18, 55) to LocalTime.of(20, 25)
)
for (day in days) {
assertEquals(day.date.toString(), expectedTimes, day.intervals.map { it.start to it.end })
}
}
@Test
fun emptyDaysHaveNoLessons() {
val emptyDays = listOf("2026-09-14", "2026-09-19", "2026-09-20", "2026-09-26", "2026-09-27")
for (date in emptyDays) {
assertEquals(date, emptyList<Lesson>(), allLessons(date))
}
}
@Test
fun parsesTwoLecturesInOneSlot() {
assertEquals(
listOf(
"Объектно-ориентированный анализ и проектирование|LECTURE|2-202|Преподаватель -.П.|null",
"Разработка Web-приложений|LECTURE|1-236|Преподаватель -.П.|null"
),
lessons("2026-09-15", "10:00").map { it.canonical() }
)
}
@Test
fun parsesThreeLecturesInOneSlot() {
assertEquals(
listOf(
"Инструментальные средства проектирования компонентов вычислительной техники|LECTURE|2-201|Семеновых В.И.|null",
"Предметно-ориентированные автоматизированные информационные системы|LECTURE|1-313|Прозорова Ю.А.|null",
"Сценарное и функциональное программирование|LECTURE|1-311|Преподаватель -.П.|null"
),
lessons("2026-09-15", "11:45").map { it.canonical() }
)
}
@Test
fun parsesLaboratoryWork() {
assertEquals(
listOf(
"Инструментальные средства проектирования компонентов вычислительной техники|LABORATORY|2-201|Семеновых В.И.|null",
"Предметно-ориентированные автоматизированные информационные системы|LABORATORY|1-236|Прозорова Ю.А.|null",
"Сценарное и функциональное программирование|LABORATORY|1-114|Преподаватель -.П.|null"
),
lessons("2026-09-15", "14:00").map { it.canonical() }
)
}
@Test
fun parsesWednesdayLectures() {
assertEquals(
listOf("Введение в архитектуру ЭВМ|LECTURE|1-242|Страбыкин Д.А.|null"),
lessons("2026-09-16", "08:20").map { it.canonical() }
)
assertEquals(
listOf("Теория компиляторов и формальные грамматики|LECTURE|1-242|Мельцов В.Ю.|null"),
lessons("2026-09-16", "10:00").map { it.canonical() }
)
}
@Test
fun parsesPhysicalEducationSubgroups() {
val parsed = lessons("2026-09-17", "11:45").filter { it.room != null }
assertEquals(
listOf(
"Элективные дисциплины (модули) по физической культуре и спорту|PRACTICE|9-115|Трушкова Е.Л.|01 подгруппа",
"Элективные дисциплины (модули) по физической культуре и спорту|PRACTICE|9-301|Салопин О.М.|01 подгруппа",
"Элективные дисциплины (модули) по физической культуре и спорту|PRACTICE|9-301|Салопин О.М.|02 подгруппа"
),
parsed.map { it.canonical() }
)
}
@Test
fun keepsRawTextWhenRoomHasTrailingPunctuation() {
val unparsed = lessons("2026-09-17", "11:45").filter { it.room == null }
assertEquals(2, unparsed.size)
assertTrue(unparsed.any { it.raw.contains("9-209.") })
assertTrue(unparsed.any { it.raw.contains("9-208__") })
}
@Test
fun parsesFridaySubgroupLaboratories() {
assertEquals(
listOf(
"Введение в архитектуру ЭВМ|LABORATORY|1-117|Страбыкин Д.А.|01 подгруппа",
"Введение в архитектуру ЭВМ|LABORATORY|1-117_|Страбыкин Д.А.|02 подгруппа"
),
lessons("2026-09-18", "08:20").map { it.canonical() }
)
}
@Test
fun parsesSecondWeekMondayLaboratories() {
assertEquals(
listOf(
"Теория компиляторов и формальные грамматики|LABORATORY|2-308|Караваева О.В.|01 подгруппа",
"Теория компиляторов и формальные грамматики|LABORATORY|2-308_|Караваева О.В.|02 подгруппа"
),
lessons("2026-09-21", "10:00").map { it.canonical() }
)
}
}
@@ -54,5 +54,17 @@ fun BuildingsScreen() {
} }
HorizontalDivider(Modifier.padding(horizontal = 16.dp)) HorizontalDivider(Modifier.padding(horizontal = 16.dp))
} }
item {
Spacer(Modifier.height(24.dp))
Text(
"Версия ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
}
} }
} }
@@ -25,15 +25,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -54,27 +48,20 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
val hPadding = 16.dp val hPadding = 16.dp
@@ -141,32 +128,6 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
if (day == null) { if (day == null) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else { } else {
val schedule = state.schedule!!
val weekDays = state.weekStart
?.let { ws -> schedule.periods.firstOrNull { it.weekStart == ws } }
?.let { schedule.daysOf(it) }
.orEmpty()
val selectedDayIndex = weekDays.indexOfFirst { it.date == state.dayDate }.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
LaunchedEffect(state.dayDate) {
val targetPage = weekDays.indexOfFirst { it.date == state.dayDate }
if (targetPage != -1 && targetPage != pagerState.currentPage) {
pagerState.scrollToPage(targetPage) // Мгновенное переключение без анимации
}
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
if (pagerState.isScrollInProgress && page in weekDays.indices) {
viewModel.selectDay(weekDays[page].date)
}
}
}
val isTodayDay = day.date == state.todayDate val isTodayDay = day.date == state.todayDate
val dayLabel = if (isTodayDay) "Сегодня " else "" val dayLabel = if (isTodayDay) "Сегодня " else ""
@@ -192,31 +153,20 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
days = weekDays, days = weekDays,
selectedDate = state.dayDate, selectedDate = state.dayDate,
todayDate = state.todayDate, todayDate = state.todayDate,
onSelect = viewModel::selectDay, onSelect = viewModel::selectDay
pagerState = pagerState
) )
HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding)) HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding))
if (day.intervals.all { it.lessons.isEmpty() }) {
HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page -> Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
val day = weekDays.getOrNull(page) } else {
LazyColumn(
if (day == null || day.intervals.all { it.lessons.isEmpty() }) { modifier = Modifier.weight(1f),
Box( verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = 8.dp)
contentAlignment = Alignment.Center ) {
) { itemsIndexed(day.intervals) { slotIndex, slot ->
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) LessonSlot(slotIndex = slotIndex, slot = slot)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
itemsIndexed(day.intervals) { slotIndex, slot ->
LessonSlot(slotIndex = slotIndex, slot = slot)
}
} }
} }
} }
@@ -292,70 +242,14 @@ private fun WeekSelector(
} }
} }
private fun getVisiblePercentage(item: LazyListItemInfo, layoutInfo: LazyListLayoutInfo): Float {
val viewportStart = layoutInfo.viewportStartOffset
val viewportEnd = layoutInfo.viewportEndOffset
val itemStart = item.offset
val itemEnd = item.offset + item.size
val visibleStart = max(itemStart, viewportStart)
val visibleEnd = min(itemEnd, viewportEnd)
val visibleSize = visibleEnd - visibleStart
return if (visibleSize <= 0) 0f else visibleSize.toFloat() / item.size
}
@Composable @Composable
private fun DayChips( private fun DayChips(
days: List<Day>, days: List<Day>,
selectedDate: LocalDate?, selectedDate: LocalDate?,
todayDate: LocalDate?, todayDate: LocalDate?,
pagerState: PagerState,
onSelect: (LocalDate) -> Unit onSelect: (LocalDate) -> Unit
) { ) {
val listState = rememberLazyListState() LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
val density = LocalDensity.current
val paddingPx = with(density) { hPadding.toPx() }
LaunchedEffect(pagerState, days) {
snapshotFlow {
pagerState.currentPage
}.collectLatest { targetPage ->
if (targetPage in days.indices) {
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
val targetItem = visibleItems.firstOrNull { it.index == targetPage }
if (targetItem != null) {
val visiblePercentage = getVisiblePercentage(targetItem, layoutInfo)
val minVisibleThreshold = 0.9f
if (visiblePercentage < minVisibleThreshold) {
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
val halfViewport = viewPortWidth / 2f
val centerOffset = (halfViewport - (targetItem.size / 2f) - paddingPx).toInt()
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
}
} else {
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
val halfViewport = viewPortWidth / 2f
val averageSize = visibleItems.map { it.size }.average().toInt().coerceAtLeast(150)
val centerOffset = (halfViewport - (averageSize / 2f) - paddingPx).toInt()
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
}
}
}
}
LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
items(days, key = { it.date.toEpochDay() }) { day -> items(days, key = { it.date.toEpochDay() }) { day ->
val selected = selectedDate == day.date val selected = selectedDate == day.date
val isTodayChip = todayDate == day.date val isTodayChip = todayDate == day.date
@@ -118,7 +118,7 @@ class AppUpdater(
} }
val remoteVersion = release.tagName?.trimStart('v', 'V') val remoteVersion = release.tagName?.trimStart('v', 'V')
if (remoteVersion == null || !isNewerVersion(remoteVersion, BuildConfig.VERSION_NAME.trimStart('v', 'V'))) { if (remoteVersion == null || !isNewerVersion(remoteVersion, BuildConfig.VERSION_NAME)) {
_state.value = UpdateState.UpToDate _state.value = UpdateState.UpToDate
return null return null
} }
@@ -1,17 +0,0 @@
package agzam.vyatsu
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -0,0 +1,57 @@
package agzam.vyatsu.data.local
import agzam.vyatsu.domain.model.LessonType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.time.LocalDate
import java.time.LocalTime
class ConvertersTest {
private val converters = Converters()
@Test
fun localDate_roundTripsThroughEpochDay() {
val date = LocalDate.of(2026, 9, 14)
assertEquals(date, converters.epochDayToLocalDate(converters.localDateToEpochDay(date)))
}
@Test
fun nullDate_staysNull() {
assertNull(converters.localDateToEpochDay(null))
assertNull(converters.epochDayToLocalDate(null))
}
@Test
fun localTime_roundTripsThroughMinutes() {
assertEquals(
LocalTime.of(8, 20),
converters.minutesToLocalTime(converters.localTimeToMinutes(LocalTime.of(8, 20)))
)
}
@Test
fun localTime_boundariesMapToMinutes() {
assertEquals(0, converters.localTimeToMinutes(LocalTime.of(0, 0)))
assertEquals(1439, converters.localTimeToMinutes(LocalTime.of(23, 59)))
}
@Test
fun nullTime_staysNull() {
assertNull(converters.localTimeToMinutes(null))
assertNull(converters.minutesToLocalTime(null))
}
@Test
fun lessonType_roundTripsEveryValue() {
for (type in LessonType.entries) {
assertEquals(type, converters.stringToLessonType(converters.lessonTypeToString(type)))
}
}
@Test
fun unknownLessonType_isOther() {
assertEquals(LessonType.OTHER, converters.stringToLessonType("NOPE"))
}
}
@@ -0,0 +1,162 @@
package agzam.vyatsu.data.remote
import agzam.vyatsu.domain.model.DateRange
import agzam.vyatsu.domain.model.Season
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.InputStream
class GroupsParserTest {
private val faculty = "Факультет автоматики и вычислительной техники (ОРУ)"
private val facultyShort = "ФАВТ (Факультет автоматики и вычислительной техники)"
private fun page(vararg blocks: String): String = blocks.joinToString("\n")
private fun facultyDiv(name: String): String = """<div class="fak_name">$name</div>"""
private fun groupDiv(periodId: String, name: String): String =
"""<div class="grpPeriod" data-grp_period_id="$periodId">$name</div>"""
private fun pdfLink(url: String): String = """<a href="$url">x</a>"""
private fun parse(html: String): ParsedGroups =
GroupsParser.parseStream(html.byteInputStream(Charsets.UTF_8))
private fun parseLineByLine(html: String): ParsedGroups {
val lines = html.split("\n").map { "$it\n" }
var index = 0
val stream = object : InputStream() {
override fun read(): Int = -1
override fun read(b: ByteArray, off: Int, len: Int): Int {
if (index >= lines.size) return -1
val bytes = lines[index].toByteArray(Charsets.UTF_8)
val n = minOf(len, bytes.size)
System.arraycopy(bytes, 0, b, off, n)
index++
return n
}
}
return GroupsParser.parseStream(stream)
}
@Test
fun dropsLastDigitFromPeriodId() {
val html = page(
facultyDiv(faculty),
groupDiv("123456", "ИВТб-3304-05-00 (бакалавриат)")
)
val group = parse(html).groups.single()
assertEquals("12345", group.id)
assertEquals("ИВТб-3304-05-00", group.name)
assertEquals(facultyShort, group.faculty)
}
@Test
fun removesParenthesizedSuffixFromGroupName() {
val html = page(facultyDiv(faculty), groupDiv("777", "Группа А (подгруппа)"))
val group = parse(html).groups.single()
assertEquals("77", group.id)
assertEquals("Группа А", group.name)
}
@Test
fun keepsFacultyNameWhenNoShorthand() {
val html = page(facultyDiv("Некий институт (ОРУ)"), groupDiv("5555", "Гр"))
val group = parse(html).groups.single()
assertEquals("Некий институт", group.faculty)
}
@Test
fun facultyAppliesToGroupsUntilChanged() {
val html = page(
facultyDiv(faculty),
groupDiv("1111", "А-1"),
groupDiv("2222", "А-2"),
facultyDiv("Юридический институт (факультет) (ОРУ)"),
groupDiv("3333", "Ю-1")
)
val faculties = parse(html).groups.map { it.faculty }
assertEquals(
listOf(facultyShort, facultyShort, "ЮИ (Юридический институт)"),
faculties
)
}
@Test
fun parsesDateRangesGroupedBySeason() {
val html = page(
pdfLink("/reports/schedule/Group/12345_1_14092026_27092026.pdf"),
pdfLink("/reports/schedule/Group/12345_1_28092026_11102026.pdf"),
pdfLink("/reports/schedule/Group/12345_2_09022027_22022027.pdf")
)
val ranges = parse(html).dateRanges
assertEquals(
listOf(DateRange("14092026", "27092026"), DateRange("28092026", "11102026")),
ranges["12345"]?.get(Season.AUTUMN)
)
assertEquals(
listOf(DateRange("09022027", "22022027")),
ranges["12345"]?.get(Season.SPRING)
)
}
@Test
fun ignoresUnrelatedLinks() {
val html = page(
pdfLink("/other/12345_1_14092026_27092026.pdf"),
pdfLink("/reports/schedule/Group/123_1_14092026_27092026.pdf")
)
assertTrue(parse(html).dateRanges.isEmpty())
}
@Test
fun extractsRangesFromTextBeforeTag() {
val html = "/reports/schedule/Group/12345_1_14092026_27092026.pdf " + pdfLink("#")
val ranges = parse(html).dateRanges
assertEquals(
listOf(DateRange("14092026", "27092026")),
ranges["12345"]?.get(Season.AUTUMN)
)
}
@Test
fun greaterThanInsideQuotedAttributeDoesNotBreakTag() {
val html = page(
facultyDiv(faculty),
"""<div class="grpPeriod" data-grp_period_id="555" title="a>b">Группа Б</div>"""
)
val group = parse(html).groups.single()
assertEquals("55", group.id)
assertEquals("Группа Б", group.name)
}
@Test
fun multipleReadsProduceSameResultAsSingleRead() {
val html = page(
facultyDiv(faculty),
groupDiv("123456", "ИВТб-3304-05-00 (бакалавриат)"),
pdfLink("/reports/schedule/Group/12345_1_14092026_27092026.pdf"),
groupDiv("7654321", "ИВТб-3304-05-01"),
pdfLink("/reports/schedule/Group/765432_2_09022027_22022027.pdf")
)
assertEquals(parse(html), parseLineByLine(html))
}
}
@@ -0,0 +1,43 @@
package agzam.vyatsu.data.remote
import agzam.vyatsu.domain.model.DateRange
import agzam.vyatsu.domain.model.Season
import org.junit.Assert.assertEquals
import org.junit.Test
class VyatsuUrlsTest {
private val autumnRange = DateRange("14092026", "27092026")
@Test
fun groupPdfUrl_usesAutumnKey() {
assertEquals(
"https://www.vyatsu.ru/reports/schedule/Group/12345_1_14092026_27092026.pdf",
VyatsuUrls.groupPdfUrl("12345", Season.AUTUMN, autumnRange)
)
}
@Test
fun groupPdfUrl_usesSpringKey() {
assertEquals(
"https://www.vyatsu.ru/reports/schedule/Group/765432_2_09022027_22022027.pdf",
VyatsuUrls.groupPdfUrl("765432", Season.SPRING, DateRange("09022027", "22022027"))
)
}
@Test
fun season_apiKeysMatchUrlScheme() {
assertEquals("1", Season.AUTUMN.apiKey)
assertEquals("2", Season.SPRING.apiKey)
}
@Test
fun generatedUrl_isRecognizedByGroupsParser() {
val url = VyatsuUrls.groupPdfUrl("12345", Season.AUTUMN, autumnRange)
val html = """<a href="$url">x</a>"""
val parsed = GroupsParser.parseStream(html.byteInputStream(Charsets.UTF_8))
assertEquals(listOf(autumnRange), parsed.dateRanges["12345"]?.get(Season.AUTUMN))
}
}
@@ -0,0 +1,82 @@
package agzam.vyatsu.domain
import agzam.vyatsu.domain.model.Season
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import java.time.LocalDate
import java.time.format.DateTimeParseException
class ScheduleDatesTest {
private val firstMonday = "14092026"
@Test
fun asDate_parsesDdMmYyyy() {
assertEquals(LocalDate.of(2026, 9, 14), ScheduleDates.asDate("14092026"))
assertEquals(LocalDate.of(2026, 9, 1), ScheduleDates.asDate("01092026"))
}
@Test
fun asDate_wrongFormat_throws() {
assertThrows(DateTimeParseException::class.java) {
ScheduleDates.asDate("2026-09-14")
}
}
@Test
fun dayIndexes_mondayOfFirstWeek_isZeroZero() {
assertEquals(0 to 0, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 14)))
}
@Test
fun dayIndexes_tuesdayToSaturday_areSameWeek() {
assertEquals(0 to 1, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 15)))
assertEquals(0 to 2, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 16)))
assertEquals(0 to 3, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 17)))
assertEquals(0 to 4, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 18)))
assertEquals(0 to 5, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 19)))
}
@Test
fun dayIndexes_sunday_movesToNextMonday() {
assertEquals(1 to 0, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 20)))
}
@Test
fun dayIndexes_secondWeek() {
assertEquals(1 to 0, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 21)))
assertEquals(1 to 1, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 22)))
assertEquals(1 to 5, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 26)))
}
@Test
fun dayIndexes_thirdCalendarWeek_wrapsToFirstScheduleWeek() {
assertEquals(0 to 0, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 28)))
}
@Test
fun dayIndexes_dateBeforeFirstMonday_isSecondScheduleWeek() {
assertEquals(1 to 0, ScheduleDates.dayIndexes(firstMonday, LocalDate.of(2026, 9, 7)))
}
@Test
fun dateByIndexes_composesFromFirstDate() {
assertEquals(LocalDate.of(2026, 9, 14), ScheduleDates.dateByIndexes(firstMonday, 0, 0))
assertEquals(LocalDate.of(2026, 9, 15), ScheduleDates.dateByIndexes(firstMonday, 0, 1))
assertEquals(LocalDate.of(2026, 9, 21), ScheduleDates.dateByIndexes(firstMonday, 1, 0))
assertEquals(LocalDate.of(2026, 9, 27), ScheduleDates.dateByIndexes(firstMonday, 1, 6))
}
@Test
fun currentSeason_augustToDecember_isAutumn() {
assertEquals(Season.AUTUMN, ScheduleDates.currentSeason(LocalDate.of(2026, 8, 1)))
assertEquals(Season.AUTUMN, ScheduleDates.currentSeason(LocalDate.of(2026, 12, 31)))
}
@Test
fun currentSeason_januaryToJuly_isSpring() {
assertEquals(Season.SPRING, ScheduleDates.currentSeason(LocalDate.of(2026, 1, 1)))
assertEquals(Season.SPRING, ScheduleDates.currentSeason(LocalDate.of(2026, 7, 31)))
}
}
@@ -0,0 +1,40 @@
package agzam.vyatsu.domain.model
import org.junit.Assert.assertEquals
import org.junit.Test
class LessonTypeTest {
@Test
fun fromRaw_recognizesExactLabels() {
assertEquals(LessonType.LECTURE, LessonType.fromRaw("Лекция"))
assertEquals(LessonType.PRACTICE, LessonType.fromRaw("Практическое занятие"))
assertEquals(LessonType.LABORATORY, LessonType.fromRaw("Лабораторная работа"))
}
@Test
fun fromRaw_trimsSurroundingWhitespace() {
assertEquals(LessonType.LECTURE, LessonType.fromRaw(" Лекция "))
}
@Test
fun fromRaw_unknownValue_isOther() {
assertEquals(LessonType.OTHER, LessonType.fromRaw("Семинар"))
assertEquals(LessonType.OTHER, LessonType.fromRaw(""))
}
@Test
fun labels_matchParserVocabulary() {
assertEquals("Лекция", LessonType.LECTURE.label)
assertEquals("Практическое занятие", LessonType.PRACTICE.label)
assertEquals("Лабораторная работа", LessonType.LABORATORY.label)
assertEquals("Занятие", LessonType.OTHER.label)
}
@Test
fun fromRaw_roundTripsEveryLabel() {
for (type in LessonType.entries) {
assertEquals(type, LessonType.fromRaw(type.label))
}
}
}
@@ -0,0 +1,72 @@
package agzam.vyatsu.domain.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Test
import java.time.LocalDate
class ScheduleTest {
private val autumnPeriod = SchedulePeriod(
weekStart = LocalDate.of(2026, 9, 14),
weekEnd = LocalDate.of(2026, 9, 27),
season = Season.AUTUMN
)
private val nextPeriod = SchedulePeriod(
weekStart = LocalDate.of(2026, 9, 28),
weekEnd = LocalDate.of(2026, 10, 11),
season = Season.AUTUMN
)
private val dayInFirstPeriod = Day(LocalDate.of(2026, 9, 15), emptyList())
private val dayInSecondPeriod = Day(LocalDate.of(2026, 9, 29), emptyList())
private val dayOutsidePeriods = Day(LocalDate.of(2026, 10, 20), emptyList())
private val schedule = Schedule(
groupId = "12345",
periods = listOf(autumnPeriod, nextPeriod),
days = listOf(dayInFirstPeriod, dayInSecondPeriod, dayOutsidePeriods)
)
@Test
fun periodOf_matchesDateInsidePeriod() {
assertEquals(autumnPeriod, schedule.periodOf(LocalDate.of(2026, 9, 15)))
}
@Test
fun periodOf_includesBothBounds() {
assertEquals(autumnPeriod, schedule.periodOf(autumnPeriod.weekStart))
assertEquals(autumnPeriod, schedule.periodOf(autumnPeriod.weekEnd))
assertEquals(nextPeriod, schedule.periodOf(nextPeriod.weekStart))
assertEquals(nextPeriod, schedule.periodOf(nextPeriod.weekEnd))
}
@Test
fun periodOf_returnsNullForDateOutsideAllPeriods() {
assertNull(schedule.periodOf(LocalDate.of(2026, 10, 20)))
}
@Test
fun daysOf_returnsOnlyDaysWithinPeriod() {
assertEquals(listOf(dayInFirstPeriod), schedule.daysOf(autumnPeriod))
assertEquals(listOf(dayInSecondPeriod), schedule.daysOf(nextPeriod))
}
@Test
fun daysOf_returnsEmptyWhenPeriodHasNoDays() {
val emptyPeriod = SchedulePeriod(
weekStart = LocalDate.of(2027, 1, 4),
weekEnd = LocalDate.of(2027, 1, 17),
season = Season.SPRING
)
assertEquals(emptyList<Day>(), schedule.daysOf(emptyPeriod))
}
@Test
fun daysOf_neverReturnsDayOutsideEveryPeriod() {
val collected = schedule.periods.flatMap { schedule.daysOf(it) }
assertFalse(collected.contains(dayOutsidePeriods))
}
}
+2 -4
View File
@@ -3,7 +3,7 @@ agp = "9.1.1"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" runner = "1.5.2"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0" activityCompose = "1.8.0"
kotlin = "2.2.10" kotlin = "2.2.10"
@@ -22,7 +22,7 @@ monitor = "1.8.0"
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
@@ -30,8 +30,6 @@ androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
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" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }