Skip to content

Commit

Permalink
Add nsuts support (#127)
Browse files Browse the repository at this point in the history
Add NSU testing system support
  • Loading branch information
klimarissa17 authored and kunyavskiy committed Dec 1, 2023
1 parent 9f15cb9 commit 193d9a3
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config/_examples/_nsu/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "nsu",
"email": "$creds.nsu_login",
"password": "$creds.nsu_password",
"olympiadId": 228,
"tourId": 11881,
"url": "https://olympic.nsu.ru/nsuts-new"
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ graphql = "7.0.2" # https://github.com/ExpediaGroup/graphql-kot

ktor-client-cio = { version.ref = "ktor", group = "io.ktor", name = "ktor-client-cio" }
ktor-client-auth = { version.ref = "ktor", group = "io.ktor", name = "ktor-client-auth" }
ktor-client-contentNegotiation = { version.ref = "ktor", group = "io.ktor", name = "ktor-client-content-negotiation" }

ktor-serialization-kotlinx-json = { version.ref = "ktor", group = "io.ktor", name = "ktor-serialization-kotlinx-json" }

Expand Down
46 changes: 46 additions & 0 deletions schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,49 @@
],
"title": "noop"
},
"nsu": {
"type": "object",
"properties": {
"type": {
"const": "nsu",
"default": "nsu"
},
"url": {
"type": "string"
},
"olympiadId": {
"type": "number"
},
"tourId": {
"type": "number"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"timeZone": {
"type": "string"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
},
"network": {
"$ref": "#/$defs/org.icpclive.cds.settings.NetworkSettings?<kotlin.Boolean>"
}
},
"additionalProperties": false,
"required": [
"type",
"url",
"olympiadId",
"tourId",
"email",
"password"
],
"title": "nsu"
},
"pcms": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -534,6 +577,9 @@
{
"$ref": "#/$defs/noop"
},
{
"$ref": "#/$defs/nsu"
},
{
"$ref": "#/$defs/pcms"
},
Expand Down
1 change: 1 addition & 0 deletions src/backend/src/main/kotlin/org/icpclive/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.ktor.server.websocket.*
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonElement
Expand Down
2 changes: 2 additions & 0 deletions src/cds/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ dependencies {
implementation(libs.kotlinx.serialization.json5)
implementation(libs.ktor.client.auth)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.protobuf)
implementation(libs.graphql.ktor.client)
runtimeOnly(libs.grpc.netty)
Expand Down
213 changes: 213 additions & 0 deletions src/cds/src/main/kotlin/org/icpclive/cds/nsu/NSUDataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package org.icpclive.cds.nsu


import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import kotlinx.datetime.toKotlinLocalDateTime
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import org.icpclive.api.*
import org.icpclive.cds.common.ContestParseResult
import org.icpclive.cds.common.FullReloadContestDataSource
import org.icpclive.cds.common.defaultHttpClient
import org.icpclive.cds.settings.NSUSettings
import java.time.format.DateTimeFormatter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds


internal class NSUDataSource(val settings: NSUSettings) : FullReloadContestDataSource(5.seconds) {

val format = Json { ignoreUnknownKeys = true }

@Serializable
class Credentials(
val email: String,
val password: String,
val method: String = "internal"
)

@Serializable
class Team(
val id: Int,
val title: String
)

@Serializable
class Task(
val id: Int,
val title: String
)

@Serializable
class TourTimes(
val tour_start_time: String,
val tour_end_time: String,
val rating_freeze_time: String?
)

override suspend fun loadOnce(): ContestParseResult {
val queueLimit = 999999
val loginUrl = "${settings.url}/api/login"
val selectOlympiadUrl = "${settings.url}/api/olympiads/enter"
val selectTourUrl = "${settings.url}/api/tours/enter?tour=${settings.tourId}"
val submissionsUrl = "${settings.url}/api/queue/submissions?limit=" + queueLimit.toString()
val filtersUrl = "${settings.url}/api/queue/filters"
val ratingUrl = "${settings.url}/api/rating/rating?showFrozen=0"


httpClient.post(loginUrl) {
contentType(ContentType.Application.Json)
setBody(Credentials(settings.email.value, settings.password.value))
}.bodyAsText()

httpClient.post(selectOlympiadUrl) {
contentType(ContentType.Application.Json)
setBody(mapOf("olympiad" to settings.olympiadId.toString()))
}.bodyAsText()

httpClient.get(selectTourUrl).bodyAsText()

val queue: JsonObject = httpClient.get(submissionsUrl) {
contentType(ContentType.Application.Json)
}.body()


val submissions: List<Submission> = format.decodeFromJsonElement(queue["submissions"] as JsonArray)
val filters: JsonObject = httpClient.get(filtersUrl).body()
val teams: List<Team> = format.decodeFromJsonElement(filters["teams"] as JsonArray)
val tasks: List<Task> = format.decodeFromJsonElement(filters["tasks"] as JsonArray)

val teamList: List<TeamInfo> = teams.map {
TeamInfo(
id = it.id,
fullName = it.title,
displayName = it.title,
contestSystemId = it.id.toString(),
groups = emptyList(),
hashTag = null,
medias = emptyMap(),
isHidden = false,
isOutOfContest = false,
organizationId = null,
)
}.sortedBy { it.id }


val problemsList: List<ProblemInfo> = tasks.mapIndexed { index, it ->
ProblemInfo(
fullName = it.title,
displayName = it.title.substringBefore('.'),
id = it.id,
ordinal = index,
contestSystemId = it.id.toString()
)
}

val rating: JsonObject = httpClient.post(ratingUrl) {
contentType(ContentType.Application.Json)
setBody(mapOf("selectedAttributes" to emptyList<String>().toString()))
}.body()

val timeSettings: TourTimes = format.decodeFromJsonElement(rating["tourTimes"] as JsonObject)
val contestName: String = rating["tourTitle"]?.jsonPrimitive.toString()


val startTime = parseNSUTime(timeSettings.tour_start_time)
val contestLength = parseNSUTime(timeSettings.tour_end_time) - startTime

val freezeTime: Duration = if (timeSettings.rating_freeze_time != null) {
parseNSUTime(timeSettings.rating_freeze_time) - startTime
} else {
contestLength
}

val info = ContestInfo(
name = contestName,
status = ContestStatus.byCurrentTime(startTime, contestLength),
resultType = ContestResultType.ICPC,
startTime = startTime,
contestLength = contestLength,
freezeTime = freezeTime,
problemList = problemsList,
teamList = teamList,
groupList = emptyList(),
organizationList = emptyList(),
penaltyRoundingMode = PenaltyRoundingMode.EACH_SUBMISSION_DOWN_TO_MINUTE
)

val runs: List<RunInfo> = submissions.map {
RunInfo(
id = it.id,
result = getRunResult(it.res, it.status),
problemId = it.taskId,
teamId = it.teamId,
percentage = 0.0,
time = parseNSUTime(it.smtime) - startTime
)
}

return ContestParseResult(info, runs, emptyList())

}

private val httpClient = defaultHttpClient(null, settings.network) {
install(HttpCookies)
install(ContentNegotiation) { json() }
}

private fun getRunResult(result: String?, status: Int): RunResult? {
val verdict = when (val letter = result?.last()) {
'A' -> Verdict.Accepted
'C' -> Verdict.CompilationError
// "Deadlock - Timeout": astronomical time limit exceeded
'D' -> Verdict.TimeLimitExceeded
'M' -> Verdict.MemoryLimitExceeded
// "No output file"
'O' -> Verdict.PresentationError
'P' -> Verdict.PresentationError
'S' -> Verdict.SecurityViolation
'R' -> Verdict.RuntimeError
'T' -> Verdict.TimeLimitExceeded
'W' -> Verdict.WrongAnswer
// "Static Analysis Failed"
'X' -> Verdict.CompilationError
// "Dynamic Analysis failed"
'Y' -> Verdict.CompilationErrorWithPenalty
'.' -> Verdict.Ignored
'F', 'J', 'K' -> null
else -> error("Unknown verdict: $letter")
}
if (verdict == Verdict.Accepted && status != 3) return null
return verdict?.toRunResult()
}

private val timePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

private fun parseNSUTime(time: String): Instant {
return java.time.LocalDateTime.parse(
time, timePattern
).toKotlinLocalDateTime()
.toInstant(settings.timeZone)
}

@Serializable
@Suppress("unused")
class Submission(
val id: Int,
val teamId: Int,
val smtime: String,
val status: Int,
val res: String?,
val taskId: Int
)
}


17 changes: 17 additions & 0 deletions src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.icpclive.cds.common.isHttpUrl
import org.icpclive.cds.ejudge.EjudgeDataSource
import org.icpclive.cds.eolymp.EOlympDataSource
import org.icpclive.cds.krsu.KRSUDataSource
import org.icpclive.cds.nsu.NSUDataSource
import org.icpclive.cds.noop.NoopDataSource
import org.icpclive.cds.pcms.PCMSDataSource
import org.icpclive.cds.testsys.TestSysDataSource
Expand Down Expand Up @@ -149,6 +150,22 @@ public class KRSUSettings(
override fun toDataSource() = KRSUDataSource(this)
}

@Serializable
@SerialName("nsu")
public class NSUSettings(
public val url: String,
public val olympiadId: Int,
public val tourId: Int,
public val email: Credential,
public val password: Credential,
@Serializable(with = TimeZoneSerializer::class)
public val timeZone: TimeZone = TimeZone.of("Asia/Novosibirsk"),
override val emulation: EmulationSettings? = null,
override val network: NetworkSettings? = null
) : CDSSettings() {
override fun toDataSource() = NSUDataSource(this)
}

@Serializable
@SerialName("ejudge")
public class EjudgeSettings(
Expand Down

0 comments on commit 193d9a3

Please sign in to comment.