diff --git a/Task_2_4_1/.gitignore b/Task_2_4_1/.gitignore new file mode 100644 index 0000000..4fe79f8 --- /dev/null +++ b/Task_2_4_1/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +*.jar +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/Task_2_4_1/.idea/.gitignore b/Task_2_4_1/.idea/.gitignore new file mode 100644 index 0000000..4287606 --- /dev/null +++ b/Task_2_4_1/.idea/.gitignore @@ -0,0 +1,4 @@ +# Default ignored files +/shelf/ +/workspace.xml +*.jar diff --git a/Task_2_4_1/.idea/artifacts/Task_2_4_1_jar.xml b/Task_2_4_1/.idea/artifacts/Task_2_4_1_jar.xml new file mode 100644 index 0000000..a24e261 --- /dev/null +++ b/Task_2_4_1/.idea/artifacts/Task_2_4_1_jar.xml @@ -0,0 +1,19 @@ + + + $PROJECT_DIR$/out/artifacts/Task_2_4_1_jar + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Task_2_4_1/.idea/gradle.xml b/Task_2_4_1/.idea/gradle.xml new file mode 100644 index 0000000..2a65317 --- /dev/null +++ b/Task_2_4_1/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/Task_2_4_1/.idea/misc.xml b/Task_2_4_1/.idea/misc.xml new file mode 100644 index 0000000..b35a12a --- /dev/null +++ b/Task_2_4_1/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Task_2_4_1/.idea/vcs.xml b/Task_2_4_1/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Task_2_4_1/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Task_2_4_1/build.gradle b/Task_2_4_1/build.gradle new file mode 100644 index 0000000..cfba2f5 --- /dev/null +++ b/Task_2_4_1/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'java' + id 'groovy' + id 'application' + id 'com.gradleup.shadow' version '8.3.0' +} + +group = 'ru.nsu.nmashkin.task241' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.codehaus.groovy:groovy:3.0.19' + implementation 'org.codehaus.groovy:groovy-templates:3.0.19' + + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +test { + useJUnitPlatform() +} + +application { + mainClass = 'ru.nsu.nmashkin.task241.Main' +} + +sourceSets { + main { + // Отдаём все исходники компилятору Groovy. Он умеет компилировать и Java. + groovy { + srcDirs = ['src/main/groovy', 'src/main/java'] + } + // Отключаем отдельный javac, чтобы не было конфликта и циклов + java { + srcDirs = [] + } + } +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/Task_2_4_1/config/groups.groovy b/Task_2_4_1/config/groups.groovy new file mode 100644 index 0000000..b91db5b --- /dev/null +++ b/Task_2_4_1/config/groups.groovy @@ -0,0 +1,10 @@ +group("24213") { + IlyaStub("Ilya Stubarev", "https://github.com/IlyaStub/OOP") + "7AD0VNIK"("Klim Sadov", "https://github.com/7AD0VNIK/OOP") + aelsi2("Andrey Eliseev", "https://github.com/aelsi2/OOP") + Matvey("Matvey Solovev", "https://github.com/fresh-ops/OOP") +} + +group("24214") { + LookAsLukas("Mashkin Nikolay", "https://github.com/LookAsLukas/OOP") +} diff --git a/Task_2_4_1/config/tasks.groovy b/Task_2_4_1/config/tasks.groovy new file mode 100644 index 0000000..ee4eca6 --- /dev/null +++ b/Task_2_4_1/config/tasks.groovy @@ -0,0 +1,10 @@ +tasks { + Task_1_2_1("Graph bs", 3, "01.11.2025", "08.11.2025") + Task_1_1_1("IDKlol", 1, "01.09.2025", "08.09.2025") +} + +criteria { + excellent 4 + goida 2 + loh 1 +} diff --git a/Task_2_4_1/config/test_check.groovy b/Task_2_4_1/config/test_check.groovy new file mode 100644 index 0000000..73fcf83 --- /dev/null +++ b/Task_2_4_1/config/test_check.groovy @@ -0,0 +1,21 @@ +importConfig 'config/tasks.groovy' +importConfig 'config/groups.groovy' + +checkpoints { + oct "01.10.2025" + lol "01.01.2026" +} + +points { + IlyaStub (-67) + "7AD0VNIK" (67) + aelsi2 (0) + Matvey (1) + LookAsLukas 999999 +} + +check { + LookAsLukas "Task_1_1_1" + LookAsLukas "Task_1_2_1" + aelsi2 "Task_1_1_1" +} diff --git a/Task_2_4_1/gradle/wrapper/gradle-wrapper.properties b/Task_2_4_1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6e512f7 --- /dev/null +++ b/Task_2_4_1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 23 09:41:48 NOVT 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Task_2_4_1/gradlew b/Task_2_4_1/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/Task_2_4_1/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Task_2_4_1/gradlew.bat b/Task_2_4_1/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/Task_2_4_1/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Task_2_4_1/index.html b/Task_2_4_1/index.html new file mode 100644 index 0000000..b52413e Binary files /dev/null and b/Task_2_4_1/index.html differ diff --git a/Task_2_4_1/settings.gradle b/Task_2_4_1/settings.gradle new file mode 100644 index 0000000..4fadcb5 --- /dev/null +++ b/Task_2_4_1/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Task_2_4_1' \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/BonusBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/BonusBlock.groovy new file mode 100644 index 0000000..ea40423 --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/BonusBlock.groovy @@ -0,0 +1,15 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Config + +class BonusBlock { + private final Config config + + BonusBlock(Config config) { this.config = config } + + def methodMissing(String name, args) { + if (args.length == 1 && args[0] instanceof Number) { + config.addBonus(name, args[0] as int) + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckBlock.groovy new file mode 100644 index 0000000..28074d3 --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckBlock.groovy @@ -0,0 +1,16 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Check +import ru.nsu.nmashkin.task241.core.Config + +class CheckBlock { + private final Config config + + CheckBlock(Config config) { this.config = config } + + def methodMissing(String name, args) { + if (args.length == 1) { + config.addCheckCommand(new Check(args[0], name)) + } + } +} diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckpointsBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckpointsBlock.groovy new file mode 100644 index 0000000..b9cd2ca --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CheckpointsBlock.groovy @@ -0,0 +1,20 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Checkpoint +import ru.nsu.nmashkin.task241.core.Config +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class CheckpointsBlock { + private final Config config + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + CheckpointsBlock(Config config) { this.config = config } + + def methodMissing(String name, args) { + if (args.length == 1 && args[0] instanceof String) { + config.addCheckpoint(new Checkpoint(name, + LocalDate.parse(args[0] as CharSequence, DF))) + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/ConfigBuilder.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/ConfigBuilder.groovy new file mode 100644 index 0000000..c7c1654 --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/ConfigBuilder.groovy @@ -0,0 +1,76 @@ +package ru.nsu.nmashkin.task241.dsl + +import org.codehaus.groovy.control.CompilerConfiguration +import ru.nsu.nmashkin.task241.core.Config +import ru.nsu.nmashkin.task241.core.Group + +class ConfigBuilder { + private final Config config = new Config() + + void tasks(Closure closure) { + def block = new TasksBlock(config) + closure.delegate = block + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void group(String name, Closure closure) { + def group = new Group(name) + def block = new GroupBlock(group) + closure.delegate = block + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + config.addGroup(group) + } + + void checkpoints(Closure closure) { + closure.delegate = new CheckpointsBlock(config) + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void points(Closure closure) { + closure.delegate = new BonusBlock(config) + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void criteria(Closure closure) { + closure.delegate = new CriteriaBlock(config) + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void check(Closure closure) { + closure.delegate = new CheckBlock(config) + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void settings(Closure closure) { + closure.delegate = new SettingsBlock(config) + closure.resolveStrategy = Closure.DELEGATE_ONLY + closure.call() + } + + void importConfig(String path) { + def file = new File(path) + if (!file.exists()) { + System.err.println("[ERROR] Config file not found: $path") + return + } + + def cc = new CompilerConfiguration() + cc.setScriptBaseClass(DelegatingScript.class.getName()) + + def shell = new GroovyShell(this.class.classLoader, new Binding(), cc) + + DelegatingScript script = (DelegatingScript) shell.parse(file) + script.setDelegate(this) + script.run() + } + + Config build() { + return config + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CriteriaBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CriteriaBlock.groovy new file mode 100644 index 0000000..8738a5e --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/CriteriaBlock.groovy @@ -0,0 +1,13 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Config + +class CriteriaBlock { + private final Config config + + CriteriaBlock(Config config) { this.config = config } + + def methodMissing(String name, args) { + config.addGradeCriteria(args[0] as int, name); + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/GroupBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/GroupBlock.groovy new file mode 100644 index 0000000..072b725 --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/GroupBlock.groovy @@ -0,0 +1,20 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Group +import ru.nsu.nmashkin.task241.core.Student + +class GroupBlock { + private final Group group + + GroupBlock(Group group) { this.group = group } + + void student(String githubNick, String fullName, String repoUrl) { + group.addStudent(new Student(githubNick, fullName, repoUrl)) + } + + def methodMissing(String name, args) { + if (args.length == 2) { + group.addStudent(new Student(name, args[0], args[1])); + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/SettingsBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/SettingsBlock.groovy new file mode 100644 index 0000000..04b0a4c --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/SettingsBlock.groovy @@ -0,0 +1,23 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Config + +class SettingsBlock { + private final Config config + + SettingsBlock(Config config) { this.config = config; } + + void methodMissing(String name, String... args) { + if (name.equals("testTimeout")) { + if (args[1].endsWith('s')) { + config.setTestTimeoutSeconds(args[1].dropRight(1).toLong()) + } + if (args[1].endsWith('m')) { + config.setTestTimeoutSeconds(args[1].dropRight(1).toLong() * 60) + } + } + if (name.equals("skipAuthCheck")) { + config.setSkipAuthCheck(args[1].toBoolean()); + } + } +} diff --git a/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/TasksBlock.groovy b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/TasksBlock.groovy new file mode 100644 index 0000000..db44d20 --- /dev/null +++ b/Task_2_4_1/src/main/groovy/ru/nsu/nmashkin/task241/dsl/TasksBlock.groovy @@ -0,0 +1,27 @@ +package ru.nsu.nmashkin.task241.dsl + +import ru.nsu.nmashkin.task241.core.Config +import ru.nsu.nmashkin.task241.core.Task + +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class TasksBlock { + private final Config config + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + TasksBlock(Config config) { this.config = config } + + void task(String id, String name, Integer maxPoints, + String softDeadline, String hardDeadline) { + config.addTask(new Task(id, name, maxPoints, + LocalDate.parse(softDeadline, DF), LocalDate.parse(hardDeadline, DF))) + } + + def methodMissing(String name, args) { + if (args.length == 4) { + config.addTask(new Task(name, args[0] as String, args[1] as int, + LocalDate.parse(args[2] as String, DF), LocalDate.parse(args[3] as String, DF))) + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/Main.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/Main.java new file mode 100644 index 0000000..c5166ec --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/Main.java @@ -0,0 +1,151 @@ +package ru.nsu.nmashkin.task241; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import ru.nsu.nmashkin.task241.cli.ConfigLoader; +import ru.nsu.nmashkin.task241.core.Check; +import ru.nsu.nmashkin.task241.core.Config; +import ru.nsu.nmashkin.task241.core.Student; +import ru.nsu.nmashkin.task241.core.Task; +import ru.nsu.nmashkin.task241.service.BuildService; +import ru.nsu.nmashkin.task241.service.DocGenerator; +import ru.nsu.nmashkin.task241.service.GitService; +import ru.nsu.nmashkin.task241.service.HtmlReportGenerator; +import ru.nsu.nmashkin.task241.service.ScoreCalculator; +import ru.nsu.nmashkin.task241.service.StyleChecker; +import ru.nsu.nmashkin.task241.service.TaskVerificationResult; +import ru.nsu.nmashkin.task241.service.TestService; + +/** + * . + */ +public class Main { + + /** + * . + * + * @param build . + * @param docs . + * @param style . + * @param tests . + * @param score . + */ + public record TaskCheckResult(boolean build, boolean docs, boolean style, + TestService.TestResult tests, ScoreCalculator.ScoreResult score) { + static TaskCheckResult missing() { + return new TaskCheckResult(false, false, false, + new TestService.TestResult(0, 0, 0, false), + new ScoreCalculator.ScoreResult(0.0, "0", 0)); + } + } + + /** + * . + * + * @param student . + * @param taskId . + * @param taskResult . + * @param activityPercent . + */ + public record StudentData(Student student, String taskId, + TaskCheckResult taskResult, double activityPercent) {} + + /** + * . + * + * @param args . + * @throws Exception . + */ + public static void main(String[] args) throws Exception { + String configPath = args.length > 0 ? args[0] : "config/test_check.groovy"; + System.err.println("[INFO] Loading config: " + new File(configPath).getAbsolutePath()); + Config config = ConfigLoader.load(new File(configPath)); + + GitService git = new GitService(); + BuildService build = new BuildService(); + TestService test = new TestService(); + StyleChecker style = new StyleChecker(); + DocGenerator docs = new DocGenerator(); + ScoreCalculator scorer = new ScoreCalculator(); + + Set results = new HashSet<>(); + Map gitCloned = new HashMap<>(); + + for (Check check : config.getChecks()) { + Student student = config.getStudent(check.studentNick()); + Task task = config.getTasks().get(check.taskId()); + + System.err.println("[INFO] Processing: " + student.name() + " (" + task.name() + ")"); + Path tempDir; + Path repoDir; + + try { + tempDir = gitCloned.getOrDefault(student.nick(), null); + if (tempDir == null) { + System.err.println("[INFO] Cloning repo \"" + student.url() + "\""); + tempDir = Files.createTempDirectory("oop-" + student.nick()); + repoDir = git.cloneRepository(student.url(), tempDir, "default"); + gitCloned.put(student.nick(), tempDir); + } else { + repoDir = tempDir.resolve("repo"); + } + + Path taskDir = repoDir.resolve(task.id()); + + if (!Files.exists(taskDir)) { + results.add(new StudentData(student, task.id(), + TaskCheckResult.missing(), 100)); + System.err.println("[ERROR] Project not found: " + task.id()); + continue; + } + + System.err.println("[INFO] Building project: " + task.id()); + boolean buildOk = build.compileJava(taskDir); + boolean docsOk = false; + boolean styleOk = false; + + if (buildOk) { + System.err.println("[INFO] Generating doc: " + task.id()); + docsOk = docs.generateDocs(taskDir); + System.err.println("[INFO] Checking style: " + task.id()); + styleOk = style.checkStyle(taskDir); + } + + TestService.TestResult testRes; + if (buildOk && docsOk && styleOk) { + System.err.println("[INFO] Running tests: " + task.id()); + testRes = test.runTests(taskDir, config.getTestTimeoutSeconds()); + } else { + testRes = new TestService.TestResult(0, 0, 0, false); + } + + System.err.println("[INFO] Calculating score: " + task.id()); + ScoreCalculator.ScoreResult score = scorer.calculate(task, + new TaskVerificationResult(buildOk, docsOk, styleOk, testRes, null), + student.nick(), config); + + results.add(new StudentData(student, task.id(), + new TaskCheckResult(buildOk, docsOk, styleOk, testRes, score), + 100)); + System.err.println("[INFO] Score: " + score.numericScore()); + + } catch (Exception e) { + System.err.println("[ERROR] Fatal error: " + e.getMessage()); + } + + } + + for (Path tempDir : gitCloned.values()) { + git.cleanup(tempDir); + } + + System.err.println("[INFO] Generating HTML report..."); + HtmlReportGenerator htmlReportGenerator = new HtmlReportGenerator(); + htmlReportGenerator.generate(config, results, System.out); + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/cli/ConfigLoader.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/cli/ConfigLoader.java new file mode 100644 index 0000000..6a05beb --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/cli/ConfigLoader.java @@ -0,0 +1,43 @@ +package ru.nsu.nmashkin.task241.cli; + +import groovy.lang.GroovyShell; +import groovy.util.DelegatingScript; +import java.io.File; +import java.io.IOException; +import org.codehaus.groovy.control.CompilerConfiguration; +import ru.nsu.nmashkin.task241.core.Config; +import ru.nsu.nmashkin.task241.dsl.ConfigBuilder; + +/** + * . + */ +public class ConfigLoader { + /** + * . + * + * @param configFile . + * @return . + * @throws IOException . + */ + public static Config load(File configFile) throws IOException { + if (!configFile.exists()) { + throw new RuntimeException("Config not found: " + configFile.getAbsolutePath()); + } + + CompilerConfiguration cc = new CompilerConfiguration(); + cc.setScriptBaseClass(DelegatingScript.class.getName()); + + GroovyShell shell = new GroovyShell( + ConfigBuilder.class.getClassLoader(), + new groovy.lang.Binding(), cc); + + DelegatingScript script = (DelegatingScript) shell.parse(configFile); + + ConfigBuilder builder = new ConfigBuilder(); + script.setDelegate(builder); + + script.run(); + + return builder.build(); + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Check.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Check.java new file mode 100644 index 0000000..c8f011e --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Check.java @@ -0,0 +1,9 @@ +package ru.nsu.nmashkin.task241.core; + +/** + * . + * + * @param taskId . + * @param studentNick . + */ +public record Check(String taskId, String studentNick) {} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Checkpoint.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Checkpoint.java new file mode 100644 index 0000000..055b357 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Checkpoint.java @@ -0,0 +1,11 @@ +package ru.nsu.nmashkin.task241.core; + +import java.time.LocalDate; + +/** + * . + * + * @param id . + * @param date . + */ +public record Checkpoint(String id, LocalDate date) {} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Config.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Config.java new file mode 100644 index 0000000..487e01f --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Config.java @@ -0,0 +1,203 @@ +package ru.nsu.nmashkin.task241.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * . + */ +public class Config { + private final Map tasks = new HashMap<>(); + private final Map groups = new HashMap<>(); + private final List checkpoints = new ArrayList<>(); + private final List checks = new ArrayList<>(); + private final Map bonusPoints = new HashMap<>(); + private final Map gradeCriteria = new TreeMap<>(Collections.reverseOrder()); + + private long testTimeoutSeconds = 30; + private boolean skipAuthCheck = false; + + /** + * . + * + * @param task . + */ + public void addTask(Task task) { + tasks.put(task.id(), task); + } + + /** + * . + * + * @param group . + */ + public void addGroup(Group group) { + groups.put(group.name(), group); + } + + /** + * . + * + * @param cp . + */ + public void addCheckpoint(Checkpoint cp) { + checkpoints.add(cp); + } + + /** + * . + * + * @param cmd . + */ + public void addCheckCommand(Check cmd) { + checks.add(cmd); + } + + /** + * . + * + * @param studentNick . + * @param points . + */ + public void addBonus(String studentNick, int points) { + bonusPoints.put(studentNick, points); + } + + /** + * . + * + * @param minScore . + * @param grade . + */ + public void addGradeCriteria(int minScore, String grade) { + gradeCriteria.put(minScore, grade); + } + + /** + * . + * + * @param timeoutSeconds . + */ + public void setTestTimeoutSeconds(long timeoutSeconds) { + testTimeoutSeconds = timeoutSeconds; + } + + /** + * . + * + * @return . + */ + public Map getTasks() { + return tasks; + } + + /** + * . + * + * @return . + */ + public Map getGroups() { + return groups; + } + + /** + * . + * + * @return . + */ + public List getCheckpoints() { + return checkpoints; + } + + /** + * . + * + * @return . + */ + public List getChecks() { + return checks; + } + + /** + * . + * + * @return . + */ + public Map getBonusPoints() { + return bonusPoints; + } + + /** + * . + * + * @return . + */ + public Map getGradeCriteria() { + return gradeCriteria; + } + + /** + * . + * + * @return . + */ + public long getTestTimeoutSeconds() { + return testTimeoutSeconds; + } + + /** + * . + * + * @return . + */ + public boolean skipAuthCheck() { + return skipAuthCheck; + } + + /** + * . + * + * @param skipAuthCheck . + */ + public void setSkipAuthCheck(boolean skipAuthCheck) { + this.skipAuthCheck = skipAuthCheck; + } + + /** + * . + * + * @param nick . + * @return . + */ + public Student getStudent(String nick) { + return groups.values().stream() + .flatMap(g -> g.students().stream()) + .filter(s -> s.nick().equals(nick)) + .findAny().orElse(null); + } + + /** + * . + * + * @return . + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("\n=== Parsed Configuration ===\n"); + sb.append("Tasks: ").append(tasks.keySet()).append("\n"); + sb.append("Groups: ").append(groups.keySet()).append("\n"); + sb.append("Checkpoints: ").append(checkpoints).append("\n"); + sb.append("Checks: ").append(checks.size()).append(" command(s)\n"); + for (Check c : checks) { + sb.append(" -> ").append(c.taskId()).append(" / ") + .append(c.studentNick()).append("\n"); + } + sb.append("Bonuses: ").append(bonusPoints).append("\n"); + sb.append("Criteria: ").append(gradeCriteria).append("\n"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Group.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Group.java new file mode 100644 index 0000000..86f3943 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Group.java @@ -0,0 +1,30 @@ +package ru.nsu.nmashkin.task241.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * . + * + * @param name . + * @param students . + */ +public record Group(String name, List students) { + /** + * . + * + * @param name . + */ + public Group(String name) { + this(name, new ArrayList<>()); + } + + /** + * . + * + * @param student . + */ + public void addStudent(Student student) { + students.add(student); + } +} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Student.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Student.java new file mode 100644 index 0000000..30bf7b9 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Student.java @@ -0,0 +1,10 @@ +package ru.nsu.nmashkin.task241.core; + +/** + * . + * + * @param nick . + * @param name . + * @param url . + */ +public record Student(String nick, String name, String url) {} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Task.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Task.java new file mode 100644 index 0000000..2587c0c --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/core/Task.java @@ -0,0 +1,15 @@ +package ru.nsu.nmashkin.task241.core; + +import java.time.LocalDate; + +/** + * . + * + * @param id . + * @param name . + * @param maxScore . + * @param softDeadline . + * @param hardDeadline . + */ +public record Task(String id, String name, int maxScore, + LocalDate softDeadline, LocalDate hardDeadline) {} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/BuildService.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/BuildService.java new file mode 100644 index 0000000..7fcd2c4 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/BuildService.java @@ -0,0 +1,24 @@ +package ru.nsu.nmashkin.task241.service; + +import java.nio.file.Path; + +/** + * . + */ +public class BuildService { + /** + * . + * + * @param taskDir . + * @return . + */ + public boolean compileJava(Path taskDir) { + try { + GradleExecutor.run(taskDir, "compileJava", 0); + return true; + } catch (Exception e) { + System.err.println("[ERROR] Build failed: " + e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/DocGenerator.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/DocGenerator.java new file mode 100644 index 0000000..4ec87f2 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/DocGenerator.java @@ -0,0 +1,24 @@ +package ru.nsu.nmashkin.task241.service; + +import java.nio.file.Path; + +/** + * . + */ +public class DocGenerator { + /** + * . + * + * @param taskDir . + * @return . + */ + public boolean generateDocs(Path taskDir) { + try { + GradleExecutor.run(taskDir, "javadoc", 60); + return true; + } catch (Exception e) { + System.err.println("[ERROR] Javadoc generation failed"); + return false; + } + } +} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GitService.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GitService.java new file mode 100644 index 0000000..6e2dc3d --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GitService.java @@ -0,0 +1,87 @@ +package ru.nsu.nmashkin.task241.service; // Замените на ваш пакет + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * . + */ +public class GitService { + /** + * . + * + * @param repoUrl . + * @param targetDir . + * @param branch . + * @return . + * @throws IOException . + */ + public Path cloneRepository(String repoUrl, + Path targetDir, + String branch) throws IOException { + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + + String[] branchesToTry = branch.equalsIgnoreCase("default") + ? new String[]{"main", "master"} + : new String[]{branch}; + + for (String b : branchesToTry) { + Path cloneDir = targetDir.resolve("repo"); + if (tryClone(repoUrl, b, cloneDir)) { + return cloneDir; + } + + cleanup(cloneDir); + } + throw new IOException("Failed to clone repo." + + "Neither 'main' nor 'master' found or accessible."); + } + + private boolean tryClone(String url, String branch, Path dir) { + try { + ProcessBuilder pb = new ProcessBuilder( + "git", "clone", "--depth", "1", "--single-branch", + "--branch", branch, url, dir.toString() + ); + pb.redirectErrorStream(true); + Process p = pb.start(); + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(p.getInputStream()))) { + while (br.readLine() != null) { + + } + } + return p.waitFor() == 0; + } catch (Exception e) { + return false; + } + } + + /** + * . + * + * @param repoPath . + * @throws IOException . + */ + public void cleanup(Path repoPath) throws IOException { + if (repoPath == null || !Files.exists(repoPath)) { + return; + } + try (var stream = Files.walk(repoPath)) { + stream.sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GradleExecutor.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GradleExecutor.java new file mode 100644 index 0000000..344d2e8 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/GradleExecutor.java @@ -0,0 +1,82 @@ +package ru.nsu.nmashkin.task241.service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * . + */ +public class GradleExecutor { + + /** + * . + * + * @param projectDir . + * @param task . + * @param timeoutSec . + * @return . + * @throws IOException . + * @throws InterruptedException . + */ + public static boolean run(Path projectDir, + String task, + long timeoutSec) throws IOException, InterruptedException { + List command = buildGradleCommand(projectDir, task); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(projectDir.toFile()); + pb.redirectErrorStream(true); + + Process proc = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + + boolean finished = timeoutSec > 0 + ? proc.waitFor(timeoutSec, TimeUnit.SECONDS) + : proc.waitFor() == 0; + if (!finished) { + proc.destroyForcibly(); + System.err.println("[ERROR] Task \"" + task + "\" timed out after " + timeoutSec + "s"); + return true; + } + + if (proc.exitValue() != 0) { + System.err.println("[ERROR] Task \"" + task + + "'\" failed with code " + proc.exitValue() + "\n" + output); + } + return false; + } + + private static List buildGradleCommand(Path dir, String task) { + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); + Path wrapper = dir.resolve(isWindows ? "gradlew.bat" : "gradlew"); + + List cmd = new ArrayList<>(); + + if (Files.exists(wrapper)) { + if (isWindows) { + cmd.addAll(List.of("cmd", "/c", "gradlew.bat")); + } else { + cmd.add("./gradlew"); + } + } else { + cmd.add("gradle"); + } + + cmd.addAll(List.of(task, "--rerun-tasks", "--no-daemon", "--console=plain", "-q")); + return cmd; + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/HtmlReportGenerator.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/HtmlReportGenerator.java new file mode 100644 index 0000000..67de6f1 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/HtmlReportGenerator.java @@ -0,0 +1,116 @@ +package ru.nsu.nmashkin.task241.service; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import ru.nsu.nmashkin.task241.Main; +import ru.nsu.nmashkin.task241.core.Config; +import ru.nsu.nmashkin.task241.core.Group; + +/** + * . + */ +public class HtmlReportGenerator { + + /** + * . + * + * @param config . + * @param results . + * @param output . + * @throws IOException . + */ + public void generate(Config config, + Set results, + Appendable output) throws IOException { + output.append("") + .append("\n"); + + Map> byGroup = results.stream() + .collect(Collectors.groupingBy( + d -> config.getGroups().values().stream() + .filter(g -> g.students().contains(d.student())) + .findFirst() + .orElse(null), + LinkedHashMap::new, + Collectors.toList() + )); + + for (Map.Entry> entry : byGroup.entrySet()) { + Group group = entry.getKey(); + if (group == null) { + continue; + } + + List groupResults = entry.getValue(); + + output.append("

Group ").append(escapeHtml(group.name())).append("

\n") + .append("") + .append("" + + "") + .append("\n"); + + groupResults.stream() + .sorted(Comparator + .comparing((Main.StudentData d) -> d.student().name()) + .thenComparing(Main.StudentData::taskId)) + .forEach(data -> { + try { + Main.TaskCheckResult res = data.taskResult(); + System.err.println(res); + String build = res.build() ? "ok" : "fail"; + String docs = res.docs() ? "ok" : "fail"; + String style = res.style() ? "ok" : "fail"; + String tests = res.tests() != null ? res.tests().toString() : "-"; + String total = String.format("%.1f / %d", + res.score().numericScore(), + config.getTasks().get(data.taskId()).maxScore()); + + output.append("") + .append("").append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + output.append("
StudentTaskBuildDocStyleTestsTotal
").append(escapeHtml(data.student().name())) + .append("") + .append(escapeHtml(data.taskId())).append("").append(build).append("").append(docs).append("").append(style).append("").append(tests).append("").append(total).append("
\n"); + } + + output.append(""); + } + + private String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/ScoreCalculator.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/ScoreCalculator.java new file mode 100644 index 0000000..247fc02 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/ScoreCalculator.java @@ -0,0 +1,65 @@ +package ru.nsu.nmashkin.task241.service; + +import java.util.Map; +import ru.nsu.nmashkin.task241.core.Config; +import ru.nsu.nmashkin.task241.core.Task; + +/** + * . + */ +public class ScoreCalculator { + + /** + * . + * + * @param numericScore . + * @param grade . + * @param bonusApplied . + */ + public record ScoreResult(double numericScore, String grade, int bonusApplied) {} + + /** + * . + * + * @param task . + * @param verification . + * @param studentNick . + * @param config . + * @return . + */ + public ScoreResult calculate(Task task, TaskVerificationResult verification, + String studentNick, Config config) { + + if (!verification.buildOk() || !verification.docsOk() || !verification.styleOk()) { + return new ScoreResult(0.0, "0", 0); + } + + double baseScore = 0.0; + TestService.TestResult testRes = verification.tests(); + if (testRes != null) { + int total = testRes.passed() + testRes.failed() + testRes.skipped(); + if (total > 0) { + baseScore = ((double) testRes.passed() / total) * task.maxScore(); + } + } + + int bonus = config.getBonusPoints().getOrDefault(studentNick, 0); + double finalScore = Math.max(0.0, baseScore + bonus); + + String grade = mapToGrade(finalScore, config.getGradeCriteria()); + + return new ScoreResult(finalScore, grade, bonus); + } + + private String mapToGrade(double score, Map criteria) { + String result = "2"; + int maxMetCriteria = 0; + for (var entry : criteria.entrySet()) { + if (entry.getKey() >= maxMetCriteria && score >= entry.getKey()) { + maxMetCriteria = entry.getKey(); + result = entry.getValue(); + } + } + return result; + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/StyleChecker.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/StyleChecker.java new file mode 100644 index 0000000..b5e916c --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/StyleChecker.java @@ -0,0 +1,131 @@ +package ru.nsu.nmashkin.task241.service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.TimeUnit; + +/** + * . + */ +public class StyleChecker { + + private static final String CHECKSTYLE_JAR_URL = + "https://github.com/checkstyle/checkstyle/releases/" + + "download/checkstyle-10.17.0/checkstyle-10.17.0-all.jar"; + private static final String JAR_NAME = "checkstyle-10.17.0-all.jar"; + private final Path cacheDir; + + /** + * . + */ + public StyleChecker() { + this.cacheDir = Path.of(System.getProperty("user.home"), ".oop-checker", "tools"); + } + + /** + * . + * + * @param taskDir . + * @return . + */ + public boolean checkStyle(Path taskDir) { + try { + Path jarPath = downloadTool(); + Path configPath = extractConfigFromResources(); + Path src = taskDir.resolve("src"); + + if (!Files.exists(src)) { + System.err.println("[ERROR] \"src\" not found"); + return false; + } + + return runCheckstyle(jarPath, configPath, src); + } catch (Exception e) { + System.err.println("[ERROR] Style check failed: " + e.getMessage()); + return false; + } + } + + private Path downloadTool() throws IOException { + Files.createDirectories(cacheDir); + Path jarPath = cacheDir.resolve(JAR_NAME); + + if (!Files.exists(jarPath)) { + System.err.println("[INFO] Downloading checkstyle..."); + try (InputStream in = new URL(CHECKSTYLE_JAR_URL).openStream()) { + Files.copy(in, jarPath, StandardCopyOption.REPLACE_EXISTING); + } + } + return jarPath; + } + + private Path extractConfigFromResources() throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("google_style.xml"); + if (url == null) { + throw new IOException("No google_style.xml found in resources"); + } + + String urlPath = url.toString(); + if (urlPath.contains("!")) { + Path tempConfig = Files.createTempFile("checkstyle-config-", ".xml"); + try (InputStream in = url.openStream()) { + Files.copy(in, tempConfig, StandardCopyOption.REPLACE_EXISTING); + } + tempConfig.toFile().deleteOnExit(); + return tempConfig; + } + return Path.of(url.toURI()); + } + + private boolean runCheckstyle(Path jarPath, Path configPath, Path srcDir) + throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder( + "java", "-jar", jarPath.toString(), + "-c", configPath.toAbsolutePath().toString(), + srcDir.toAbsolutePath().toString() + ); + pb.redirectErrorStream(true); + Process proc = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + boolean finished = proc.waitFor(60, TimeUnit.SECONDS); + if (!finished) { + proc.destroyForcibly(); + System.err.println("[ERROR] Checkstyle timed out after 60s"); + return false; + } + + if (proc.exitValue() == 0) { + System.err.println("[INFO] Checkstyle: PASSED"); + return true; + } + + System.err.println("[WARNING] Style violations found:"); + String[] lines = output.toString().split("\n"); + int count = 0; + for (String line : lines) { + if (line.contains("[ERROR]") || line.contains("[WARN]")) { + String clean = line.replaceFirst("^.+\\b(src[/\\\\]main[/\\\\]java)", "$1"); + System.err.println(" " + clean); + count++; + } + } + System.err.println(" Total violations: " + count); + return false; + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TaskVerificationResult.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TaskVerificationResult.java new file mode 100644 index 0000000..07048c6 --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TaskVerificationResult.java @@ -0,0 +1,14 @@ +package ru.nsu.nmashkin.task241.service; + +/** + * . + * + * @param buildOk . + * @param docsOk . + * @param styleOk . + * @param tests . + * @param error . + */ +public record TaskVerificationResult( + boolean buildOk, boolean docsOk, boolean styleOk, + TestService.TestResult tests, String error) {} diff --git a/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TestService.java b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TestService.java new file mode 100644 index 0000000..ae9d3de --- /dev/null +++ b/Task_2_4_1/src/main/java/ru/nsu/nmashkin/task241/service/TestService.java @@ -0,0 +1,80 @@ +package ru.nsu.nmashkin.task241.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * . + */ +public class TestService { + + /** + * . + * + * @param passed . + * @param failed . + * @param skipped . + * @param timeout . + */ + public record TestResult(int passed, int failed, int skipped, boolean timeout) { + @Override + public String toString() { + return String.format("%d/%d/%d", passed, failed, skipped); + } + } + + /** + * . + * + * @param taskDir . + * @param timeoutSec . + * @return . + */ + public TestResult runTests(Path taskDir, long timeoutSec) { + try { + if (GradleExecutor.run(taskDir, "test", timeoutSec)) { + System.out.println("[INFO] Test execution timed out"); + return new TestResult(0, 0, 0, true); + } + return parseHtmlReport(taskDir); + } catch (Exception e) { + System.err.println("[ERROR] Test execution failed"); + return new TestResult(0, 0, 0, false); + } + } + + private TestResult parseHtmlReport(Path taskDir) throws IOException { + String html = Files.readString(taskDir.resolve("build/reports/tests/test/index.html")); + + int tests = extractBySelector(html, "id=\"tests\""); + int failures = extractBySelector(html, "id=\"failures\""); + int ignored = extractBySelector(html, "id=\"ignored\""); + + return new TestResult(Math.max(0, tests - failures - ignored), failures, ignored, false); + } + + private int extractBySelector(String html, String infoBoxSelector) { + int start = html.indexOf(infoBoxSelector); + if (start == -1) { + return 0; + } + + int counterStart = html.indexOf("
", start); + if (counterStart == -1) { + return 0; + } + + int valueStart = counterStart + "
".length(); + int valueEnd = html.indexOf("
", valueStart); + if (valueEnd == -1) { + return 0; + } + + try { + return Integer.parseInt(html.substring(valueStart, valueEnd).trim()); + } catch (NumberFormatException e) { + return 0; + } + } +} \ No newline at end of file diff --git a/Task_2_4_1/src/main/resources/META-INF/MANIFEST.MF b/Task_2_4_1/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..c7f58a6 --- /dev/null +++ b/Task_2_4_1/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: ru.nsu.nmashkin.task241.Main + diff --git a/Task_2_4_1/src/main/resources/google_style.xml b/Task_2_4_1/src/main/resources/google_style.xml new file mode 100644 index 0000000..c63eb28 --- /dev/null +++ b/Task_2_4_1/src/main/resources/google_style.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file