Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f58466d25 | ||
|
|
c6f280ff72 | ||
|
|
f8b4e8cfd3 | ||
|
|
1fa2f53d35 | ||
|
|
5e22c3a391 | ||
|
|
8df7f7c74c | ||
|
|
45139640eb | ||
|
|
aa568cb628 | ||
|
|
4160a79574 | ||
|
|
8d04c10f48 | ||
|
|
02bfe60e8b | ||
|
|
4ff7b1a5ea |
@@ -74,9 +74,9 @@ jobs:
|
||||
SUFFIX=$((SUFFIX + 1))
|
||||
TAG="v${BASE}.${SUFFIX}"
|
||||
done
|
||||
echo "version_name=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "version_name=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||||
echo "version_tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "Generated version: $BASE"
|
||||
echo "Generated version: ${TAG#v} (tag: $TAG)"
|
||||
|
||||
- name: Restore keystore from secret
|
||||
# Ключ подписи передаётся как base64-секрет и декодируется в файл.
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
ORG_GRADLE_JVMARGS: '-Xmx2048m'
|
||||
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
|
||||
GRADLE_OPTS: '-Dorg.gradle.daemon=true'
|
||||
|
||||
- name: Determine APK path
|
||||
id: apk
|
||||
|
||||
@@ -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'
|
||||
@@ -109,11 +109,7 @@ dependencies {
|
||||
implementation(libs.pdfbox.android)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation("org.apache.pdfbox:pdfbox:3.0.4")
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,9 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
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.items
|
||||
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.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -54,27 +48,20 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
val hPadding = 16.dp
|
||||
@@ -141,32 +128,6 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
||||
if (day == null) {
|
||||
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
|
||||
} 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 dayLabel = if (isTodayDay) "Сегодня " else ""
|
||||
|
||||
@@ -192,31 +153,20 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
||||
days = weekDays,
|
||||
selectedDate = state.dayDate,
|
||||
todayDate = state.todayDate,
|
||||
onSelect = viewModel::selectDay,
|
||||
pagerState = pagerState
|
||||
onSelect = viewModel::selectDay
|
||||
)
|
||||
|
||||
HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding))
|
||||
|
||||
HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page ->
|
||||
val day = weekDays.getOrNull(page)
|
||||
|
||||
if (day == null || day.intervals.all { it.lessons.isEmpty() }) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
if (day.intervals.all { it.lessons.isEmpty() }) {
|
||||
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
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
|
||||
private fun DayChips(
|
||||
days: List<Day>,
|
||||
selectedDate: LocalDate?,
|
||||
todayDate: LocalDate?,
|
||||
pagerState: PagerState,
|
||||
onSelect: (LocalDate) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
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),) {
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
|
||||
items(days, key = { it.date.toEpochDay() }) { day ->
|
||||
val selected = selectedDate == day.date
|
||||
val isTodayChip = todayDate == day.date
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ agp = "9.1.1"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
runner = "1.5.2"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
kotlin = "2.2.10"
|
||||
@@ -22,7 +22,7 @@ monitor = "1.8.0"
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
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-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
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-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
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-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
|
||||
Reference in New Issue
Block a user