diff --git a/app/src/test/java/agzam/vyatsu/data/remote/GroupsParserTest.kt b/app/src/test/java/agzam/vyatsu/data/remote/GroupsParserTest.kt
new file mode 100644
index 0000000..05d9f77
--- /dev/null
+++ b/app/src/test/java/agzam/vyatsu/data/remote/GroupsParserTest.kt
@@ -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 = """
$name
"""
+
+ private fun groupDiv(periodId: String, name: String): String =
+ """$name
"""
+
+ private fun pdfLink(url: String): String = """x"""
+
+ 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),
+ """Группа Б
"""
+ )
+
+ 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))
+ }
+}