diff --git a/README.md b/README.md index 040487b2ae20..556d5b40ac88 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,9 @@ You can find a guide on [how to write documentation](docs/README.md). Setting up Artemis in your development environment or a demo production environment is really easy following the instructions on https://docs.artemis.cit.tum.de/dev/setup. When you want to support programming exercises, we recommend using the [Integrated Code Lifecycle](https://docs.artemis.cit.tum.de/dev/setup/#integrated-code-lifecycle-setup). Artemis can also be set up in conjunction with external tools for version control and continuous integration: -1. [GitLab and Jenkins](https://docs.artemis.cit.tum.de/dev/setup/#jenkins-and-gitlab-setup) -2. [GitLab and GitLab CI (experimental)](https://docs.artemis.cit.tum.de/dev/setup/#gitlab-ci-and-gitlab-setup) +1. [LocalVC and Jenkins](https://docs.artemis.cit.tum.de/dev/setup/#jenkins-and-localvc-setup) +2. [GitLab and Jenkins (deprecated)](https://docs.artemis.cit.tum.de/dev/setup/#jenkins-and-gitlab-setup) +3. [GitLab and GitLab CI (experimental and deprecated)](https://docs.artemis.cit.tum.de/dev/setup/#gitlab-ci-and-gitlab-setup) Artemis uses these external tools for user management and the configuration of programming exercises. diff --git a/build.gradle b/build.gradle index 8e18d5db388c..5377a61b745b 100644 --- a/build.gradle +++ b/build.gradle @@ -152,11 +152,8 @@ private excludedClassFilesForReport(classDirectories) { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ - "**/de/tum/in/www1/artemis/domain/**/*_*", - "**/de/tum/in/www1/artemis/config/migration/entries/**", - "**/de/tum/in/www1/artemis/service/connectors/pyris/dto/**", - "**/de/tum/in/www1/artemis/web/rest/iris/dto/**", - "**/org/eclipse/jgit/**", + "**/de/tum/cit/aet/artemis/**/domain/**/*_*", + "**/de/tum/cit/aet/artemis/core/config/migration/entries/**", "**/gradle-wrapper.jar/**" ] ) @@ -180,13 +177,13 @@ jacocoTestCoverageVerification { counter = "INSTRUCTION" value = "COVEREDRATIO" // TODO: in the future the following value should become higher than 0.92 - minimum = 0.895 + minimum = 0.894 } limit { counter = "CLASS" value = "MISSEDCOUNT" // TODO: in the future the following value should become less than 10 - maximum = 40 + maximum = 60 } } } @@ -231,7 +228,8 @@ dependencies { // implementation "com.offbytwo.jenkins:jenkins-client:0.3.8" implementation files("libs/jenkins-client-0.4.1.jar") // The following 4 dependencies are explicitly integrated as transitive dependencies of jenkins-client-0.4.0.jar - implementation "org.apache.httpcomponents.client5:httpclient5:5.3.1" + // NOTE: we cannot upgrade to the latest version for org.apache.httpcomponents because of exceptions in Docker Java + implementation "org.apache.httpcomponents.client5:httpclient5:5.3.1" // also used by Docker Java implementation "org.apache.httpcomponents.core5:httpcore5:5.2.5" implementation "org.apache.httpcomponents:httpmime:4.5.14" implementation("org.dom4j:dom4j:2.1.4") { @@ -256,6 +254,13 @@ dependencies { implementation "de.jplag:python-3:${jplag_version}" implementation "de.jplag:text:${jplag_version}" + // those are transitive dependencies of JPlag Text --> Stanford NLP + // Note: ideally we would exclude them, but for some reason this does not work + implementation "org.apache.lucene:lucene-queryparser:${lucene_version}" + implementation "org.apache.lucene:lucene-core:${lucene_version}" + implementation "org.apache.lucene:lucene-analyzers-common:${lucene_version}" + + // we have to override those values to use the latest version implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}" implementation "org.slf4j:jul-to-slf4j:${slf4j_version}" @@ -316,7 +321,13 @@ dependencies { implementation "org.jsoup:jsoup:1.18.1" implementation "commons-codec:commons-codec:1.17.1" // needed for spring security saml2 - implementation "org.springdoc:springdoc-openapi-ui:1.8.0" + // TODO: decide if we want to use OpenAPI and Swagger v3 +// implementation 'io.swagger.core.v3:swagger-annotations:2.2.23' +// implementation "org.springdoc:springdoc-openapi-ui:1.8.0" + + // use the latest version to avoid security vulnerabilities + implementation "org.springframework:spring-webmvc:6.1.13" + implementation "com.vdurmont:semver4j:3.1.0" implementation "com.github.docker-java:docker-java-core:${docker_java_version}" @@ -346,6 +357,9 @@ dependencies { implementation "com.fasterxml.jackson.datatype:jackson-datatype-hibernate6:${fasterxml_version}" // Support XML serialization and deserialization implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${fasterxml_version}" + // Support YML serialization and deserialization + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${fasterxml_version}" + implementation "com.hazelcast:hazelcast:${hazelcast_version}" implementation "com.hazelcast:hazelcast-spring:${hazelcast_version}" @@ -381,7 +395,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-oauth2-client:${spring_boot_version}" implementation "org.springframework.ldap:spring-ldap-core:3.2.6" - implementation "org.springframework.data:spring-data-ldap:3.3.3" + implementation "org.springframework.data:spring-data-ldap:3.3.4" implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:4.1.3") { // NOTE: these modules contain security vulnerabilities and are not needed @@ -393,8 +407,8 @@ dependencies { implementation "io.netty:netty-all:4.1.113.Final" implementation "io.projectreactor.netty:reactor-netty:1.1.22" - implementation "org.springframework:spring-messaging:6.1.12" - implementation "org.springframework.retry:spring-retry:2.0.8" + implementation "org.springframework:spring-messaging:6.1.13" + implementation "org.springframework.retry:spring-retry:2.0.9" implementation "org.springframework.security:spring-security-config:${spring_security_version}" implementation "org.springframework.security:spring-security-data:${spring_security_version}" @@ -402,7 +416,7 @@ dependencies { implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}" implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}" // use newest version of nimbus-jose-jwt to avoid security issues through outdated dependencies - implementation "com.nimbusds:nimbus-jose-jwt:9.40" + implementation "com.nimbusds:nimbus-jose-jwt:9.41.1" implementation "org.springframework.security:spring-security-oauth2-jose:${spring_security_version}" implementation "org.springframework.security:spring-security-crypto:${spring_security_version}" @@ -423,10 +437,6 @@ dependencies { implementation "org.bouncycastle:bcpkix-jdk18on:1.78.1" implementation "org.bouncycastle:bcprov-jdk18on:1.78.1" - implementation("io.springfox:springfox-swagger2:3.0.0") { - exclude module: "mapstruct" - } - implementation "io.springfox:springfox-bean-validators:3.0.0" implementation "com.mysql:mysql-connector-j:9.0.0" implementation "org.postgresql:postgresql:42.7.4" @@ -438,10 +448,14 @@ dependencies { // NOTE: 3.0.2 is broken for splitting lecture specific PDFs implementation "org.apache.pdfbox:pdfbox:3.0.1" implementation "org.apache.commons:commons-csv:1.11.0" - implementation "org.commonmark:commonmark:0.22.0" + implementation "org.commonmark:commonmark:0.23.0" implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" + implementation "org.jgrapht:jgrapht-core:1.5.2" + // use the latest version explicitly to avoid security vulnerabilities (currently Artemis and JPlag rely on jgrapht 1.5.2 which relies on apfloat) + implementation "org.apfloat:apfloat:1.14.0" + // use newest version of guava to avoid security issues through outdated dependencies implementation "com.google.guava:guava:33.3.0-jre" implementation "com.sun.activation:jakarta.activation:2.0.1" @@ -450,7 +464,7 @@ dependencies { implementation "com.google.code.gson:gson:2.11.0" - implementation "com.google.errorprone:error_prone_annotations:2.31.0" + implementation "com.google.errorprone:error_prone_annotations:2.32.0" // NOTE: we want to keep the same unique version for all configurations, implementation and annotationProcessor implementation("net.bytebuddy:byte-buddy") { @@ -506,13 +520,15 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test:${spring_boot_version}") { exclude group: "org.junit.vintage", module: "junit-vintage-engine" exclude group: "com.vaadin.external.google", module: "android-json" + exclude group: "org.xmlunit", module: "xmlunit-core" } testImplementation "org.springframework.security:spring-security-test:${spring_security_version}" testImplementation "org.springframework.boot:spring-boot-test:${spring_boot_version}" testImplementation "org.assertj:assertj-core:3.26.3" testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" - testImplementation "io.github.classgraph:classgraph:4.8.175" + + testImplementation "io.github.classgraph:classgraph:4.8.176" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" testImplementation "org.gradle:gradle-tooling-api:8.10.1" diff --git a/docker/gitlab/gitlab-local-setup.sh b/docker/gitlab/gitlab-local-setup.sh index 8a29ab818082..4e979c3ab2ea 100644 --- a/docker/gitlab/gitlab-local-setup.sh +++ b/docker/gitlab/gitlab-local-setup.sh @@ -47,7 +47,7 @@ artemis: " echo -echo 'The access Jenkins token has been created and can be copied into your jenkins-casc-config.yml file:' +echo 'The access Jenkins token has been created and can be copied into your jenkins-casc-config-gitlab.yml file:' echo " credentials: system: diff --git a/docker/jenkins.yml b/docker/jenkins.yml index 86b532593197..ca4f35492475 100644 --- a/docker/jenkins.yml +++ b/docker/jenkins.yml @@ -13,7 +13,7 @@ services: - artemis-jenkins-data:/var/jenkins_home - ./jenkins/jenkins-disable-csrf.groovy:/var/jenkins_home/init.groovy # Disable CSRF token - ./jenkins/master-key-only-for-testing.key:/var/jenkins_home/master.key # Preset master key to use pre-generated secrets - - ./jenkins/jenkins-casc-config.yml:/usr/share/jenkins/ref/jenkins-casc-config.yml:ro + - ./jenkins/jenkins-casc-config-gitlab.yml:/usr/share/jenkins/ref/jenkins-casc-config.yml:ro - /var/run/docker.sock:/var/run/docker.sock ports: - "8082:8080" diff --git a/docker/jenkins/jenkins-casc-config.yml b/docker/jenkins/jenkins-casc-config-gitlab.yml similarity index 100% rename from docker/jenkins/jenkins-casc-config.yml rename to docker/jenkins/jenkins-casc-config-gitlab.yml diff --git a/docker/jenkins/jenkins-casc-config-localvc.yml b/docker/jenkins/jenkins-casc-config-localvc.yml new file mode 100644 index 000000000000..36766ce7dd01 --- /dev/null +++ b/docker/jenkins/jenkins-casc-config-localvc.yml @@ -0,0 +1,33 @@ +jenkins: + systemMessage: "Jenkins has been configured to run with Artemis\n\n" + securityRealm: + local: + allowsSignup: false + users: + - id: "artemis_admin" # Artemis admin username + password: "artemis_admin" # Artemis admin password + labelString: "docker" + authorizationStrategy: + projectMatrix: + permissions: + - "Overall/Administer:artemis_admin" # Make sure that the username matches the Artemis admin username + - "Overall/Read:authenticated" +credentials: + system: + domainCredentials: + - credentials: + - string: + id: artemis_notification_plugin_token # Value of the "artemis-authentication-token-key" defined key in application-artemis.yml + description: 'Access token for Artemis Test Notification Plugin' + scope: GLOBAL + secret: artemis_admin # Value of the "artemis-authentication-token-value" key defined in application-artemis.yml + - usernamePassword: + id: artemis_localvc_credentials + scope: GLOBAL + username: "jenkins" # artemis.version-control.build-agent-git-username + password: "artemis_admin" # artemis.version-control.build-agent-git-password + description: "LocalVC credentials for Artemis" +unclassified: + timestamper: + elapsedTimeFormat: "''yyyy-MM-dd'T'HH:mm:ssX' '" + systemTimeFormat: "''yyyy-MM-dd'T'HH:mm:ssX' '" diff --git a/docker/jenkins/plugins.yml b/docker/jenkins/plugins.yml index 028d098e34fb..859200f9f59a 100644 --- a/docker/jenkins/plugins.yml +++ b/docker/jenkins/plugins.yml @@ -2,7 +2,6 @@ plugins: - artifactId: matrix-auth - artifactId: docker-workflow - - artifactId: gitlab-plugin - artifactId: pipeline-maven - artifactId: timestamper - artifactId: configuration-as-code diff --git a/docs/dev/setup.rst b/docs/dev/setup.rst index 3ed91396dc39..01c6a0acec05 100644 --- a/docs/dev/setup.rst +++ b/docs/dev/setup.rst @@ -46,7 +46,8 @@ following dependencies/tools on your machine: There are multiple stacks available for the integration with Artemis: * :ref:`Integrated Code Lifecycle Setup ` - * :ref:`GitLab and Jenkins ` + * :ref:`LocalVC and Jenkins ` + * :ref:`GitLab and Jenkins ` (deprecated) * :ref:`GitLab and GitLab CI ` (experimental, not yet production ready) ------------------------------------------------------------------------------------------------------------------------ @@ -74,6 +75,7 @@ following dependencies/tools on your machine: setup/server setup/client setup/integrated-code-lifecycle + setup/jenkins-localvc setup/jenkins-gitlab setup/gitlabci-gitlab setup/aeolus diff --git a/docs/dev/setup/jenkins-gitlab.rst b/docs/dev/setup/jenkins-gitlab.rst index 37672cbca460..3adbbabac478 100644 --- a/docs/dev/setup/jenkins-gitlab.rst +++ b/docs/dev/setup/jenkins-gitlab.rst @@ -3,6 +3,12 @@ Jenkins and GitLab Setup ------------------------ +.. warning:: + + GitLab support will be removed with Artemis 8.0.0. + Please use :ref:`LocalVC and Jenkins ` instead for new installations with Jenkins as the CI system. + For existing Jenkins and GitLab Setups, you can migrate to LocalVC with this `not merged Pull Request `__. + This section describes how to set up a programming exercise environment based on Jenkins and GitLab. Optional commands are in curly brackets ``{}``. @@ -465,8 +471,8 @@ Automated Jenkins Server Setup The following steps describe how to deploy a pre-configured version of the Jenkins server. This is ideal as a quickstart for developers. For a more detailed setup, see `Manual Jenkins Server Setup <#manual-jenkins-server-setup>`__. -In a production setup, you have to at least change the user credentials (in the file ``jenkins-casc-config.yml``) and -generate random access tokens and push tokens. +In a production setup, you have to at least change the user credentials (in the file ``jenkins-casc-config-gitlab.yml``) and +generate random access tokens. 1. Create a new access token in GitLab named ``Jenkins`` and give it **api** and **read_repository** rights. You can do either do it manually or using the following command: @@ -479,46 +485,46 @@ do either do it manually or using the following command: 2. You can now first build and deploy Jenkins, then you can also start the other services which weren't started yet: - .. code:: bash + .. code:: bash - JAVA_OPTS=-Djenkins.install.runSetupWizard=false docker compose -f docker/.yml up --build -d jenkins - docker compose -f docker/.yml up -d + JAVA_OPTS=-Djenkins.install.runSetupWizard=false docker compose -f docker/.yml up --build -d jenkins + docker compose -f docker/.yml up -d Jenkins is then reachable under ``http://localhost:8082/`` and you can login using the credentials specified - in ``jenkins-casc-config.yml`` (defaults to ``artemis_admin`` as both username and password). + in ``jenkins-casc-config-gitlab.yml`` (defaults to ``artemis_admin`` as both username and password). -3. The `application-local.yml` must be adapted with the values configured in ``jenkins-casc-config.yml``: +3. The `application-local.yml` must be adapted with the values configured in ``jenkins-casc-config-gitlab.yml``: -.. code:: yaml + .. code:: yaml - artemis: - user-management: - use-external: false - internal-admin: - username: artemis_admin - password: artemis_admin - version-control: - url: http://localhost:8081 - user: artemis_admin - password: artemis_admin - continuous-integration: - user: artemis_admin - password: artemis_admin - url: http://localhost:8082 - vcs-credentials: artemis_gitlab_admin_credentials - artemis-authentication-token-key: artemis_notification_plugin_token - artemis-authentication-token-value: artemis_admin + artemis: + user-management: + use-external: false + internal-admin: + username: artemis_admin + password: artemis_admin + version-control: + url: http://localhost:8081 + user: artemis_admin + password: artemis_admin + continuous-integration: + user: artemis_admin + password: artemis_admin + url: http://localhost:8082 + vcs-credentials: artemis_gitlab_admin_credentials + artemis-authentication-token-key: artemis_notification_plugin_token + artemis-authentication-token-value: artemis_admin 4. Open the ``src/main/resources/config/application-jenkins.yml`` and change the following: Again, if you are using a development setup, the template in the beginning of this page already contains the correct values. -.. code:: yaml + .. code:: yaml - jenkins: - internal-urls: - ci-url: http://jenkins:8080 - vcs-url: http://gitlab:80 + jenkins: + internal-urls: + ci-url: http://jenkins:8080 + vcs-url: http://gitlab:80 5. You're done. You can now run Artemis with the GitLab/Jenkins environment. @@ -685,6 +691,18 @@ Start Jenkins user: your.chosen.username password: your.chosen.password +11. In a local setup, you have to disable CSRF otherwise some API endpoints will return HTTP Status 403 Forbidden. + This is done be executing the following command: + ``docker compose -f docker/.yml exec -T jenkins dd of=/var/jenkins_home/init.groovy < docker/jenkins/jenkins-disable-csrf.groovy`` + + The last step is to disable the ``use-crumb`` option in ``application-local.yml``: + + .. code:: yaml + + jenkins: + use-crumb: false + + Required Jenkins Plugins """""""""""""""""""""""" @@ -698,22 +716,19 @@ The list of plugins is maintained in ``docker/jenkins/plugins.yml``. You will need to install the following plugins (apart from the recommended ones that got installed during the setup process): -1. `GitLab `__ for enabling - webhooks to and from GitLab - -2. `Timestamper `__ for adding the +1. `Timestamper `__ for adding the time to every line of the build output (Timestamper might already be installed) -3. `Pipeline `__ for defining the +2. `Pipeline `__ for defining the build description using declarative files (Pipeline might already be installed) **Note:** This is a suite of plugins that will install multiple plugins -4. `Pipeline Maven `__ to use maven within the pipelines. If you want to +3. `Pipeline Maven `__ to use maven within the pipelines. If you want to use Docker for your build agents you may also need to install `Docker Pipeline `__ . -5. `Matrix Authorization Strategy Plugin `__ for configuring permissions +4. `Matrix Authorization Strategy Plugin `__ for configuring permissions for users on a project and build plan level (Matrix Authorization Strategy might already be installed). @@ -855,45 +870,6 @@ GitLab Repository Access continuous-integration: vcs-credentials: the.id.of.the.username.and.password.credentials.from.jenkins -GitLab to Jenkins push notification token -""""""""""""""""""""""""""""""""""""""""" - -GitLab has to notify Jenkins build plans if there are any new commits to -the repository. The push notification that gets sent here is secured by -a token generated by Jenkins. In order to get this token, you have to do -the following steps: - -1. Create a new item in Jenkins (use the Freestyle project type) and - name it **TestProject** - -2. In the project configuration, go to *Build Triggers → Build when a - change is pushed to GitLab* and activate this option - -3. Click on *Advanced*. - -4. You will now have a couple of new options here, one of them being a - “**Secret token**”. - -5. Click on the “*Generate*” button right below the text box for that - token. - -6. Copy the generated value, let’s call it **$gitlab-push-token** - -7. Apply these change to the plan (i.e. click on *Apply*) - - .. figure:: jenkins-gitlab/jenkins_test_project.png - :align: center - -8. In a local setup, you have to disable CSRF otherwise some API endpoints will return HTTP Status 403 Forbidden. - This is done be executing the following command: - ``docker compose -f docker/.yml exec -T jenkins dd of=/var/jenkins_home/init.groovy < docker/jenkins/jenkins-disable-csrf.groovy`` - - The last step is to disable the ``use-crumb`` option in ``application-local.yml``: - - .. code:: yaml - - jenkins: - use-crumb: false Upgrading Jenkins """"""""""""""""" @@ -956,7 +932,7 @@ You should also update the Jenkins plugins regularly due to security reasons. You can update them directly in the Web User Interface in the Plugin Manager. -.. _jenkins_build_agents: +.. _jenkins_build_agents_gitlab: Build agents ^^^^^^^^^^^^ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_project_config_xml.png b/docs/dev/setup/jenkins-gitlab/jenkins_project_config_xml.png deleted file mode 100644 index 4d5cc9bda468..000000000000 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_project_config_xml.png and /dev/null differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png b/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png deleted file mode 100644 index f37c0dd6ff6f..000000000000 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png and /dev/null differ diff --git a/docs/dev/setup/jenkins-localvc.rst b/docs/dev/setup/jenkins-localvc.rst new file mode 100644 index 000000000000..384affe8dc05 --- /dev/null +++ b/docs/dev/setup/jenkins-localvc.rst @@ -0,0 +1,779 @@ +.. _Jenkins and LocalVC Setup: + +Jenkins and LocalVC Setup +------------------------- + +This section describes how to set up a programming exercise environment +based on Jenkins and LocalVC, which is integrated in Artemis. Optional commands are in curly brackets ``{}``. + +The following assumes that all instances run on separate servers. If you +have one single server, or your own NGINX instance, just skip all NGINX +related steps and use the configurations provided under *Separate NGINX +Configurations* + +**If you want to setup everything on your local development computer, +ignore all NGINX related steps.** **Just make sure that you use +unique port mappings for your Docker containers (e.g.** ``8081`` **for +Jenkins,** ``8080`` **for Artemis)** + +**Prerequisites:** + +* `Docker `__ + + Make sure that docker has enough memory (~ 6GB). To adapt it, go to ``Preferences -> Resources`` and restart Docker. + +.. contents:: Content of this section + :local: + :depth: 3 + +Artemis +^^^^^^^ + +In order to use Artemis with Jenkins as **Continuous Integration** +Server and LocalVC as integrated **Version Control** Server, you have to configure +the file ``application-prod.yml`` (Production Server) or +``application-artemis.yml`` (Local Development) accordingly. Please note +that all values in ``<..>`` have to be configured properly. These values +will be explained below in the corresponding sections. If you want to set up a local environment, copy the values +below into your ``application-artemis.yml`` or ``application-local.yml`` file (the latter is recommended). + +.. code:: yaml + + artemis: + course-archives-path: ./exports/courses + repo-clone-path: ./repos + repo-download-clone-path: ./repos-download + bcrypt-salt-rounds: 11 # The number of salt rounds for the bcrypt password hashing. Lower numbers make it faster but more unsecure and vice versa. + # Please use the bcrypt benchmark tool to determine the best number of rounds for your system. https://github.com/ls1intum/bcrypt-Benchmark + user-management: + use-external: false + internal-admin: + username: artemis_admin + password: artemis_admin + accept-terms: false + login: + account-name: TUM + version-control: + url: http://localhost:8080 + user: root + password: dummy # has to be set, but does not matter for LocalVC + build-agent-git-username: jenkins + build-agent-git-password: artemis_admin # choose some strong password and username (gives read access to all repositories) + continuous-integration: + user: artemis_admin + password: artemis_admin + url: http://localhost:8082 + vcs-credentials: artemis_localvc_credentials + artemis-authentication-token-key: artemis_notification_plugin_token + artemis-authentication-token-value: artemis_admin + build-timeout: 30 + git: + name: Artemis + email: artemis@xcit.tum.de + jenkins: + # only required if Artemis and Jenkins cannot communicate on their public URLs + # e.g., Jenkins is only available in a local container network + internal-urls: + ci-url: http://jenkins:8080 + vcs-url: http://172.17.0.1:8080 # `http://host.docker.internal:8080` for Windows + use-crumb: false + server: + port: 8080 + url: http://172.17.0.1:8080 # `http://host.docker.internal:8080` for Windows + +In addition, you have to start Artemis with the profiles ``localvc`` and +``jenkins`` so that the correct adapters will be used, e.g.: + +:: + + --spring.profiles.active=dev,jenkins,localvc,artemis,scheduling + +Please read :ref:`Server Setup` for more details. + +For a local setup on Windows you can use `http://host.docker.internal` appended +by the chosen ports as the continuous-integration url and the internal vcs url. + +Make sure to change the ``server.url`` and ``artemis.version-control.url`` value in ``application-dev.yml`` +or ``application-prod.yml`` accordingly. The ``server.url`` value will be used for the +communication hooks from Jenkins to Artemis. +In case you use a different port than 80 (http) or 443 (https) for the +communication, you have to append it to the both urls value, +e.g. \ ``127.0.0.1:8080``. + +When you start Artemis for the first time, it will automatically create +an admin user. + +**Note:** Sometimes Artemis does not generate the admin user which may lead to a startup +error. You will have to create the user manually in the MySQL database. Make sure +both are set up correctly and follow these steps: + +1. Use the tool mentioned above to generate a password hash. +2. Connect to the database via a client like `MySQL Workbench `__ + and execute the following query to create the user. Replace `artemis_admin` and `HASHED_PASSWORD` with your + chosen username and password: + + .. code:: sql + + INSERT INTO `artemis`.`jhi_user` (`id`,`login`,`password_hash`,`first_name`,`last_name`,`email`, + `activated`,`lang_key`,`activation_key`,`reset_key`,`created_by`,`created_date`,`reset_date`, + `last_modified_by`,`last_modified_date`,`image_url`,`last_notification_read`,`registration_number`) + VALUES (1,"artemis_admin","HASHED_PASSWORD","artemis","administrator","artemis_admin@localhost", + 1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +3. Give the user admin and user roles: + + .. code:: sql + + INSERT INTO `artemis`.`jhi_user_authority` (`user_id`, `authority_name`) VALUES (1,"ROLE_ADMIN"); + INSERT INTO `artemis`.`jhi_user_authority` (`user_id`, `authority_name`) VALUES (1,"ROLE_USER"); + +Starting the Artemis server should now succeed. + +Jenkins +^^^^^^^ + +Automated Jenkins Server Setup +"""""""""""""""""""""""""""""" + +The following steps describe how to deploy a pre-configured version of the Jenkins server. +This is ideal as a quickstart for developers. For a more detailed setup, see +`Manual Jenkins Server Setup <#manual-jenkins-server-setup>`__. +In a production setup, you have to at least change the user credentials (in the file ``jenkins-casc-config-localvc.yml``). + +#. You can now first build and deploy Jenkins, then you can also start the other services which weren't started yet: + + .. code:: bash + + JAVA_OPTS=-Djenkins.install.runSetupWizard=false docker compose -f docker/.yml up --build -d jenkins + docker compose -f docker/.yml up -d + + Jenkins is then reachable under ``http://localhost:8082/`` and you can login using the credentials specified + in ``jenkins-casc-config-localvc.yml`` (defaults to ``artemis_admin`` as both username and password). + +#. The `application-local.yml` must be adapted with the values configured in ``jenkins-casc-config-localvc.yml``: + + .. code:: yaml + + artemis: + user-management: + use-external: false + internal-admin: + username: artemis_admin + password: artemis_admin + version-control: + url: http://localhost:8080 + user: root + password: dummy # have to be set, but does not matter for LocalVC + build-agent-git-username: jenkins + build-agent-git-password: artemis_admin # choose some strong password and username (gives read access to all repositories) + continuous-integration: + user: artemis_admin + password: artemis_admin + url: http://localhost:8082 + vcs-credentials: artemis_localvc_credentials + artemis-authentication-token-key: artemis_notification_plugin_token + artemis-authentication-token-value: artemis_admin + +#. Open the ``src/main/resources/config/application-jenkins.yml`` and change the following: + Again, if you are using a development setup, the template in the beginning of this page already contains the + correct values. + + .. code:: yaml + + jenkins: + internal-urls: + ci-url: http://jenkins:8080 + vcs-url: http://172.17.0.1:8080 # `http://host.docker.internal:8080` for Windows + +#. You're done. You can now run Artemis with the LocalVC/Jenkins environment. + +Manual Jenkins Server Setup +""""""""""""""""""""""""""" + +1. Pull the latest Jenkins LTS Docker image + + Run the following command to get the latest jenkins LTS docker image. + + .. code:: bash + + docker pull jenkins/jenkins:lts + +Nginx proxy setup +################# + +If you run your own NGINX or if you install Jenkins on a local development computer, you can skip this section. + +2. Create a file increasing the maximum file size for the Nginx proxy. + The nginx-proxy uses a default file limit that is too small for the + plugin that will be uploaded later. **Skip this step if you have your + own NGINX instance.** + + .. code:: bash + + echo "client_max_body_size 16m;" > client_max_body_size.conf + +3. The NGINX default timeout is pretty low. For plagiarism check and unlocking student repos for the exam a higher + timeout is advisable. Therefore we write our own nginx.conf and load it in the container. + + + .. code:: nginx + + user nginx; + worker_processes auto; + + error_log /var/log/nginx/error.log warn; + pid /var/run/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + fastcgi_read_timeout 300; + proxy_read_timeout 300; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; + } + daemon off + +4. Run the NGINX proxy docker container, this will automatically setup + all reverse proxies and force https on all connections. (This image + would also setup proxies for all other running containers that have + the VIRTUAL_HOST and VIRTUAL_PORT environment variables). **Skip this + step if you have your own NGINX instance.** + + .. code:: bash + + docker run -itd --name nginx_proxy \ + -p 80:80 -p 443:443 \ + --restart always \ + -v /var/run/docker.sock:/tmp/docker.sock:ro \ + -v /etc/nginx/certs \ + -v /etc/nginx/vhost.d \ + -v /usr/share/nginx/html \ + -v $(pwd)/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro \ + -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \ + jwilder/nginx-proxy + +5. The nginx proxy needs another docker-container to generate + letsencrypt certificates. Run the following command to start it (make + sure to change the email-address). **Skip this step if you have your + own NGINX instance.** + + .. code:: bash + + docker run --detach \ + --name nginx_proxy-letsencrypt \ + --volumes-from nginx_proxy \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --env "DEFAULT_EMAIL=mail@yourdomain.tld" \ + jrcs/letsencrypt-nginx-proxy-companion + +Start Jenkins +############# + +6. Run Jenkins by executing the following command (change the hostname + and choose which port alternative you need) + + .. code:: bash + + docker run -itd --name jenkins \ + --restart always \ + -v jenkins_data:/var/jenkins_home \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /usr/bin/docker:/usr/bin/docker:ro \ + -e VIRTUAL_HOST=your.jenkins.domain -e VIRTUAL_PORT=8080 \ # Alternative 1: If you are NOT using a separate NGINX instance + -e LETSENCRYPT_HOST=your.jenkins.domain \ # Only needed if Alternative 1 is used + -p 8082:8080 \ # Alternative 2: If you ARE using a separate NGINX instance OR you ARE installing Jenkins on a local development computer + -u root \ + jenkins/jenkins:lts + + Note that you can omit the ``-u root``, ``-v /var/run/docker.sock:/var/run/docker.sock`` and + ``-v /usr/bin/docker:/usr/bin/docker:ro`` parameters, if you do not want to run Docker builds on the Jenkins controller + (but e.g. use remote agents). + +7. Open Jenkins in your browser (e.g. ``localhost:8082``) and setup the + admin user account (install all suggested plugins). You can get the + initial admin password using the following command. + + .. code:: bash + + # Jenkins highlights the password in the logs, you can't miss it + docker logs -f jenkins + or alternatively + docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword + +8. Set the chosen credentials in the Artemis configuration + *application-artemis.yml* + + .. code:: yaml + + artemis: + continuous-integration: + user: your.chosen.username + password: your.chosen.password + +Required Jenkins Plugins +"""""""""""""""""""""""" + +**Note:** The custom Jenkins Dockerfile takes advantage of the +`Plugin Installation Manager Tool for Jenkins `__ +to automatically install the plugins listed below. If you used the Dockerfile, you can skip these steps and +`Server Notification Plugin <#server-notification-plugin>`__. +The list of plugins is maintained in ``docker/jenkins/plugins.yml``. + + +You will need to install the following plugins (apart from the +recommended ones that got installed during the setup process): + +1. `Timestamper `__ for adding the + time to every line of the build output (Timestamper might already be installed) + +2. `Pipeline `__ for defining the + build description using declarative files (Pipeline might already be installed) + + **Note:** This is a suite of plugins that will install multiple plugins + +3. `Pipeline Maven `__ to use maven within the pipelines. If you want to + use Docker for your build agents you may also need to install + `Docker Pipeline `__ . + +4. `Matrix Authorization Strategy Plugin `__ for configuring permissions + for users on a project and build plan level (Matrix Authorization Strategy might already be installed). + + +The plugins above (and the pipeline-setup associated with it) got introduced in Artemis 4.7.3. +If you are using exercises that were created before 4.7.3, you also have to install these plugins: + +Please note that this setup is **deprecated** and will be removed in the future. +Please migrate to the new pipeline-setup if possible. + +1. `Multiple SCMs `__ for combining the + exercise test and assignment repositories in one build + +2. `Post Build Task `__ for preparing build + results to be exported to Artemis + +3. `Xvfb `__ for exercises based on GUI + libraries, for which tests have to have some virtual display + +Choose “Download now and install after restart” and checking the +“Restart Jenkins when installation is complete and no jobs are running” box + +Timestamper Configuration +""""""""""""""""""""""""" + +Go to *Manage Jenkins → System Configuration → Configure*. There you will find the +Timestamper configuration, use the following value for both formats: + +:: + + ''yyyy-MM-dd'T'HH:mm:ssX' ' + +.. figure:: jenkins-gitlab/timestamper_config.png + :align: center + +Server Notification Plugin +"""""""""""""""""""""""""" + +Artemis needs to receive a notification after every build, which +contains the test results and additional commit information. For that +purpose, we developed a Jenkins plugin, that can aggregate and *POST* +JUnit formatted results to any URL. + +You can download the current release of the plugin +`here `__ +(Download the **.hpi** file). Go to the Jenkins plugin page (*Manage +Jenkins → System Configuration → Plugins*) and install the downloaded file under the +*Advanced settings* tab under *Deploy Plugin* + +.. figure:: jenkins-gitlab/jenkins_custom_plugin.png + :align: center + +Jenkins Credentials +""""""""""""""""""" + +Go to *Manage Jenkins → Security → Credentials → Jenkins → Global credentials* and create the +following credentials + +Server Notification Token +######################### + +1. Create a new Jenkins credential containing the token, which gets send + by the server notification plugin to Artemis with every build result: + + 1. **Kind**: Secret text + 2. **Scope**: Global + 3. **Secret**: *your.secret_token_value* (choose any value you want, + copy it for the nex step) + 4. Leave the ID field blank + 5. The description is up to you + +2. Copy the generated ID of the new credentials and put it into the + Artemis configuration *application-artemis.yml* + + .. code:: yaml + + artemis: + continuous-integration: + artemis-authentication-token-key: the.id.of.the.notification.token.credential + +3. Copy the actual value you chose for the token and put it into the + Artemis configuration *application-artemis.yml* + + .. code:: yaml + + artemis: + continuous-integration: + artemis-authentication-token-value: the.actual.value.of.the.notification.token + +LocalVC Repository Access +######################### + +1. Create a new Jenkins credentials containing the username and password + of the build-agent-git-user: + + 1. **Kind**: Username with password + 2. **Scope**: Global + 3. **Username**: *the_username_you_chose_at_build-agent-git-username* + 4. **Password**: *the_password_you_chose_at_build-agent-git-password* + 5. Leave the ID field blank + 6. The description is up to you + +2. Copy the generated ID (e.g. ``ea0e3c08-4110-4g2f-9c83-fb2cdf6345fa``) + of the new credentials and put it into the Artemis configuration file + *application-artemis.yml* + + .. code:: yaml + + artemis: + continuous-integration: + vcs-credentials: the.id.of.the.username.and.password.credentials.from.jenkins + +Upgrading Jenkins +""""""""""""""""" + +In order to upgrade Jenkins to a newer version, you need to rebuild the Docker image targeting the new version. +The stable LTS versions can be viewed through the `changelog `__ +and the corresponding Docker image can be found on +`dockerhub `__. + +1. Open the Jenkins Dockerfile and replace the value of ``FROM`` with ``jenkins/jenkins:lts``. + After running the command ``docker pull jenkins/jenkins:lts``, this will use the latest LTS version + in the following steps. + You can also use a specific LTS version. + For example, if you want to upgrade Jenkins to version ``2.289.2``, you will need to use the + ``jenkins/jenkins:2.289.2-lts`` image. + +2. If you're using ``docker compose``, you can simply use the following command and skip the next steps. + + .. code:: bash + + docker compose -f docker/.yml up --build -d + +3. Build the new Docker image: + + .. code:: bash + + docker build --no-cache -t jenkins-artemis . + + The name of the image is called ``jenkins-artemis``. + +4. Stop the current Jenkins container (change jenkins to the name of your container): + + .. code:: bash + + docker stop jenkins + +5. Rename the container to ``jenkins_old`` so that it can be used as a backup: + + .. code:: bash + + docker rename jenkins jenkins_old + +6. Run the new Jenkins instance: + + .. code:: bash + + docker run -itd --name jenkins --restart always \ + -v jenkins_data:/var/jenkins_home \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p 9080:8080 jenkins-artemis \ + +7. You can remove the backup container if it's no longer needed: + + .. code:: bash + + docker rm jenkins_old + + +You should also update the Jenkins plugins regularly due to security +reasons. You can update them directly in the Web User Interface in the +Plugin Manager. + +.. _jenkins_build_agents: + +Build agents +^^^^^^^^^^^^ + +You can either run the builds locally (that means on the machine that hosts Jenkins) or on remote build agents. + +Configuring local build agents +"""""""""""""""""""""""""""""" + +Go to `Manage Jenkins` → `Nodes` → `Built-In Node` → `Configure` + +Configure your master node like this (adjust the number of executors, if needed). Make sure to add the docker label. + + .. figure:: jenkins-gitlab/jenkins_local_node.png + :align: center + + Jenkins local node + +Alternative local build agents setup using docker +""""""""""""""""""""""""""""""""""""""""""""""""" + +An alternative way of adding a build agent that will use docker (similar to the remote agents below) but running +locally, can be done using the jenkins/ssh-agent docker image `docker image `__. + +Prerequisites: + +1. Make sure to have Docker `installed `__ + +Agent setup: + +1. Create a new SSH key using ``ssh-keygen`` (if a passphrase is added, store it for later) + +2. Copy the public key content (e.g. in ~/.ssh/id_rsa.pub) + +3. Run: + + .. code:: bash + + docker run -d --name jenkins_agent -v /var/run/docker.sock:/var/run/docker.sock \ + jenkins/ssh-agent:latest "" + +4. Get the GID of the 'docker' group with ``cat /etc/groups`` and remember it for later + +5. Enter the agent's container with ``docker exec -it jenkins_agent bash`` + +6. Install Docker with ``apt update && apt install docker.io`` + +7. Check if group 'docker' already exists with ``cat /etc/groups``. If yes, remove it with ``groupdel docker`` + +8. Add a new 'docker' group with the same GID as seen in point 2 with ``groupadd -g docker`` + +9. Add 'jenkins' user to the group with ``usermod -aG docker jenkins`` + +10. Activate changes with ``newgrp docker`` + +11. Now check if 'jenkins' has the needed permissions to run docker commands + + 1. Log in as 'jenkins' with ``su jenkins`` + + 2. Try if ``docker inspect `` works or if a permission error occurs + + 3. If an permission error occurs, try to restart the docker container + +12. Now you can exit the container executing ``exit`` twice (the first will exit the jenkins user and + the second the container) + +Add agent in Jenkins: + +1. Open Jenkins in your browser (e.g. localhost:8082) + +2. Go to Manage Jenkins → Credentials → System → Global credentials (unrestricted) → Add Credentials + + - Kind: SSH Username with private key + + - Scope: Global (Jenkins, nodes, items, all child items, etc) + + - ID: leave blank + + - Description: Up to you + + - Username: jenkins + + - Private Key: (e.g /root/.ssh/id_rsa) + + - Passphrase: (you can leave it blank if none has been specified) + + .. figure:: jenkins-gitlab/alternative_jenkins_node_credentials.png + :align: center + +3. Go to Manage Jenkins → Nodes → New Node + + - Node name: Up to you (e.g. Docker agent node) + + - Check 'Permanent Agent' + + .. figure:: jenkins-gitlab/alternative_jenkins_node_setup.png + :align: center + +4. Node settings: + + - # of executors: Up to you (e.g. 4) + + - Remote root directory: /home/jenkins/agent + + - Labels: docker + + - Usage: Only build jobs with label expressions matching this node + + - Launch method: Launch agents via SSH + + - Host: output of command ``docker inspect --format '{{ .Config.Hostname }}' jenkins_agent`` + + - Credentials: + + - Host Key Verification Strategy: Non verifying Verification Strategy + + - Availability: Keep this agent online as much as possible + + .. figure:: jenkins-gitlab/alternative_jenkins_node.png + :align: center + +5. Save the new node + +6. Node should now be up and running + +Installing remote build agents +"""""""""""""""""""""""""""""" +You might want to run the builds on additional Jenkins agents, especially if a large amount of students should use +the system at the same time. +Jenkins supports remote build agents: The actual compilation of the students submissions happens on these +other machines but the whole process is transparent to Artemis. + +This guide explains setting up a remote agent on an Ubuntu virtual machine that supports docker builds. + +Prerequisites: +1. Install Docker on the remote machine: https://docs.docker.com/engine/install/ubuntu/ + +2. Add a new user to the remote machine that Jenkins will use: ``sudo adduser --disabled-password --gecos "" jenkins`` + +3. Add the jenkins user to the docker group (This allows the jenkins user to interact with docker): + ``sudo usermod -a -G docker jenkins`` + +4. Generate a new SSH key locally (e.g. using ``ssh-keygen``) and add the public key to the ``.ssh/authorized_keys`` + file of the jenkins user on the agent VM. + +5. Validate that you can connect to the build agent machine using SSH and the generated private key and validate that + you can use docker (`docker ps` should not show an error) + +6. Log in with your normal account on the build agent machine and install Java: ``sudo apt install default-jre`` + +7. Add a new secret in Jenkins, enter private key you just generated and add the passphrase, if set: + + .. figure:: jenkins-gitlab/jenkins_ssh_credentials.png + :align: center + + Jenkins SSH Credentials + +8. Add a new node (select a name and select `Permanent Agent`): + Set the number of executors so that it matches your machine's specs: This is the number of concurrent builds + this agent can handle. It is recommended to match the number of cores of the machine, + but you might want to adjust this later if needed. + + Set the remote root directory to ``/home/jenkins/remote_agent``. + + Set the usage to `Only build jobs with label expressions matching this node`. + This ensures that only docker-jobs will be built on this agent, and not other jobs. + + Add a label ``docker`` to the agent. + + Set the launch method to `Launch via SSH` and add the host of the machine. + Select the credentials you just created and select `Manually trusted key Verification Strategy` + as Host key verification Strategy. + Save it. + + + .. figure:: jenkins-gitlab/jenkins_node.png + :align: center + + Add a Jenkins node + +9. Wait for some moments while jenkins installs it's remote agent on the agent's machine. + You can track the progress using the `Log` page when selecting the agent. System information should also be available. + +10. Change the settings of the master node to be used only for specific jobs. + This ensures that the docker tasks are not executed on the master agent but on the remote agent. + + + .. figure:: jenkins-gitlab/jenkins_master_node.png + :align: center + + Adjust Jenkins master node settings + +11. You are finished, the new agent should now also process builds. + + +Jenkins User Management +^^^^^^^^^^^^^^^^^^^^^^^ + +Artemis supports user management in Jenkins as of version 4.11.0. Creating an account in Artemis will also create an +account on Jenkins using the same password. This enables users to login and access Jenkins. Updating and/or deleting +users from Artemis will also lead to updating and/or deleting from Jenkins. + +Unfortunately, Jenkins does not provide a Rest API for user management which present the following **caveats**: + + - The username of a user is treated as a unique identifier in Jenkins. + - It's not possible to update an existing user with a single request. + We update by deleting the user from Jenkins and recreating it with the updated data. + - In Jenkins, users are created in an on-demand basis. + For example, when a build is performed, its change log is computed and as a result commits from users + who Jenkins has never seen may be discovered and created. + - Since Jenkins users may be re-created automatically, issues may occur such as 1) creating a user, deleting it, + and then re-creating it and 2) changing the username of the user and reverting back to the previous one. + - Updating a user will re-create it in Jenkins and therefore remove any additionally saved Jenkins-specific + user data such as API access tokens. + + +Jenkins Build Plan Access Control Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Artemis takes advantage of the Project-based Matrix Authorization Strategy plugin to support build plan +access control in Jenkins. +This enables specific Artemis users to access build plans and execute actions such as triggering a build. +This section explains the changes required in Jenkins in order to set up build plan access control: + +1. Navigate to Manage Jenkins → Plugins → Installed plugins and make sure that you have the + `Matrix Authorization Strategy `__ plugin installed + +2. Navigate to Manage Jenkins → Security and navigate to the "Authorization" section + +3. Select the "Project-based Matrix Authorization Strategy" option + +4. In the table make sure that the "Read" permission under the "Overall" section is assigned to + the "Authenticated Users" user group. + +5. In the table make sure that all "Administer" permission is assigned to all administrators. + +6. You are finished. If you want to fine-tune permissions assigned to teaching assistants and/or instructors, + you can change them within the ``JenkinsJobPermission.java`` file. + +.. figure:: jenkins-gitlab/jenkins_authorization_permissions.png + :align: center + + +Caching +^^^^^^^ + +You can configure caching for e.g. Maven repositories. +See :ref:`this section in the administration documentation ` for more details. diff --git a/docs/requirements.txt b/docs/requirements.txt index e009c5eb21e1..fdf26e7ee025 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,8 @@ Sphinx==7.4.7 sphinx-rtd-theme==2.0.0 -sphinx-autobuild==2024.4.16 -sphinxcontrib-bibtex==2.6.2 +sphinx-autobuild==2024.9.19 +sphinxcontrib-bibtex==2.6.3 +requests==2.32.3 +zipp==3.20.2 +docutils==0.20.1 +urllib3==2.2.3 diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 3675a9e4c2b0..6d0b257dd96e 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -35,6 +35,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | Rust | yes | yes | +----------------------+----------+---------+ + | JavaScript | yes | yes | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -67,6 +69,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | Rust | no | no | no | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | JavaScript | no | no | no | no | n/a | no | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/gradle.properties b/gradle.properties index 5cac2d2e4f08..0ddd8b8f3fdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ npm_version=10.8.0 # Dependency versions jhipster_dependencies_version=8.7.0 -spring_boot_version=3.3.3 +spring_boot_version=3.3.4 spring_security_version=6.3.3 # TODO: upgrading to 6.6.0 currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code hibernate_version=6.4.10.Final @@ -21,6 +21,9 @@ jgit_version=7.0.0.202409031743-r sshd_version=2.13.2 checkstyle_version=10.18.1 jplag_version=5.1.0 +# not really used in Artemis, nor Jplag, nor the used version of Stanford CoreNLP, but we use the latest to avoid security vulnerabilities +# NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it +lucene_version=8.11.3 slf4j_version=2.0.16 sentry_version=7.14.0 liquibase_version=4.29.2 diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index 66a042ffea0e..fd0cb47e843b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -450,10 +450,11 @@ public Result getResultForParticipationAndCheckAccess(Long participationId, Long /** * Get a map of result ids to the respective build job ids if build log files for this build job exist. * - * @param results the results for which to check the availability of build logs + * @param results the results for which to check the availability of build logs + * @param participation the participation the results belong to * @return a map of result ids to respective build job ids if the build log files exist, null otherwise */ - public Map getLogsAvailabilityForResults(List results) { + public Map getLogsAvailabilityForResults(List results, Participation participation) { Map logsAvailability = new HashMap<>(); @@ -466,7 +467,7 @@ public Map getLogsAvailabilityForResults(List results) { String buildJobId = resultBuildJobSet.get(resultId); if (buildJobId != null) { - if (buildLogEntryService.buildJobHasLogFile(buildJobId)) { + if (buildLogEntryService.buildJobHasLogFile(buildJobId, ((ProgrammingExerciseParticipation) participation).getProgrammingExercise())) { logsAvailability.put(resultId, buildJobId); } else { diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index ea8f2fcaa290..1692beaa7d69 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -187,7 +187,7 @@ public ResponseEntity> getBuildJobIdsForResultsOfParticipation Participation participation = participationRepository.findByIdElseThrow(participationId); List results = resultRepository.findAllByParticipationIdOrderByCompletionDateDesc(participationId); - Map resultBuildJobMap = resultService.getLogsAvailabilityForResults(results); + Map resultBuildJobMap = resultService.getLogsAvailabilityForResults(results, participation); participationAuthCheckService.checkCanAccessParticipationElseThrow(participation); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java index 77cedf02539a..5031332a8862 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java @@ -40,7 +40,6 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.util.TimeLogUtil; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; /** @@ -110,7 +109,7 @@ public ResponseEntity createMessage(@PathVariable Long courseId, @Valid @R */ @GetMapping("courses/{courseId}/messages") @EnforceAtLeastStudent - public ResponseEntity> getMessages(@ApiParam Pageable pageable, PostContextFilterDTO postContextFilter, Principal principal) { + public ResponseEntity> getMessages(Pageable pageable, PostContextFilterDTO postContextFilter, Principal principal) { long timeNanoStart = System.nanoTime(); Page coursePosts; diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java index 3a714eb5cd33..911ea5cbfbff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java @@ -31,7 +31,6 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.util.TimeLogUtil; import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupService; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; /** @@ -73,7 +72,7 @@ public NotificationResource(NotificationRepository notificationRepository, UserR */ @GetMapping("notifications") @EnforceAtLeastStudent - public ResponseEntity> getAllNotificationsForCurrentUserFilteredBySettings(@ApiParam Pageable pageable) { + public ResponseEntity> getAllNotificationsForCurrentUserFilteredBySettings(Pageable pageable) { long start = System.nanoTime(); User currentUser = userRepository.getUserWithGroupsAndAuthorities(); log.info("REST request to get notifications page {} with size {} for current user {} filtered by settings", pageable.getPageNumber(), pageable.getPageSize(), diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/SystemNotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/SystemNotificationResource.java index 2fd24822e321..2d797d153f8a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/SystemNotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/SystemNotificationResource.java @@ -23,7 +23,6 @@ import de.tum.cit.aet.artemis.communication.repository.SystemNotificationRepository; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; import tech.jhipster.web.util.ResponseUtil; @@ -51,7 +50,7 @@ public SystemNotificationResource(SystemNotificationRepository systemNotificatio */ @GetMapping("system-notifications") @EnforceAtLeastTutor - public ResponseEntity> getAllSystemNotifications(@ApiParam Pageable pageable) { + public ResponseEntity> getAllSystemNotifications(Pageable pageable) { log.debug("REST request to get all Courses the user has access to"); final Page page = systemNotificationRepository.findAll(pageable); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AuditResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AuditResource.java index 03be23e493f2..03acab9f2da7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AuditResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AuditResource.java @@ -24,7 +24,6 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAdmin; import de.tum.cit.aet.artemis.core.service.AuditEventService; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.ResponseUtil; /** @@ -49,7 +48,7 @@ public AuditResource(AuditEventService auditEventService) { */ @GetMapping("audits") @EnforceAdmin - public ResponseEntity> getAll(@ApiParam Pageable pageable) { + public ResponseEntity> getAll(Pageable pageable) { Page page = auditEventService.findAll(pageable); HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); @@ -65,8 +64,7 @@ public ResponseEntity> getAll(@ApiParam Pageable pageable) { */ @GetMapping(value = "audits", params = { "fromDate", "toDate" }) @EnforceAdmin - public ResponseEntity> getByDates(@RequestParam(value = "fromDate") LocalDate fromDate, @RequestParam(value = "toDate") LocalDate toDate, - @ApiParam Pageable pageable) { + public ResponseEntity> getByDates(@RequestParam(value = "fromDate") LocalDate fromDate, @RequestParam(value = "toDate") LocalDate toDate, Pageable pageable) { Instant from = fromDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); Instant to = toDate.atStartOfDay(ZoneId.systemDefault()).plusDays(1).toInstant(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java index 6b403e5602e0..289d296c5624 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java @@ -45,7 +45,6 @@ import de.tum.cit.aet.artemis.core.service.user.UserCreationService; import de.tum.cit.aet.artemis.core.service.user.UserService; import de.tum.cit.aet.artemis.core.util.HeaderUtil; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; import tech.jhipster.web.util.ResponseUtil; @@ -237,7 +236,7 @@ public ResponseEntity syncUserViaLdap(@PathVariable Long userId) { */ @GetMapping("users") @EnforceAdmin - public ResponseEntity> getAllUsers(@ApiParam UserPageableSearchDTO userSearch) { + public ResponseEntity> getAllUsers(UserPageableSearchDTO userSearch) { final Page page = userRepository.getAllManagedUsers(userSearch); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java index 6f035a7695bb..40be2685e58e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java @@ -112,7 +112,6 @@ import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; import de.tum.cit.aet.artemis.exercise.service.SubmissionService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; /** @@ -513,7 +512,7 @@ private void checkExamAttendanceCheckSettings(Exam exam) { */ @GetMapping("exams/active") @EnforceAtLeastInstructor - public ResponseEntity> getAllActiveExams(@ApiParam Pageable pageable) { + public ResponseEntity> getAllActiveExams(Pageable pageable) { final var user = userRepository.getUserWithGroupsAndAuthorities(); Page page = examService.getAllActiveExams(pageable, user); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java b/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java index f1bb6ed318c1..1dbfede94a83 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java @@ -36,7 +36,6 @@ import de.tum.cit.aet.artemis.lti.domain.LtiPlatformConfiguration; import de.tum.cit.aet.artemis.lti.repository.LtiPlatformConfigurationRepository; import de.tum.cit.aet.artemis.lti.service.LtiDeepLinkingService; -import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; /** @@ -113,7 +112,7 @@ public ResponseEntity lti13DeepLinking(@PathVariable Long courseId, @Req */ @GetMapping("lti-platforms") @EnforceAtLeastInstructor - public ResponseEntity> getAllConfiguredLtiPlatforms(@ApiParam Pageable pageable) { + public ResponseEntity> getAllConfiguredLtiPlatforms(Pageable pageable) { log.info("REST request to get all configured LTI platforms"); Page platformsPage = ltiPlatformConfigurationRepository.findAll(pageable); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), platformsPage); diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingAssessmentResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingAssessmentResource.java index 721dc7c91d7c..5b781c0b657b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingAssessmentResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingAssessmentResource.java @@ -28,7 +28,6 @@ import de.tum.cit.aet.artemis.assessment.service.AssessmentService; import de.tum.cit.aet.artemis.assessment.web.AssessmentResource; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.exception.ErrorConstants; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; @@ -43,8 +42,6 @@ import de.tum.cit.aet.artemis.modeling.dto.ModelingAssessmentDTO; import de.tum.cit.aet.artemis.modeling.repository.ModelingExerciseRepository; import de.tum.cit.aet.artemis.modeling.repository.ModelingSubmissionRepository; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; /** * REST controller for managing ModelingAssessment. @@ -58,10 +55,6 @@ public class ModelingAssessmentResource extends AssessmentResource { private static final String ENTITY_NAME = "modelingAssessment"; - private static final String PUT_SUBMIT_ASSESSMENT_200_REASON = "Given assessment has been saved and used for automatic assessment by Compass"; - - private static final String POST_ASSESSMENT_AFTER_COMPLAINT_200_REASON = "Assessment has been updated after complaint"; - private final ModelingExerciseRepository modelingExerciseRepository; private final AuthorizationCheckService authCheckService; @@ -114,8 +107,6 @@ public ResponseEntity getModelingExampleAssessment(@PathVariable long ex * @return result after saving/submitting modeling assessment */ @ResponseStatus(HttpStatus.OK) - @ApiResponses({ @ApiResponse(code = 200, message = PUT_SUBMIT_ASSESSMENT_200_REASON, response = Result.class), - @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON) }) @PutMapping("modeling-submissions/{submissionId}/result/{resultId}/assessment") @EnforceAtLeastTutor public ResponseEntity saveModelingAssessment(@PathVariable long submissionId, @PathVariable long resultId, @@ -132,8 +123,6 @@ public ResponseEntity saveModelingAssessment(@PathVariable long submissi * @return result after saving example modeling assessment */ @ResponseStatus(HttpStatus.OK) - @ApiResponses({ @ApiResponse(code = 200, message = PUT_SUBMIT_ASSESSMENT_200_REASON, response = Result.class), - @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON) }) @PutMapping("modeling-submissions/{exampleSubmissionId}/example-assessment") @EnforceAtLeastTutor public ResponseEntity saveModelingExampleAssessment(@PathVariable long exampleSubmissionId, @RequestBody List feedbacks) { @@ -151,8 +140,6 @@ public ResponseEntity saveModelingExampleAssessment(@PathVariable long e * @return the updated result */ @ResponseStatus(HttpStatus.OK) - @ApiResponses({ @ApiResponse(code = 200, message = POST_ASSESSMENT_AFTER_COMPLAINT_200_REASON, response = Result.class), - @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON) }) @PutMapping("modeling-submissions/{submissionId}/assessment-after-complaint") @EnforceAtLeastTutor public ResponseEntity updateModelingAssessmentAfterComplaint(@PathVariable Long submissionId, @RequestBody AssessmentUpdateDTO assessmentUpdate) { diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java index 75ee0c51c4bb..95a41a0f695a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java @@ -29,11 +29,9 @@ import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion; import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository; -import de.tum.cit.aet.artemis.assessment.service.ResultService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.exception.ErrorConstants; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; @@ -53,8 +51,6 @@ import de.tum.cit.aet.artemis.modeling.repository.ModelingSubmissionRepository; import de.tum.cit.aet.artemis.modeling.service.ModelingSubmissionService; import de.tum.cit.aet.artemis.plagiarism.service.PlagiarismService; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; /** * REST controller for managing ModelingSubmission. @@ -71,8 +67,6 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource { @Value("${jhipster.clientApp.name}") private String applicationName; - private static final String GET_200_SUBMISSIONS_REASON = ""; - private final ModelingSubmissionService modelingSubmissionService; private final ModelingSubmissionRepository modelingSubmissionRepository; @@ -85,7 +79,7 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource { private final PlagiarismService plagiarismService; - public ModelingSubmissionResource(SubmissionRepository submissionRepository, ResultService resultService, ModelingSubmissionService modelingSubmissionService, + public ModelingSubmissionResource(SubmissionRepository submissionRepository, ModelingSubmissionService modelingSubmissionService, ModelingExerciseRepository modelingExerciseRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExerciseRepository exerciseRepository, GradingCriterionRepository gradingCriterionRepository, ExamSubmissionService examSubmissionService, StudentParticipationRepository studentParticipationRepository, ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService) { @@ -170,8 +164,6 @@ private ResponseEntity handleModelingSubmission(Long exercis * @return a list of modeling submissions */ @ResponseStatus(HttpStatus.OK) - @ApiResponses({ @ApiResponse(code = 200, message = GET_200_SUBMISSIONS_REASON, response = ModelingSubmission.class, responseContainer = "List"), - @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON), }) @GetMapping("exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastTutor public ResponseEntity> getAllModelingSubmissions(@PathVariable Long exerciseId, @RequestParam(defaultValue = "false") boolean submittedOnly, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java new file mode 100644 index 000000000000..3caec801ed53 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java @@ -0,0 +1,24 @@ +package de.tum.cit.aet.artemis.programming.domain; + +public enum AuthenticationMechanism { + /** + * The user used password to authenticate to the LocalVC + */ + PASSWORD, + /** + * The user used the participation+user token to authenticate to the LocalVC + */ + PARTICIPATION_VCS_ACCESS_TOKEN, + /** + * The user used the user token to authenticate to the LocalVC + */ + USER_VCS_ACCESS_TOKEN, + /** + * The user used SSH user token to authenticate to the LocalVC + */ + SSH, + /** + * The user used the artemis client code editor to authenticate to the LocalVC + */ + CODE_EDITOR +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index ea435ac5ea5a..4206bfe15dbc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -48,7 +48,8 @@ public enum ProgrammingLanguage { ASSEMBLER, SWIFT, OCAML, - RUST + RUST, + JAVASCRIPT ); // @formatter:on diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/VcsAccessLog.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/VcsAccessLog.java new file mode 100644 index 000000000000..560ca52a31c1 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/VcsAccessLog.java @@ -0,0 +1,107 @@ +package de.tum.cit.aet.artemis.programming.domain; + +import java.time.ZonedDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; +import de.tum.cit.aet.artemis.programming.web.repository.RepositoryActionType; + +/** + * A Vcs access log entry. + */ +@Entity +@Table(name = "vcs_access_log") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class VcsAccessLog extends DomainObject { + + @ManyToOne + private User user; + + @ManyToOne + private Participation participation; + + @Column(name = "name") + private String name; + + @Column(name = "email") + private String email; + + @Column(name = "repository_action_type", nullable = false) + @Enumerated(EnumType.ORDINAL) + private RepositoryActionType repositoryActionType; + + @Column(name = "authentication_mechanism", nullable = false) + @Enumerated(EnumType.ORDINAL) + private AuthenticationMechanism authenticationMechanism; + + @Column(name = "commit_hash") + private String commitHash; + + @Column(name = "ip_address") + private String ipAddress; + + @Column(name = "timestamp") + private ZonedDateTime timestamp; + + public VcsAccessLog(User user, Participation participation, String name, String email, RepositoryActionType repositoryActionType, + AuthenticationMechanism authenticationMechanism, String commitHash, String ipAddress) { + this.user = user; + this.participation = participation; + this.name = name; + this.email = email; + this.repositoryActionType = repositoryActionType; + this.authenticationMechanism = authenticationMechanism; + this.commitHash = commitHash; + this.ipAddress = ipAddress; + this.timestamp = ZonedDateTime.now(); + } + + public VcsAccessLog() { + } + + public void setCommitHash(String commitHash) { + this.commitHash = commitHash; + } + + public User getUser() { + return user; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getCommitHash() { + return commitHash; + } + + public ZonedDateTime getTimestamp() { + return timestamp; + } + + public AuthenticationMechanism getAuthenticationMechanism() { + return authenticationMechanism; + } + + public RepositoryActionType getRepositoryActionType() { + return repositoryActionType; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/dto/VcsAccessLogDTO.java b/src/main/java/de/tum/cit/aet/artemis/programming/dto/VcsAccessLogDTO.java new file mode 100644 index 000000000000..b9896cf6efbe --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/dto/VcsAccessLogDTO.java @@ -0,0 +1,29 @@ +package de.tum.cit.aet.artemis.programming.dto; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; + +/** + * DTO representing a VCS access log entry. + * + * @param id The id of the access log entry. + * @param userId The user's id associated with the access log event. + * @param name The name associated with the user. + * @param email The email associated with the user. + * @param repositoryActionType The type of action performed in the repository (read or write). + * @param authenticationMechanism The method the user used for authenticating to the repository. + * @param commitHash The latest commit hash at the access event. + * @param timestamp The date and time when the access event occurred. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record VcsAccessLogDTO(Long id, Long userId, String name, String email, String repositoryActionType, String authenticationMechanism, String commitHash, + ZonedDateTime timestamp) { + + public static VcsAccessLogDTO of(VcsAccessLog vcsAccessLog) { + return new VcsAccessLogDTO(vcsAccessLog.getId(), vcsAccessLog.getUser().getId(), vcsAccessLog.getName(), vcsAccessLog.getEmail(), + vcsAccessLog.getRepositoryActionType().name(), vcsAccessLog.getAuthenticationMechanism().name(), vcsAccessLog.getCommitHash(), vcsAccessLog.getTimestamp()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java index d97b7f03abd7..dfce5b56ef52 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java @@ -114,4 +114,10 @@ default Page findAllWithDataByCourseId(long courseId, Pageable pageabl """) List getBuildJobsResultsStatistics(@Param("fromDateTime") ZonedDateTime fromDateTime, @Param("courseId") Long courseId); + Optional findByBuildJobId(String buildJobId); + + default BuildJob findByBuildJobIdElseThrow(String buildJobId) { + return getValueElseThrow(findByBuildJobId(buildJobId)); + } + } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java index f943d7824b84..32739a6eed1e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java @@ -992,4 +992,14 @@ public String getFetchPath() { return fetchPath; } } + + /** + * Find a programming exercise by its id and throw an Exception if it cannot be found + * + * @param programmingExerciseId of the programming exercise. + * @return The programming exercise related to the given id + */ + default ProgrammingExercise findByIdElseThrow(long programmingExerciseId) { + return getValueElseThrow(findById(programmingExerciseId)); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/VcsAccessLogRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/VcsAccessLogRepository.java new file mode 100644 index 000000000000..af342179e111 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/VcsAccessLogRepository.java @@ -0,0 +1,74 @@ +package de.tum.cit.aet.artemis.programming.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; +import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; + +/** + * Spring Data JPA repository for the User entity.
+ *
+ *

+ * Note: Please keep in mind that the User entities are soft-deleted when adding new queries to this repository. + * If you don't need deleted user entities, add `WHERE user.isDeleted = FALSE` to your query. + *

+ */ +@Profile(PROFILE_LOCALVC) +@Repository +public interface VcsAccessLogRepository extends ArtemisJpaRepository { + + /** + * Find the access log entry which does not have any commit hash yet + * + * @param participationId The id of the participation the repository belongs to + * @return a log entry belonging to the participationId, which has no commit hash + */ + @Query(""" + SELECT vcsAccessLog + FROM VcsAccessLog vcsAccessLog + WHERE vcsAccessLog.participation.id = :participationId + AND vcsAccessLog.commitHash IS NULL + ORDER BY vcsAccessLog.timestamp DESC + LIMIT 1 + """) + Optional findNewestByParticipationIdWhereCommitHashIsNull(@Param("participationId") long participationId); + + /** + * Retrieves a list of {@link VcsAccessLog} entities associated with the specified participation ID. + * The results are ordered by the log ID in ascending order. + * + * @param participationId the ID of the participation to filter the access logs by. + * @return a list of {@link VcsAccessLog} entities for the given participation ID, sorted by log ID in ascending order. + */ + @Query(""" + SELECT vcsAccessLog + FROM VcsAccessLog vcsAccessLog + WHERE vcsAccessLog.participation.id = :participationId + ORDER BY vcsAccessLog.id ASC + """) + List findAllByParticipationId(@Param("participationId") long participationId); + + /** + * Retrieves a list of {@link VcsAccessLog} entities associated with the specified participation ID. + * The results are ordered by the log ID in ascending order. + * + * @param date The date before which all log ids should be fetched + * + * @return a list of ids of the access logs, which have a timestamp before the date + */ + @Query(""" + SELECT vcsAccessLog.id + FROM VcsAccessLog vcsAccessLog + WHERE vcsAccessLog.timestamp < :date + """) + List findAllIdsBeforeDate(@Param("date") ZonedDateTime date); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java index 1c235dcce3b7..f6143a43561c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java @@ -24,10 +24,14 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.service.ProfileService; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; +import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; +import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; import de.tum.cit.aet.artemis.programming.repository.BuildLogEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingSubmissionRepository; import de.tum.cit.aet.artemis.programming.service.ci.ContinuousIntegrationService; @@ -43,16 +47,23 @@ public class BuildLogEntryService { private final ProfileService profileService; + private final BuildJobRepository buildJobRepository; + + private final ProgrammingExerciseRepository programmingExerciseRepository; + @Value("${artemis.continuous-integration.build-log.file-expiry-days:30}") private int expiryDays; @Value("${artemis.build-logs-path:./build-logs}") private Path buildLogsPath; - public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, ProfileService profileService) { + public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, ProfileService profileService, + BuildJobRepository buildJobRepository, ProgrammingExerciseRepository programmingExerciseRepository) { this.buildLogEntryRepository = buildLogEntryRepository; this.programmingSubmissionRepository = programmingSubmissionRepository; this.profileService = profileService; + this.buildJobRepository = buildJobRepository; + this.programmingExerciseRepository = programmingExerciseRepository; } /** @@ -282,23 +293,34 @@ public void deleteBuildLogEntriesForProgrammingSubmission(ProgrammingSubmission } /** - * Save the build logs for a given submission to a file + * Saves a list of build log entries to a file for a specific build job. + * + *

+ * The log file path is constructed based on the course's short name, the exercise's short name, + * and the build job ID. If the directory structure for the logs does not already exist, it is created. + * Each log entry is written to the log file in the format of "time\tlog message". * - * @param buildLogEntries the build logs to save - * @param buildJobId the id of the build job for which to save the build logs + * @param buildLogEntries A list of {@link BuildLogEntry} objects containing the build log information to be saved. + * @param buildJobId The unique identifier of the build job whose logs are being saved. + * @param programmingExercise The programming exercise associated with the build job, used to + * retrieve the course and exercise short names. + * @throws IllegalStateException If the directory for storing the logs could not be created. + * @throws RuntimeException If an I/O error occurs while writing the log file. */ - public void saveBuildLogsToFile(List buildLogEntries, String buildJobId) { - - if (!Files.exists(buildLogsPath)) { + public void saveBuildLogsToFile(List buildLogEntries, String buildJobId, ProgrammingExercise programmingExercise) { + String courseShortName = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName(); + String exerciseShortName = programmingExercise.getShortName(); + Path exerciseLogsPath = buildLogsPath.resolve(courseShortName).resolve(exerciseShortName); + if (!Files.exists(exerciseLogsPath)) { try { - Files.createDirectories(buildLogsPath); + Files.createDirectories(exerciseLogsPath); } catch (Exception e) { throw new IllegalStateException("Could not create directory for build logs", e); } } - Path logPath = buildLogsPath.resolve(buildJobId + ".log"); + Path logPath = exerciseLogsPath.resolve(buildJobId + ".log"); StringBuilder logsStringBuilder = new StringBuilder(); for (BuildLogEntry buildLogEntry : buildLogEntries) { @@ -315,23 +337,49 @@ public void saveBuildLogsToFile(List buildLogEntries, String buil } /** - * Retrieves the build logs for a given submission from a file. + * Retrieves the build logs for a specific build job from the file system as a {@link FileSystemResource}. + * + *

+ * The method first attempts to locate the log file in the directory corresponding to the course + * and exercise short names. If the file is not found, it will attempt to retrieve the log from a + * parent directory for backward compatibility. * - * @param buildJobId the id of the build job for which to retrieve the build logs - * @return the build logs as a string or null if the file could not be found (e.g. if the build logs have been deleted) + * @param buildJobId The unique identifier of the build job whose logs are being retrieved. + * @return A {@link FileSystemResource} representing the log file if it exists, or {@code null} if the log file cannot be found. */ public FileSystemResource retrieveBuildLogsFromFileForBuildJob(String buildJobId) { - Path logPath = buildLogsPath.resolve(buildJobId + ".log"); + if (buildJobId.contains("/") || buildJobId.contains("\\") || buildJobId.contains("..")) { + log.warn("Invalid build job ID: {}", buildJobId); + throw new IllegalArgumentException("Invalid build job ID"); + } + + ProgrammingExercise programmingExercise = retrieveProgrammingExerciseByBuildJobId(buildJobId); + String courseShortName = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName(); + String exerciseShortName = programmingExercise.getShortName(); + Path logPath = buildLogsPath.resolve(courseShortName).resolve(exerciseShortName).resolve(buildJobId + ".log"); FileSystemResource fileSystemResource = new FileSystemResource(logPath); if (fileSystemResource.exists()) { log.debug("Retrieved build logs for build job {} from file {}", buildJobId, logPath); return fileSystemResource; } - else { - log.warn("Could not find build logs for build job {} in file {}", buildJobId, logPath); - return null; + + // If the file is not found in the exercise directory, try to find it in the parent directory (for backwards compatibility) + log.warn("Build log file for build job {} not found at path {}. Searching in Parent directory...", buildJobId, logPath); + logPath = buildLogsPath.resolve(buildJobId + ".log"); + fileSystemResource = new FileSystemResource(logPath); + if (fileSystemResource.exists()) { + log.debug("Retrieved build logs for build job {} from file {}", buildJobId, logPath); + return fileSystemResource; } + + log.warn("Could not find build logs for build job {} in file {}", buildJobId, logPath); + return null; + } + + private ProgrammingExercise retrieveProgrammingExerciseByBuildJobId(String buildJobId) { + BuildJob buildJob = buildJobRepository.findByBuildJobIdElseThrow(buildJobId); + return programmingExerciseRepository.findByIdElseThrow(buildJob.getExerciseId()); } /** @@ -359,25 +407,86 @@ public void deleteOldBuildLogsFiles() { if (!profileService.isSchedulingActive()) { return; } + log.info("Deleting old build log files"); - ZonedDateTime now = ZonedDateTime.now(); - try (DirectoryStream stream = Files.newDirectoryStream(buildLogsPath)) { - for (Path file : stream) { - ZonedDateTime lastModified = ZonedDateTime.ofInstant(Files.getLastModifiedTime(file).toInstant(), now.getZone()); - if (lastModified.isBefore(now.minusDays(expiryDays))) { - Files.deleteIfExists(file); - log.info("Deleted old build log file {}", file); - } - } + try { + deleteExpiredBuildLogFilesRecursively(buildLogsPath); } catch (IOException e) { log.error("Error occurred while trying to delete old build log files", e); } } - public boolean buildJobHasLogFile(String buildJobId) { - Path logPath = buildLogsPath.resolve(buildJobId + ".log"); + private void deleteExpiredBuildLogFilesRecursively(Path path) throws IOException { + if (!Files.isDirectory(path)) { + deleteFileIfExpired(path); + return; + } + + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + for (Path subPath : stream) { + deleteExpiredBuildLogFilesRecursively(subPath); + } + } + catch (IOException e) { + log.error("Error occurred while processing directory: {}", path, e); + } + + if (!path.equals(buildLogsPath)) { + deleteDirectoryIfEmpty(path); + } + } + + private void deleteFileIfExpired(Path file) throws IOException { + ZonedDateTime now = ZonedDateTime.now(); + + ZonedDateTime lastModified = ZonedDateTime.ofInstant(Files.getLastModifiedTime(file).toInstant(), now.getZone()); + if (Files.isRegularFile(file) && lastModified.isBefore(now.minusDays(expiryDays))) { + Files.deleteIfExists(file); + log.info("Deleted old build log file {}", file); + } + + } + + private void deleteDirectoryIfEmpty(Path directory) { + if (Files.isDirectory(directory)) { + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + if (!stream.iterator().hasNext()) { + Files.deleteIfExists(directory); + log.info("Deleted empty directory {}", directory); + } + } + catch (IOException e) { + log.error("Error occurred while trying to delete empty directory {}", directory, e); + } + } + } + + /** + * Checks if the log file for a specific build job exists in the file system. + * + *

+ * The log file path is constructed based on the course's short name, the exercise's short name, + * and the build job ID. The file is expected to be located at: + * {@code buildLogsPath///.log}. + * + * @param buildJobId The unique identifier of the build job whose log file is being checked. + * @param programmingExercise The programming exercise associated with the build job, used to + * retrieve the course and exercise short names. + * @return {@code true} if the log file exists, otherwise {@code false}. + */ + public boolean buildJobHasLogFile(String buildJobId, ProgrammingExercise programmingExercise) { + String courseShortName = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName(); + String exerciseShortName = programmingExercise.getShortName(); + Path logPath = buildLogsPath.resolve(courseShortName).resolve(exerciseShortName).resolve(buildJobId + ".log"); + boolean existsInExerciseFolder = Files.exists(logPath); + if (existsInExerciseFolder) { + return true; + } + + // Check parent folder for backwards compatibility + logPath = buildLogsPath.resolve(buildJobId + ".log"); return Files.exists(logPath); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java index c181815e2143..ed5dd6cbae45 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java @@ -47,6 +47,7 @@ import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.dto.FileMove; +import de.tum.cit.aet.artemis.programming.service.localvc.VcsAccessLogService; /** * Service that provides utilities for managing files in a git repository. @@ -59,11 +60,14 @@ public class RepositoryService { private final ProfileService profileService; + private final Optional vcsAccessLogService; + private static final Logger log = LoggerFactory.getLogger(RepositoryService.class); - public RepositoryService(GitService gitService, ProfileService profileService) { + public RepositoryService(GitService gitService, ProfileService profileService, Optional vcsAccessLogService) { this.gitService = gitService; this.profileService = profileService; + this.vcsAccessLogService = vcsAccessLogService; } /** @@ -468,11 +472,15 @@ public void pullChanges(Repository repository) { * * @param repository for which to execute the commit. * @param user the user who has committed the changes in the online editor + * @param domainId the id of the domain Object (participation) owning the repository * @throws GitAPIException if the staging/committing process fails. */ - public void commitChanges(Repository repository, User user) throws GitAPIException { + public void commitChanges(Repository repository, User user, Long domainId) throws GitAPIException { gitService.stageAllChanges(repository); gitService.commitAndPush(repository, "Changes by Online Editor", true, user); + if (vcsAccessLogService.isPresent()) { + vcsAccessLogService.get().storeCodeEditorAccessLog(repository, user, domainId); + } } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index 5ddd9dac7e8d..16285e6a0695 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,8 +32,8 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST -> defaultRepositoryUpgradeService; - case JAVASCRIPT, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT -> defaultRepositoryUpgradeService; + case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index f22c65b8b6d6..b4f67794c073 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,8 +219,8 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST -> "assignment"; - case JAVASCRIPT, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT -> "assignment"; + case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } @@ -230,9 +230,9 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST -> ""; + case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT -> ""; case C, VHDL, ASSEMBLER, OCAML -> "tests"; - case JAVASCRIPT, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java index c898b33edb0d..7cd996e35cc4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.MAVEN_MAVEN; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_MAVEN; @@ -25,5 +26,6 @@ public GitLabCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, false, true, false, List.of(PLAIN_MAVEN, MAVEN_MAVEN), false, false)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index 82c0a9ebf238..94e6bf8d27fc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -4,6 +4,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; @@ -41,5 +42,6 @@ public JenkinsProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, false)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index 571be75f7624..6e904910ca57 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -184,8 +184,8 @@ private JenkinsXmlConfigBuilder builderFor(ProgrammingLanguage programmingLangua throw new UnsupportedOperationException("Xcode templates are not available for Jenkins."); } return switch (programmingLanguage) { - case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, JAVASCRIPT, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException(programmingLanguage + " templates are not available for Jenkins."); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index 77ebdcabcff9..967e05604f30 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -3,8 +3,10 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALCI; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.ASSEMBLER; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.OCAML; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; @@ -36,7 +38,7 @@ public class LocalCIProgrammingLanguageFeatureService extends ProgrammingLanguag public LocalCIProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added - // TODO LOCALVC_CI: Local CI is not supporting EMPTY at the moment. + programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, true)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), false, true)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true)); @@ -48,5 +50,6 @@ public LocalCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, true)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java index 5c033aab1aaa..39bfac28ca0b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java @@ -159,6 +159,9 @@ public void processResult() { if (participationOptional.isPresent()) { ProgrammingExerciseParticipation participation = (ProgrammingExerciseParticipation) participationOptional.get(); + if (participation.getExercise() == null) { + participation.setExercise(programmingExerciseRepository.getProgrammingExerciseFromParticipation(participation)); + } if (result != null) { programmingMessagingService.notifyUserAboutNewResult(result, participation); @@ -168,15 +171,15 @@ public void processResult() { programmingMessagingService.notifyUserAboutSubmissionError((Participation) participation, new BuildTriggerWebsocketError("Result could not be processed", participation.getId())); } - } - } - if (!buildLogs.isEmpty()) { - if (savedBuildJob != null) { - buildLogEntryService.saveBuildLogsToFile(buildLogs, savedBuildJob.getBuildJobId()); - } - else { - log.warn("Couldn't save build logs as build job {} was not saved", buildJob.id()); + if (!buildLogs.isEmpty()) { + if (savedBuildJob != null) { + buildLogEntryService.saveBuildLogsToFile(buildLogs, savedBuildJob.getBuildJobId(), participation.getProgrammingExercise()); + } + else { + log.warn("Couldn't save build logs as build job {} was not saved", buildJob.id()); + } + } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/AutomaticVcsAccessLogCleanupService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/AutomaticVcsAccessLogCleanupService.java new file mode 100644 index 000000000000..f438a093edf6 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/AutomaticVcsAccessLogCleanupService.java @@ -0,0 +1,38 @@ +package de.tum.cit.aet.artemis.programming.service.localvc; + +import java.time.ZonedDateTime; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; + +@Service +@Profile("scheduling & localvc") +public class AutomaticVcsAccessLogCleanupService { + + private static final Logger log = LoggerFactory.getLogger(AutomaticVcsAccessLogCleanupService.class); + + private final VcsAccessLogRepository vcsAccessLogRepository; + + @Value("${artemis.audit-events.retention-period:120}") + private int vcsAccessLogRetentionPeriod; + + public AutomaticVcsAccessLogCleanupService(VcsAccessLogRepository vcsAccessLogRepository) { + this.vcsAccessLogRepository = vcsAccessLogRepository; + } + + /** + * Deletes all vcs access log entries from the database which have a timestamp older than vcsAccessLogRetentionPeriod days (120 by default) + */ + @Scheduled(cron = "0 30 2 * * *") // execute this every night at 2:30:00 am + public void cleanup() { + var outDatedAccessLogs = vcsAccessLogRepository.findAllIdsBeforeDate(ZonedDateTime.now().minusDays(vcsAccessLogRetentionPeriod)); + log.info("Scheduled deletion of expired access log entries: deleting {} vcs access log entries", outDatedAccessLogs.size()); + vcsAccessLogRepository.deleteAllById(outDatedAccessLogs); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java index f331ead9ad44..90186a8c2c31 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java @@ -49,6 +49,7 @@ import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.util.TimeLogUtil; +import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism; import de.tum.cit.aet.artemis.programming.domain.Commit; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; @@ -100,6 +101,9 @@ public class LocalVCServletService { private final ProgrammingTriggerService programmingTriggerService; + // TODO As soon as only LocalVC is supported, this Optional can be removed + private final Optional vcsAccessLogService; + private static URL localVCBaseUrl; private final ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository; @@ -132,7 +136,7 @@ public LocalVCServletService(AuthenticationManager authenticationManager, UserRe ProgrammingExerciseParticipationService programmingExerciseParticipationService, AuxiliaryRepositoryService auxiliaryRepositoryService, ContinuousIntegrationTriggerService ciTriggerService, ProgrammingSubmissionService programmingSubmissionService, ProgrammingMessagingService programmingMessagingService, ProgrammingTriggerService programmingTriggerService, - ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository) { + ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository, Optional vcsAccessLogService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.programmingExerciseRepository = programmingExerciseRepository; @@ -145,6 +149,7 @@ public LocalVCServletService(AuthenticationManager authenticationManager, UserRe this.programmingMessagingService = programmingMessagingService; this.programmingTriggerService = programmingTriggerService; this.participationVCSAccessTokenRepository = participationVCSAccessTokenRepository; + this.vcsAccessLogService = vcsAccessLogService; } /** @@ -238,13 +243,36 @@ public void authenticateAndAuthorizeGitRequest(HttpServletRequest request, Repos throw new LocalVCForbiddenException(); } - authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, localVCRepositoryUri.isPracticeRepository()); + var authenticationMechanism = resolveAuthenticationMechanism(authorizationHeader, user); + var ipAddress = request.getRemoteAddr(); + authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, authenticationMechanism, ipAddress, localVCRepositoryUri); request.setAttribute("user", user); log.debug("Authorizing user {} for repository {} took {}", user.getLogin(), localVCRepositoryUri, TimeLogUtil.formatDurationFrom(timeNanoStart)); } + /** + * Resolves the user's authentication mechanism for the repository + * + * @param authorizationHeader the request's authorizationHeader, containing the token or password + * @param user the user + * @return the authentication type + * @throws LocalVCAuthException if extracting the token or password from the authorizationHeader fails + */ + private AuthenticationMechanism resolveAuthenticationMechanism(String authorizationHeader, User user) throws LocalVCAuthException { + UsernameAndPassword usernameAndPassword = extractUsernameAndPassword(authorizationHeader); + + String password = usernameAndPassword.password(); + if (!password.startsWith(TOKEN_PREFIX)) { + return AuthenticationMechanism.PASSWORD; + } + if (password.equals(user.getVcsAccessToken())) { + return AuthenticationMechanism.USER_VCS_ACCESS_TOKEN; + } + return AuthenticationMechanism.PARTICIPATION_VCS_ACCESS_TOKEN; + } + private User authenticateUser(String authorizationHeader, ProgrammingExercise exercise, LocalVCRepositoryUri localVCRepositoryUri) throws LocalVCAuthException { UsernameAndPassword usernameAndPassword = extractUsernameAndPassword(authorizationHeader); @@ -377,11 +405,14 @@ private UsernameAndPassword extractUsernameAndPassword(String authorizationHeade * @param user The user that wants to access the repository. * @param exercise The exercise the repository belongs to. * @param repositoryActionType The type of the action the user wants to perform. - * @param isPracticeRepository Whether the repository is a practice repository. + * @param authenticationMechanism The authentication mechanism used by the user to authenticate to the repository + * @param ipAddress The ip address of the user + * @param localVCRepositoryUri The URI of the local repository. + * * @throws LocalVCForbiddenException If the user is not allowed to access the repository. */ - public void authorizeUser(String repositoryTypeOrUserName, User user, ProgrammingExercise exercise, RepositoryActionType repositoryActionType, boolean isPracticeRepository) - throws LocalVCForbiddenException { + public void authorizeUser(String repositoryTypeOrUserName, User user, ProgrammingExercise exercise, RepositoryActionType repositoryActionType, + AuthenticationMechanism authenticationMechanism, String ipAddress, LocalVCRepositoryUri localVCRepositoryUri) throws LocalVCForbiddenException { if (repositoryTypeOrUserName.equals(RepositoryType.TESTS.toString()) || auxiliaryRepositoryService.isAuxiliaryRepositoryOfExercise(repositoryTypeOrUserName, exercise)) { // Test and auxiliary repositories are only accessible by instructors and higher. @@ -396,7 +427,8 @@ public void authorizeUser(String repositoryTypeOrUserName, User user, Programmin ProgrammingExerciseParticipation participation; try { - participation = programmingExerciseParticipationService.getParticipationForRepository(exercise, repositoryTypeOrUserName, isPracticeRepository, false); + participation = programmingExerciseParticipationService.getParticipationForRepository(exercise, repositoryTypeOrUserName, localVCRepositoryUri.isPracticeRepository(), + false); } catch (EntityNotFoundException e) { throw new LocalVCInternalException( @@ -409,6 +441,18 @@ public void authorizeUser(String repositoryTypeOrUserName, User user, Programmin catch (AccessForbiddenException e) { throw new LocalVCForbiddenException(e); } + String commitHash = null; + try { + if (repositoryActionType == RepositoryActionType.READ) { + commitHash = getLatestCommitHash(repositories.get(localVCRepositoryUri.getRelativeRepositoryPath().toString())); + } + } + catch (GitAPIException e) { + log.warn("Failed to obtain commit hash for repository {}. Error: {}", localVCRepositoryUri.getRelativeRepositoryPath().toString(), e.getMessage()); + } + // Write a access log entry to the database + String finalCommitHash = commitHash; + vcsAccessLogService.ifPresent(service -> service.storeAccessLog(user, participation, repositoryActionType, authenticationMechanism, finalCommitHash, ipAddress)); } /** @@ -475,6 +519,10 @@ public void processNewPush(String commitHash, Repository repository) { // Process push to any repository other than the test repository. processNewPushToRepository(participation, commit); + + // For push the correct commitHash is only available here, therefore the preliminary null value is overwritten + String finalCommitHash = commitHash; + vcsAccessLogService.ifPresent(service -> service.updateCommitHash(participation, finalCommitHash)); } catch (GitAPIException | IOException e) { // This catch clause does not catch exceptions that happen during runBuildJob() as that method is called asynchronously. diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/SshGitLocationResolverService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/SshGitLocationResolverService.java index 99b90e06425d..a61712685ef7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/SshGitLocationResolverService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/SshGitLocationResolverService.java @@ -21,6 +21,7 @@ import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.exception.localvc.LocalVCForbiddenException; import de.tum.cit.aet.artemis.core.exception.localvc.LocalVCInternalException; +import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshConstants; @@ -76,7 +77,8 @@ public Path resolveRootDirectory(String command, String[] args, ServerSession se else { final var user = session.getAttribute(SshConstants.USER_KEY); try { - localVCServletService.authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, localVCRepositoryUri.isPracticeRepository()); + localVCServletService.authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, AuthenticationMechanism.SSH, session.getClientAddress().toString(), + localVCRepositoryUri); } catch (LocalVCForbiddenException e) { log.error("User {} does not have access to the repository {}", user.getLogin(), repositoryPath); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/VcsAccessLogService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/VcsAccessLogService.java new file mode 100644 index 000000000000..d77b37c02a7f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/VcsAccessLogService.java @@ -0,0 +1,86 @@ +package de.tum.cit.aet.artemis.programming.service.localvc; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; +import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository; +import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; +import de.tum.cit.aet.artemis.programming.domain.Repository; +import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; +import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; +import de.tum.cit.aet.artemis.programming.web.repository.RepositoryActionType; + +@Profile(PROFILE_LOCALVC) +@Service +public class VcsAccessLogService { + + private static final Logger log = LoggerFactory.getLogger(VcsAccessLogService.class); + + private final VcsAccessLogRepository vcsAccessLogRepository; + + private final ParticipationRepository participationRepository; + + VcsAccessLogService(VcsAccessLogRepository vcsAccessLogRepository, ParticipationRepository participationRepository) { + this.vcsAccessLogRepository = vcsAccessLogRepository; + this.participationRepository = participationRepository; + } + + /** + * Creates a vcs access log entry and stores it to the database + * + * @param user The user accessing the repository + * @param participation The participation which owns the repository + * @param actionType The action type: READ or WRITE + * @param authenticationMechanism The used authentication mechanism: password, vcs token (user/participation), SSH or code editor + * @param commitHash The latest commit hash + * @param ipAddress The ip address of the user accessing the repository + */ + public void storeAccessLog(User user, ProgrammingExerciseParticipation participation, RepositoryActionType actionType, AuthenticationMechanism authenticationMechanism, + String commitHash, String ipAddress) { + log.debug("Storing access operation for user {}", user); + + VcsAccessLog accessLogEntry = new VcsAccessLog(user, (Participation) participation, user.getName(), user.getEmail(), actionType, authenticationMechanism, commitHash, + ipAddress); + vcsAccessLogRepository.save(accessLogEntry); + } + + /** + * Updates the commit hash after a successful push + * + * @param participation The participation to which the repository belongs to + * @param commitHash The newest commit hash which should get set for the access log entry + */ + public void updateCommitHash(ProgrammingExerciseParticipation participation, String commitHash) { + vcsAccessLogRepository.findNewestByParticipationIdWhereCommitHashIsNull(participation.getId()).ifPresent(entry -> { + entry.setCommitHash(commitHash); + vcsAccessLogRepository.save(entry); + }); + } + + /** + * Stores the log for a push from the code editor. + * + * @param repo The repository to which the push is executed + * @param user The user submitting the change + * @param participationId The id of the participation belonging to the repository + * @throws GitAPIException if an error occurs while retrieving the git log + */ + public void storeCodeEditorAccessLog(Repository repo, User user, Long participationId) throws GitAPIException { + try (Git git = new Git(repo)) { + String lastCommitHash = git.log().setMaxCount(1).call().iterator().next().getName(); + var participation = participationRepository.findById(participationId); + if (participation.isPresent() && participation.get() instanceof ProgrammingExerciseParticipation programmingParticipation) { + storeAccessLog(user, programmingParticipation, RepositoryActionType.WRITE, AuthenticationMechanism.CODE_EDITOR, lastCommitHash, null); + } + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index a1e104e6fde4..d06fdf5fb975 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -11,6 +11,8 @@ import java.util.stream.Collectors; import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -31,6 +33,7 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastInstructorInExercise; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository; import de.tum.cit.aet.artemis.exam.service.ExamService; @@ -42,10 +45,13 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.RepositoryType; +import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.dto.CommitInfoDTO; +import de.tum.cit.aet.artemis.programming.dto.VcsAccessLogDTO; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; +import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseParticipationService; import de.tum.cit.aet.artemis.programming.service.ProgrammingSubmissionService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; @@ -55,6 +61,8 @@ @RequestMapping("api/") public class ProgrammingExerciseParticipationResource { + private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseParticipationResource.class); + private static final String ENTITY_NAME = "programmingExerciseParticipation"; private final ParticipationRepository participationRepository; @@ -79,11 +87,13 @@ public class ProgrammingExerciseParticipationResource { private final StudentExamRepository studentExamRepository; + private final Optional vcsAccessLogRepository; + public ProgrammingExerciseParticipationResource(ProgrammingExerciseParticipationService programmingExerciseParticipationService, ResultRepository resultRepository, ParticipationRepository participationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingSubmissionService submissionService, ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, ResultService resultService, ParticipationAuthorizationCheckService participationAuthCheckService, RepositoryService repositoryService, - StudentExamRepository studentExamRepository) { + StudentExamRepository studentExamRepository, Optional vcsAccessLogRepository) { this.programmingExerciseParticipationService = programmingExerciseParticipationService; this.participationRepository = participationRepository; this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; @@ -95,6 +105,7 @@ public ProgrammingExerciseParticipationResource(ProgrammingExerciseParticipation this.participationAuthCheckService = participationAuthCheckService; this.repositoryService = repositoryService; this.studentExamRepository = studentExamRepository; + this.vcsAccessLogRepository = vcsAccessLogRepository; } /** @@ -305,6 +316,27 @@ public ResponseEntity> getCommitHistoryForParticipationRepo( return ResponseEntity.ok(commitInfo); } + /** + * GET /programming-exercise-participations/{participationId}/vcs-access-log : + * Here we check if the user is least an instructor for the exercise. If true the user can have access to the vcs access log of any participation of the exercise. + * + * @param participationId the id of the participation for which to retrieve the vcs access log + * @return the ResponseEntity with status 200 (OK) and with body containing a list of vcsAccessLogDTOs of the participation, or 400 (Bad request) if localVC is not enabled. + */ + @GetMapping("programming-exercise-participations/{participationId}/vcs-access-log") + @EnforceAtLeastInstructor + public ResponseEntity> getVcsAccessLogForParticipationRepo(@PathVariable long participationId) { + if (vcsAccessLogRepository.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + ProgrammingExerciseStudentParticipation participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + participationAuthCheckService.checkCanAccessParticipationElseThrow(participation); + log.info("Fetching VCS access logs for participation ID: {}", participationId); + List vcsAccessLogs = vcsAccessLogRepository.get().findAllByParticipationId(participationId); + var vcsAccessLogDTOs = vcsAccessLogs.stream().map(VcsAccessLogDTO::of).toList(); + return ResponseEntity.ok(vcsAccessLogDTOs); + } + /** * GET /programming-exercise/{exerciseID}/commit-history/{repositoryType} : Get the commit history of a programming exercise repository. The repository type can be TEMPLATE or * SOLUTION or TESTS. @@ -392,6 +424,34 @@ else if (repositoryType != null) { } } + /** + * Retrieves the VCS access logs for the specified programming exercise's template or solution participation + * + * @param exerciseId the ID of the programming exercise + * @param repositoryType the type of repository (either TEMPLATE or SOLUTION) for which to retrieve the logs. + * @return the ResponseEntity with status 200 (OK) and with body containing a list of vcsAccessLogDTOs of the participation, or 400 (Bad request) if localVC is not enabled. + * @throws BadRequestAlertException if the repository type is invalid + */ + @GetMapping("programming-exercise/{exerciseId}/vcs-access-log/{repositoryType}") + @EnforceAtLeastInstructorInExercise + public ResponseEntity> getVcsAccessLogForExerciseRepository(@PathVariable long exerciseId, @PathVariable RepositoryType repositoryType) { + if (vcsAccessLogRepository.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + if (repositoryType != RepositoryType.TEMPLATE && repositoryType != RepositoryType.SOLUTION) { + throw new BadRequestAlertException("Can only get vcs access log from template and assignment repositories", ENTITY_NAME, "incorrect repositoryType"); + } + ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesElseThrow(exerciseId); + authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, programmingExercise, null); + log.info("Fetching VCS access logs for exercise ID: {} and repository type: {}", exerciseId, repositoryType); + + var participation = repositoryType == RepositoryType.TEMPLATE ? programmingExercise.getTemplateParticipation() : programmingExercise.getSolutionParticipation(); + + List vcsAccessLogs = vcsAccessLogRepository.get().findAllByParticipationId(participation.getId()); + var vcsAccessLogDTOs = vcsAccessLogs.stream().map(VcsAccessLogDTO::of).toList(); + return ResponseEntity.ok(vcsAccessLogDTOs); + } + /** * Checks if the user has access to the participation. * If the exercise has not started yet and the user is a student, access is denied. @@ -426,5 +486,4 @@ private boolean shouldHideExamExerciseResults(ProgrammingExerciseStudentParticip } return false; } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/BuildLogResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/BuildLogResource.java index 9ab8cfba30db..c8fd18e69fca 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/BuildLogResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/BuildLogResource.java @@ -43,6 +43,7 @@ public BuildLogResource(BuildLogEntryService buildLogEntryService) { public ResponseEntity getBuildLogForBuildJob(@PathVariable String buildJobId) { log.debug("REST request to get the build log for build job {}", buildJobId); HttpHeaders responseHeaders = new HttpHeaders(); + FileSystemResource buildLog = buildLogEntryService.retrieveBuildLogsFromFileForBuildJob(buildJobId); if (buildLog == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java index bd50617ee04d..f9ca7350afd1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java @@ -281,7 +281,7 @@ public ResponseEntity commitChanges(Long domainId) { return executeAndCheckForExceptions(() -> { Repository repository = getRepository(domainId, RepositoryActionType.WRITE, true); - repositoryService.commitChanges(repository, user); + repositoryService.commitChanges(repository, user, domainId); // Trigger a build, and process the result. Only implemented for local CI. // For GitLab + Jenkins, webhooks were added when creating the repository, // that notify the CI system when the commit happens and thus trigger the build. diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/TextAssessmentResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/TextAssessmentResource.java index 8ec980049277..900347ecc54e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/TextAssessmentResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/TextAssessmentResource.java @@ -43,7 +43,6 @@ import de.tum.cit.aet.artemis.athena.service.AthenaFeedbackSendingService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.exception.ErrorConstants; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; @@ -66,8 +65,6 @@ import de.tum.cit.aet.artemis.text.service.TextAssessmentService; import de.tum.cit.aet.artemis.text.service.TextBlockService; import de.tum.cit.aet.artemis.text.service.TextSubmissionService; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; /** * REST controller for managing TextAssessment. @@ -166,7 +163,6 @@ public ResponseEntity saveTextAssessment(@PathVariable Long participatio * @return result after saving example text assessment */ @ResponseStatus(HttpStatus.OK) - @ApiResponses({ @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON) }) @PutMapping("exercises/{exerciseId}/example-submissions/{exampleSubmissionId}/example-text-assessment") @EnforceAtLeastTutor public ResponseEntity saveTextExampleAssessment(@PathVariable long exerciseId, @PathVariable long exampleSubmissionId, @RequestBody TextAssessmentDTO textAssessment) { diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 7d86700066e4..576e5ca01cde 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -109,8 +109,7 @@ jhipster: port: 5000 ring-buffer-size: 512 audit-events: - retention-period: 120 # Number of days before audit events are deleted. - + retention-period: 120 # Number of days before audit events and VCS access logs are deleted. # Properties to be exposed on the /info management endpoint info: guided-tour: diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 6b3f18dfaeeb..924d087ec8f2 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -69,7 +69,7 @@ artemis: # possible overrides: maven, gradle default: "ls1tum/artemis-maven-template:java17-21" empty: - default: "ls1tum/artemis-maven-template:java17-21" + default: "ubuntu:24.04" python: default: "ls1tum/artemis-python-docker:latest" c: @@ -89,6 +89,8 @@ artemis: default: "ls1tum/artemis-ocaml-docker:v1" rust: default: "ghcr.io/ls1intum/artemis-rust-docker:v0.9.70" + javascript: + default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" management: endpoints: diff --git a/src/main/resources/config/liquibase/changelog/20240804144500_changelog.xml b/src/main/resources/config/liquibase/changelog/20240804144500_changelog.xml new file mode 100644 index 000000000000..72b4d0498221 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240804144500_changelog.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index f8be6b6255a0..2c204094c0ff 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,6 +22,8 @@ + + diff --git a/src/main/resources/templates/aeolus/empty/default.sh b/src/main/resources/templates/aeolus/empty/default.sh new file mode 100644 index 000000000000..578e9bc900c5 --- /dev/null +++ b/src/main/resources/templates/aeolus/empty/default.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +install_dependencies () { + echo '⚙️ executing install_dependencies' + # TODO: Install dependencies not provided by the Docker image + echo 'Install dependencies' +} + +run_tests () { + echo '⚙️ executing run_tests' + # TODO: Run the tests and generate JUnit XMLs + echo 'Hello World' +} + +process_results () { + echo '⚙️ executing process_results' + rm -rf results + mkdir results + # TODO: Move JUnit XMLs into the results directory +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; install_dependencies" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; run_tests" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; process_results" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/empty/default.yaml b/src/main/resources/templates/aeolus/empty/default.yaml new file mode 100644 index 000000000000..3b1b9deaedd2 --- /dev/null +++ b/src/main/resources/templates/aeolus/empty/default.yaml @@ -0,0 +1,23 @@ +api: v0.0.1 +metadata: + name: Empty + id: empty + description: Provides a starting point +actions: + - name: install_dependencies + script: |- + # TODO: Install dependencies not provided by the Docker image + echo 'Install dependencies' + - name: run_tests + script: |- + # TODO: Run the tests and generate JUnit XMLs + echo 'Hello World' + - name: process_results + script: |- + rm -rf results + mkdir results + # TODO: Move JUnit XMLs into the results directory + results: + - name: junit + path: results/*.xml + type: junit diff --git a/src/main/resources/templates/aeolus/javascript/default.sh b/src/main/resources/templates/aeolus/javascript/default.sh new file mode 100644 index 000000000000..bc63c59f3365 --- /dev/null +++ b/src/main/resources/templates/aeolus/javascript/default.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +install_dependencies () { + echo '⚙️ executing install_dependencies' + npm ci --prefer-offline --no-audit +} + +test () { + echo '⚙️ executing test' + npm run test:ci +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; install_dependencies" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; test" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/javascript/default.yaml b/src/main/resources/templates/aeolus/javascript/default.yaml new file mode 100644 index 000000000000..9786187ebe2f --- /dev/null +++ b/src/main/resources/templates/aeolus/javascript/default.yaml @@ -0,0 +1,14 @@ +api: v0.0.1 +metadata: + name: JavaScript + description: Run tests using Jest +actions: + - name: install_dependencies + script: 'npm ci --prefer-offline --no-audit' + - name: test + script: 'npm run test:ci' + runAlways: false + results: + - name: junit + path: 'junit.xml' + type: junit diff --git a/src/main/resources/templates/gitlabci/empty/regularRuns/.gitlab-ci.yml b/src/main/resources/templates/gitlabci/empty/regularRuns/.gitlab-ci.yml index 3fcb20956dc8..3a16b66b7143 100644 --- a/src/main/resources/templates/gitlabci/empty/regularRuns/.gitlab-ci.yml +++ b/src/main/resources/templates/gitlabci/empty/regularRuns/.gitlab-ci.yml @@ -17,7 +17,10 @@ test-job: - git clone --branch ${ARTEMIS_SUBMISSION_GIT_BRANCH} ${CI_SERVER_PROTOCOL}://${ARTEMIS_TEST_GIT_USER}:${ARTEMIS_TEST_GIT_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME} assignment - export ARTEMIS_NOTIFICATION_SECRET=[hidden] # Workaround for overwriting the secret - export ARTEMIS_TEST_GIT_TOKEN=[hidden] - - mvn --version && echo "ARTEMIS_BUILD_STATUS=success" > .env || echo "ARTEMIS_BUILD_STATUS=failed" > .env + # TODO: Install dependencies not provided by the Docker image + # TODO: Run the tests and generate JUnit XMLs + - echo "Hello World" && echo "ARTEMIS_BUILD_STATUS=success" > .env || echo "ARTEMIS_BUILD_STATUS=failed" > .env + # TODO: Move JUnit XMLs into the ${ARTEMIS_TEST_RESULTS_DIR} directory after_script: - echo "ARTEMIS_TEST_GIT_HASH=$(git rev-parse HEAD)" >> .env - echo "ARTEMIS_SUBMISSION_GIT_HASH=${CI_COMMIT_SHA}" >> .env diff --git a/src/main/resources/templates/gitlabci/javascript/regularRuns/.gitlab-ci.yml b/src/main/resources/templates/gitlabci/javascript/regularRuns/.gitlab-ci.yml new file mode 100644 index 000000000000..b2b5dcdaa592 --- /dev/null +++ b/src/main/resources/templates/gitlabci/javascript/regularRuns/.gitlab-ci.yml @@ -0,0 +1,49 @@ +stages: + - test + - upload + + +test-job: + image: ${ARTEMIS_BUILD_DOCKER_IMAGE} + stage: test + only: + variables: + - $CI_COMMIT_BRANCH == $ARTEMIS_SUBMISSION_GIT_BRANCH + allow_failure: true + variables: + GIT_STRATEGY: none + script: + - git clone --branch ${ARTEMIS_TEST_GIT_BRANCH} ${CI_SERVER_PROTOCOL}://${ARTEMIS_TEST_GIT_USER}:${ARTEMIS_TEST_GIT_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_NAMESPACE}/${ARTEMIS_TEST_GIT_REPOSITORY_SLUG} . + - git clone --branch ${ARTEMIS_SUBMISSION_GIT_BRANCH} ${CI_SERVER_PROTOCOL}://${ARTEMIS_TEST_GIT_USER}:${ARTEMIS_TEST_GIT_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME} assignment + - export ARTEMIS_NOTIFICATION_SECRET=[hidden] # Workaround for overwriting the secret + - export ARTEMIS_TEST_GIT_TOKEN=[hidden] + - npm ci --prefer-offline --no-audit | tee -a "${ARTEMIS_BUILD_LOGS_FILE}" + - npm run test:ci | tee -a "${ARTEMIS_BUILD_LOGS_FILE}" && echo "ARTEMIS_BUILD_STATUS=success" > .env || echo "ARTEMIS_BUILD_STATUS=failed" > .env + - test -e junit.xml && sed -i 's/]*>//g ; s/<\/testsuites>/<\/testsuite>/g' junit.xml # not supported by notification plugin + - test -e junit.xml && mkdir test-reports && cp junit.xml test-reports/junit.xml + after_script: + - echo "ARTEMIS_TEST_GIT_HASH=$(git rev-parse HEAD)" >> .env + - echo "ARTEMIS_SUBMISSION_GIT_HASH=${CI_COMMIT_SHA}" >> .env + - echo "ARTEMIS_SUBMISSION_GIT_REPOSITORY_SLUG=${CI_PROJECT_NAME}" >> .env + artifacts: + paths: + - ${ARTEMIS_BUILD_LOGS_FILE} + - test-reports/junit.xml + reports: + dotenv: .env + + +upload-job: + image: ${ARTEMIS_NOTIFICATION_PLUGIN_DOCKER_IMAGE} + stage: upload + dependencies: + - test-job + only: + variables: + - $CI_COMMIT_BRANCH == $ARTEMIS_SUBMISSION_GIT_BRANCH + variables: + GIT_STRATEGY: none + script: + - cp -r /notification-plugin/* . + - export ARTEMIS_TEST_RESULTS_DIR="test-reports" # override project variable + - gradle run diff --git a/src/main/resources/templates/javascript/exercise/.gitignore b/src/main/resources/templates/javascript/exercise/.gitignore new file mode 100644 index 000000000000..c2658d7d1b31 --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/src/main/resources/templates/javascript/exercise/package.json b/src/main/resources/templates/javascript/exercise/package.json new file mode 100644 index 000000000000..f5dfc56c86ef --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/package.json @@ -0,0 +1,11 @@ +{ + "name": "artemis-exercise", + "private": true, + "scripts": { + "start": "node ./src/client.js" + }, + "exports": { + "./*.js": "./src/*.js" + }, + "type": "module" +} diff --git a/src/main/resources/templates/javascript/exercise/src/bubblesort.js b/src/main/resources/templates/javascript/exercise/src/bubblesort.js new file mode 100644 index 000000000000..19ea2939c6f7 --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/src/bubblesort.js @@ -0,0 +1,3 @@ +export default class BubbleSort { + // TODO: implement in performSort(Date[]) +} diff --git a/src/main/resources/templates/javascript/exercise/src/client.js b/src/main/resources/templates/javascript/exercise/src/client.js new file mode 100644 index 000000000000..4cd9116f80da --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/src/client.js @@ -0,0 +1,66 @@ +const ITERATIONS = 10; +const DATES_LENGTH_MIN = 5; +const DATES_LENGTH_MAX = 15; + +/** + * Main function. + * Add code to demonstrate your implementation here. + */ +function main() { + // TODO: Init Context and Policy + + // Run multiple times to simulate different sorting strategies + for (let i = 0; i < ITERATIONS; i++) { + const dates = createRandomDates(); + + // TODO: Configure context + + console.log('Unsorted Array of dates:'); + console.log(dates); + + // TODO: Sort dates + + console.log('Sorted Array of dates:'); + console.log(dates); + } +} + +/** + * Generates an Array of random Date objects with random Array length between + * {@link DATES_LENGTH_MIN} and {@link DATES_LENGTH_MAX}. + * + * @return an Array of random Date objects + */ +function createRandomDates() { + const length = randomIntegerWithin(DATES_LENGTH_MIN, DATES_LENGTH_MAX); + + const lowestDate = new Date('2024-09-15'); + const highestDate = new Date('2025-01-15'); + + return Array.from(Array(length), () => randomDateWithin(lowestDate, highestDate)); +} + +/** + * Creates a random Date within the given range. + * + * @param low {Date} the lower bound + * @param high {Date} the upper bound + * @return {Date} random Date within the given range + */ +function randomDateWithin(low, high) { + const randomTimestamp = randomIntegerWithin(low.valueOf(), high.valueOf()); + return new Date(randomTimestamp); +} + +/** + * Creates a random int within the given range. + * + * @param low {number} the lower bound + * @param high {number} the upper bound + * @returns {number} random int within the given range + */ +function randomIntegerWithin(low, high) { + return Math.floor(Math.random() * (high - low + 1)) + low; +} + +main(); diff --git a/src/main/resources/templates/javascript/exercise/src/context.js b/src/main/resources/templates/javascript/exercise/src/context.js new file mode 100644 index 000000000000..a667a10bb29e --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/src/context.js @@ -0,0 +1,3 @@ +export default class Context { + // TODO: Create and implement a Context class according to the UML class diagram +} diff --git a/src/main/resources/templates/javascript/exercise/src/mergesort.js b/src/main/resources/templates/javascript/exercise/src/mergesort.js new file mode 100644 index 000000000000..43c86272c2cf --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/src/mergesort.js @@ -0,0 +1,3 @@ +export default class MergeSort { + // TODO: implement in performSort(Date[]) +} diff --git a/src/main/resources/templates/javascript/exercise/src/policy.js b/src/main/resources/templates/javascript/exercise/src/policy.js new file mode 100644 index 000000000000..7c8723feb1a9 --- /dev/null +++ b/src/main/resources/templates/javascript/exercise/src/policy.js @@ -0,0 +1,3 @@ +export default class Policy { + // TODO: Create and implement a Policy class as described in the problem statement +} diff --git a/src/main/resources/templates/javascript/readme b/src/main/resources/templates/javascript/readme new file mode 100755 index 000000000000..97cf7cb4339a --- /dev/null +++ b/src/main/resources/templates/javascript/readme @@ -0,0 +1,82 @@ +# Sorting with the Strategy Pattern + +In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables. + +### Part 1: Sorting + +First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`. + +**You have the following tasks:** + +1. [task][Implement Bubble Sort](structural_BubbleSort_has_method,behavior_BubbleSort_should_sort_correctly) +Implement the method `performSort(Date[])` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly. + +2. [task][Implement Merge Sort](structural_MergeSort_has_method,behavior_MergeSort_should_sort_correctly) +Implement the method `performSort(Date[])` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly. + +### Part 2: Strategy Pattern + +We want the application to apply different algorithms for sorting an Array of `Date` objects. +Use the strategy pattern to select the right sorting algorithm at runtime. + +**You have the following tasks:** + +1. [task][Context Class](structural_Context_has_properties,structural_Context_has_methods) +Create and implement a `Context` class following the below class diagram. +Add `get` and `set` accessors for the attribute. + +2. [task][Context Policy](structural_Policy_has_properties,structural_Policy_has_methods) +Create and implement a `Policy` class following the below class diagram. +Add `get` and `set` accessors for the attribute. +`Policy` should implement a simple configuration mechanism: + + 1. [task][Select MergeSort](behavior_Policy_uses_MergeSort_for_big_list) + Select `MergeSort` when the List has more than 10 dates. + + 2. [task][Select BubbleSort](behavior_Policy_uses_BubbleSort_for_small_list) + Select `BubbleSort` when the List has less or equal 10 dates. + +3. Complete the `main()` function which demonstrates switching between two strategies at runtime. + +@startuml + +class Policy { + +Policy(Context) <> + +configure() +} + +class Context { + -dates: Date[] + +sort() +} + +interface SortStrategy { + +performSort(Date[]) +} + +class BubbleSort { + +performSort(Date[]) +} + +class MergeSort { + +performSort(Date[]) +} + +MergeSort -up-|> SortStrategy #testsColor(structural_MergeSort_has_method) +BubbleSort -up-|> SortStrategy #testsColor(structural_BubbleSort_has_method) +Policy -right-> Context #testsColor(structural_Policy_has_properties): context +Context -right-> SortStrategy #testsColor(structural_Context_has_properties): sortAlgorithm + +hide empty fields +hide empty methods + +@enduml + + +### Part 3: Optional Challenges + +(These are not tested) + +1. Create a new class `QuickSort` that implements `SortStrategy` and implement the Quick Sort algorithm. + +2. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm. diff --git a/src/main/resources/templates/javascript/solution/.gitignore b/src/main/resources/templates/javascript/solution/.gitignore new file mode 100644 index 000000000000..c2658d7d1b31 --- /dev/null +++ b/src/main/resources/templates/javascript/solution/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/src/main/resources/templates/javascript/solution/package.json b/src/main/resources/templates/javascript/solution/package.json new file mode 100644 index 000000000000..f5dfc56c86ef --- /dev/null +++ b/src/main/resources/templates/javascript/solution/package.json @@ -0,0 +1,11 @@ +{ + "name": "artemis-exercise", + "private": true, + "scripts": { + "start": "node ./src/client.js" + }, + "exports": { + "./*.js": "./src/*.js" + }, + "type": "module" +} diff --git a/src/main/resources/templates/javascript/solution/src/bubblesort.js b/src/main/resources/templates/javascript/solution/src/bubblesort.js new file mode 100644 index 000000000000..cef35929a7f7 --- /dev/null +++ b/src/main/resources/templates/javascript/solution/src/bubblesort.js @@ -0,0 +1,18 @@ +export default class BubbleSort { + /** + * Sorts dates with BubbleSort. + * + * @param input {Date[]} the array of Dates to be sorted + */ + performSort(input) { + for (let i = input.length - 1; i >= 0; i--) { + for (let j = 0; j < i; j++) { + if (input[j].valueOf() > input[j + 1].valueOf()) { + const temp = input[j]; + input[j] = input[j + 1]; + input[j + 1] = temp; + } + } + } + } +} diff --git a/src/main/resources/templates/javascript/solution/src/client.js b/src/main/resources/templates/javascript/solution/src/client.js new file mode 100644 index 000000000000..0cbc568e6c24 --- /dev/null +++ b/src/main/resources/templates/javascript/solution/src/client.js @@ -0,0 +1,72 @@ +import Context from './context.js'; +import Policy from './policy.js'; + +const ITERATIONS = 10; +const DATES_LENGTH_MIN = 5; +const DATES_LENGTH_MAX = 15; + +/** + * Main function. + * Add code to demonstrate your implementation here. + */ +function main() { + // Init Context and Policy + const context = new Context(); + const policy = new Policy(context); + + // Run multiple times to simulate different sorting strategies + for (let i = 0; i < ITERATIONS; i++) { + const dates = createRandomDates(); + + context.dates = dates; + policy.configure(); + + console.log('Unsorted Array of dates:'); + console.log(dates); + + context.sort(); + + console.log('Sorted Array of dates:'); + console.log(dates); + } +} + +/** + * Generates an Array of random Date objects with random Array length between + * {@link DATES_LENGTH_MIN} and {@link DATES_LENGTH_MAX}. + * + * @return an Array of random Date objects + */ +function createRandomDates() { + const length = randomIntegerWithin(DATES_LENGTH_MIN, DATES_LENGTH_MAX); + + const lowestDate = new Date('2024-09-15'); + const highestDate = new Date('2025-01-15'); + + return Array.from(Array(length), () => randomDateWithin(lowestDate, highestDate)); +} + +/** + * Creates a random Date within the given range. + * + * @param low {Date} the lower bound + * @param high {Date} the upper bound + * @return {Date} random Date within the given range + */ +function randomDateWithin(low, high) { + const randomTimestamp = randomIntegerWithin(low.valueOf(), high.valueOf()); + return new Date(randomTimestamp); +} + +/** + * Creates a random int within the given range. + * + * @param low {number} the lower bound + * @param high {number} the upper bound + * @returns {number} random int within the given range + */ +function randomIntegerWithin(low, high) { + return Math.floor(Math.random() * (high - low + 1)) + low; +} + +main(); diff --git a/src/main/resources/templates/javascript/solution/src/context.js b/src/main/resources/templates/javascript/solution/src/context.js new file mode 100644 index 000000000000..9041afb1152e --- /dev/null +++ b/src/main/resources/templates/javascript/solution/src/context.js @@ -0,0 +1,30 @@ +export default class Context { + /** @type {?{ performSort: (input: Date[]) => void }} */ + #sortAlgorithm = null; + + /** @type {Date[]} */ + #dates = []; + + /** + * Runs the configured sort algorithm. + */ + sort() { + this.#sortAlgorithm?.performSort(this.#dates); + } + + get sortAlgorithm() { + return this.#sortAlgorithm; + } + + set sortAlgorithm(sortAlgorithm) { + this.#sortAlgorithm = sortAlgorithm; + } + + get dates() { + return this.#dates; + } + + set dates(dates) { + this.#dates = dates; + } +} diff --git a/src/main/resources/templates/javascript/solution/src/mergesort.js b/src/main/resources/templates/javascript/solution/src/mergesort.js new file mode 100644 index 000000000000..7b1fa1c918e5 --- /dev/null +++ b/src/main/resources/templates/javascript/solution/src/mergesort.js @@ -0,0 +1,66 @@ +export default class MergeSort { + /** + * Wrapper method for the real MergeSort algorithm. + * + * @param input {Date[]} the array of Dates to be sorted + */ + performSort(input) { + mergesort(input, 0, input.length - 1); + } +} + +/** + * Recursive merge sort function + * + * @param input {Date[]} + * @param low {number} + * @param high {number} + */ +function mergesort(input, low, high) { + if (high - low < 1) { + return; + } + const mid = Math.floor((low + high) / 2); + mergesort(input, low, mid); + mergesort(input, mid + 1, high); + merge(input, low, mid, high); +} + +/** + * Merge function + * + * @param input {Date[]} + * @param low {number} + * @param middle {number} + * @param high {number} + */ +function merge(input, low, middle, high) { + const temp = new Array(high - low + 1); + + let leftIndex = low; + let rightIndex = middle + 1; + let wholeIndex = 0; + + while (leftIndex <= middle && rightIndex <= high) { + if (input[leftIndex] <= input[rightIndex]) { + temp[wholeIndex] = input[leftIndex++]; + } else { + temp[wholeIndex] = input[rightIndex++]; + } + wholeIndex++; + } + + if (leftIndex <= middle && rightIndex > high) { + while (leftIndex <= middle) { + temp[wholeIndex++] = input[leftIndex++]; + } + } else { + while (rightIndex <= high) { + temp[wholeIndex++] = input[rightIndex++]; + } + } + + for (wholeIndex = 0; wholeIndex < temp.length; wholeIndex++) { + input[wholeIndex + low] = temp[wholeIndex]; + } +} diff --git a/src/main/resources/templates/javascript/solution/src/policy.js b/src/main/resources/templates/javascript/solution/src/policy.js new file mode 100644 index 000000000000..15b9ed4cb23b --- /dev/null +++ b/src/main/resources/templates/javascript/solution/src/policy.js @@ -0,0 +1,35 @@ +import BubbleSort from './bubblesort.js'; +import MergeSort from './mergesort.js'; + +const DATES_LENGTH_THRESHOLD = 10; + +export default class Policy { + /** @type {Context} */ + #context; + + /** + * @param context {Context} + */ + constructor(context) { + this.#context = context; + } + + /** + * Chooses a strategy depending on the number of date objects. + */ + configure() { + if (this.#context.dates.length > DATES_LENGTH_THRESHOLD) { + this.#context.sortAlgorithm = new MergeSort(); + } else { + this.#context.sortAlgorithm = new BubbleSort(); + } + } + + get context() { + return this.#context; + } + + set context(context) { + this.#context = context; + } +} diff --git a/src/main/resources/templates/javascript/test/.gitignore b/src/main/resources/templates/javascript/test/.gitignore new file mode 100644 index 000000000000..d81d793eaed4 --- /dev/null +++ b/src/main/resources/templates/javascript/test/.gitignore @@ -0,0 +1,4 @@ +node_modules/ + +/assignment +/junit.xml diff --git a/src/main/resources/templates/javascript/test/babel.config.json b/src/main/resources/templates/javascript/test/babel.config.json new file mode 100644 index 000000000000..2c69a84a9fad --- /dev/null +++ b/src/main/resources/templates/javascript/test/babel.config.json @@ -0,0 +1,6 @@ +{ + "targets": { + "node": 20 + }, + "presets": ["@babel/preset-env"] +} diff --git a/src/main/resources/templates/javascript/test/package-lock.json b/src/main/resources/templates/javascript/test/package-lock.json new file mode 100644 index 000000000000..b18c57c3b694 --- /dev/null +++ b/src/main/resources/templates/javascript/test/package-lock.json @@ -0,0 +1,5925 @@ +{ + "name": "artemis-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "artemis-test", + "workspaces": [ + "assignment" + ], + "devDependencies": { + "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@types/jest": "^29.5.12", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-junit": "^16.0.0" + } + }, + "assignment": { + "name": "artemis-exercise" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.14.8", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/artemis-exercise": { + "resolved": "assignment", + "link": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001636", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.810", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.14", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/main/resources/templates/javascript/test/package.json b/src/main/resources/templates/javascript/test/package.json new file mode 100644 index 000000000000..3971d2b0f3c6 --- /dev/null +++ b/src/main/resources/templates/javascript/test/package.json @@ -0,0 +1,24 @@ +{ + "name": "artemis-test", + "private": true, + "scripts": { + "test": "jest", + "test:ci": "jest --ci --reporters=default --reporters=jest-junit" + }, + "workspaces": [ + "assignment" + ], + "devDependencies": { + "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@types/jest": "^29.5.12", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-junit": "^16.0.0" + }, + "jest-junit": { + "classNameTemplate": "{classname}_{title}", + "titleTemplate": "{classname}_{title}", + "ancestorSeparator": "_" + } +} diff --git a/src/main/resources/templates/javascript/test/src/behavior.test.js b/src/main/resources/templates/javascript/test/src/behavior.test.js new file mode 100644 index 000000000000..1a1d1ba86ff7 --- /dev/null +++ b/src/main/resources/templates/javascript/test/src/behavior.test.js @@ -0,0 +1,71 @@ +import MergeSort from 'artemis-exercise/mergesort.js'; +import BubbleSort from 'artemis-exercise/bubblesort.js'; +import Context from 'artemis-exercise/context.js'; +import Policy from 'artemis-exercise/policy.js'; + +// prettier-ignore +const datesWithCorrectOrder = [ + new Date('2016-02-15'), + new Date('2017-04-15'), + new Date('2017-09-15'), + new Date('2018-11-08'), +]; + +describe('behavior', () => { + let dates; + beforeEach(() => { + // prettier-ignore + dates = [ + new Date('2018-11-08'), + new Date('2017-04-15'), + new Date('2016-02-15'), + new Date('2017-09-15'), + ]; + }); + + describe('BubbleSort', () => { + it('should_sort_correctly', () => { + const bubbleSort = new BubbleSort(); + bubbleSort.performSort(dates); + expect(dates).toEqual(datesWithCorrectOrder); + }); + }); + + describe('MergeSort', () => { + it('should_sort_correctly', () => { + const mergeSort = new MergeSort(); + mergeSort.performSort(dates); + expect(dates).toEqual(datesWithCorrectOrder); + }); + }); + + describe('Policy', () => { + it('uses_MergeSort_for_big_list', () => { + const bigList = []; + for (let i = 0; i < 11; i++) { + bigList.push(new Date()); + } + + const context = new Context(); + context.dates = bigList; + const policy = new Policy(context); + policy.configure(); + const chosenSortStrategy = context.sortAlgorithm; + expect(chosenSortStrategy).toBeInstanceOf(MergeSort); + }); + + it('uses_BubbleSort_for_small_list', () => { + const smallList = []; + for (let i = 0; i < 3; i++) { + smallList.push(new Date()); + } + + const context = new Context(); + context.dates = smallList; + const policy = new Policy(context); + policy.configure(); + const chosenSortStrategy = context.sortAlgorithm; + expect(chosenSortStrategy).toBeInstanceOf(BubbleSort); + }); + }); +}); diff --git a/src/main/resources/templates/javascript/test/src/structural.test.js b/src/main/resources/templates/javascript/test/src/structural.test.js new file mode 100644 index 000000000000..ed9ee705aa78 --- /dev/null +++ b/src/main/resources/templates/javascript/test/src/structural.test.js @@ -0,0 +1,44 @@ +import MergeSort from 'artemis-exercise/mergesort.js'; +import BubbleSort from 'artemis-exercise/bubblesort.js'; +import Context from 'artemis-exercise/context.js'; +import Policy from 'artemis-exercise/policy.js'; + +describe('structural', () => { + describe('Context', () => { + const context = new Context(); + + it('has_properties', () => { + expect(context).toHaveProperty('dates'); + expect(context).toHaveProperty('sortAlgorithm'); + }); + + it('has_methods', () => { + expect(context).toHaveProperty('sort', expect.any(Function)); + }); + }); + + describe('Policy', () => { + const context = new Context(); + const policy = new Policy(context); + + it('has_properties', () => { + expect(policy).toHaveProperty('context'); + }); + + it('has_methods', () => { + expect(policy).toHaveProperty('configure', expect.any(Function)); + }); + }); + + describe('BubbleSort', () => { + it('has_method', () => { + expect(BubbleSort.prototype).toHaveProperty('performSort', expect.any(Function)); + }); + }); + + describe('MergeSort', () => { + it('has_method', () => { + expect(MergeSort.prototype).toHaveProperty('performSort', expect.any(Function)); + }); + }); +}); diff --git a/src/main/resources/templates/jenkins/empty/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/empty/regularRuns/pipeline.groovy index b4c800a23cf1..7d52cd089c67 100644 --- a/src/main/resources/templates/jenkins/empty/regularRuns/pipeline.groovy +++ b/src/main/resources/templates/jenkins/empty/regularRuns/pipeline.groovy @@ -28,10 +28,13 @@ private void runTestSteps() { * Run unit tests */ private void test() { - stage('Build') { - sh ''' - mvn --version - ''' + stage('Install dependencies') { + // TODO: Install dependencies not provided by the Docker image + sh 'echo "Install dependencies"' + } + stage('Run tests') { + // TODO: Run the tests and generate JUnit XMLs + sh 'echo "Hello World"' } } @@ -45,6 +48,7 @@ void postBuildTasks() { rm -rf results mkdir results ''' + // TODO: Move JUnit XMLs into the results directory } // very important, do not remove diff --git a/src/main/resources/templates/jenkins/javascript/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/javascript/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..bc37c762097a --- /dev/null +++ b/src/main/resources/templates/jenkins/javascript/regularRuns/pipeline.groovy @@ -0,0 +1,59 @@ +/* + * This file configures the actual build steps for the automatic grading. + * + * !!! + * For regular exercises, there is no need to make changes to this file. + * Only this base configuration is actively supported by the Artemis maintainers + * and/or your Artemis instance administrators. + * !!! + */ + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' + +/** + * Main function called by Jenkins. + */ +void testRunner() { + docker.image(dockerImage).inside(dockerFlags) { c -> + runTestSteps() + } +} + +private void runTestSteps() { + test() +} + +/** + * Run unit tests + */ +private void test() { + stage('Build') { + sh 'npm ci --prefer-offline --no-audit' + } + stage('Test') { + sh 'npm run test:ci' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + if [ -e junit.xml ] + then + sed -i 's/]*>//g ; s/<\\/testsuites>/<\\/testsuite>/g' junit.xml + fi + cp junit.xml $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' +} + +// very important, do not remove +// required so that Jenkins finds the methods defined in this script +return this diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html index 680ebefc10d8..725183033342 100644 --- a/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html @@ -1,7 +1,7 @@ -

+

- @if (garbageCollectorMetrics) { + @if (garbageCollectorMetrics && garbageCollectorMetrics['jvm.gc.live.data.size']) {
GC Live Data Size/GC Max Data Size ({{ garbageCollectorMetrics['jvm.gc.live.data.size'] / 1048576 | number: '1.0-0' }}M / @@ -19,8 +19,10 @@

}
-
- @if (garbageCollectorMetrics) { +
+
+ @if (garbageCollectorMetrics && garbageCollectorMetrics['jvm.gc.memory.promoted']) { +
GC Memory Promoted/GC Memory Allocated ({{ garbageCollectorMetrics['jvm.gc.memory.promoted'] / 1048576 | number: '1.0-0' }}M / @@ -36,8 +38,10 @@

{{ (100 * garbageCollectorMetrics['jvm.gc.memory.promoted']) / garbageCollectorMetrics['jvm.gc.memory.allocated'] | number: '1.0-2' }}%
- } -
+
+ } +
+
@if (garbageCollectorMetrics) {
@@ -50,6 +54,8 @@

}
+
+
@if (!updating && garbageCollectorMetrics) {
@@ -67,17 +73,19 @@

- - - - - - - - - - - + @if (garbageCollectorMetrics['jvm.gc.pause']) { + + + + + + + + + + + + }
jvm.gc.pause{{ garbageCollectorMetrics['jvm.gc.pause'].count }}{{ garbageCollectorMetrics['jvm.gc.pause'].mean | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.0'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.5'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.75'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.95'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.99'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause'].max | number: '1.0-3' }}
jvm.gc.pause{{ garbageCollectorMetrics['jvm.gc.pause'].count }}{{ garbageCollectorMetrics['jvm.gc.pause'].mean | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.0'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.5'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.75'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.95'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause']['0.99'] | number: '1.0-3' }}{{ garbageCollectorMetrics['jvm.gc.pause'].max | number: '1.0-3' }}
diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index 3d47a8320bd2..ef2d95985068 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -24,6 +24,7 @@ export enum ProgrammingLanguage { OCAML = 'OCAML', EMPTY = 'EMPTY', RUST = 'RUST', + JAVASCRIPT = 'JAVASCRIPT', } export enum ProjectType { diff --git a/src/main/webapp/app/entities/vcs-access-log-entry.model.ts b/src/main/webapp/app/entities/vcs-access-log-entry.model.ts new file mode 100644 index 000000000000..c2571e9ddd0d --- /dev/null +++ b/src/main/webapp/app/entities/vcs-access-log-entry.model.ts @@ -0,0 +1,13 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import dayjs from 'dayjs/esm'; + +export class VcsAccessLogDTO implements BaseEntity { + public id?: number; + public userId?: number; + public name?: string; + public email?: string; + public repositoryActionType: string; + public authenticationMechanism: string; + public commitHash?: string; + public timestamp: dayjs.Dayjs; +} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts index 54c277c9bd7d..cf949670dcf9 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts @@ -18,6 +18,7 @@ import { RepositoryViewComponent } from 'app/localvc/repository-view/repository- import { CommitHistoryComponent } from 'app/localvc/commit-history/commit-history.component'; import { CommitDetailsViewComponent } from 'app/localvc/commit-details-view/commit-details-view.component'; import { LocalVCGuard } from 'app/localvc/localvc-guard.service'; +import { VcsRepositoryAccessLogViewComponent } from 'app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component'; @Injectable({ providedIn: 'root' }) export class ProgrammingExerciseResolve implements Resolve { @@ -183,6 +184,18 @@ export const routes: Routes = [ }, canActivate: [LocalVCGuard], }, + { + path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/vcs-access-log', + component: VcsRepositoryAccessLogViewComponent, + data: { + authorities: [Authority.ADMIN, Authority.INSTRUCTOR], + pageTitle: 'artemisApp.repository.title', + flushRepositoryCacheAfter: 900000, // 15 min + participationCache: {}, + repositoryCache: {}, + }, + canActivate: [LocalVCGuard], + }, { path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/commit-history/:commitHash', component: CommitDetailsViewComponent, @@ -219,6 +232,18 @@ export const routes: Routes = [ }, canActivate: [UserRouteAccessService, LocalVCGuard], }, + { + path: ':courseId/programming-exercises/:exerciseId/participations/:participationId/repository/vcs-access-log', + component: VcsRepositoryAccessLogViewComponent, + data: { + authorities: [Authority.ADMIN, Authority.INSTRUCTOR], + pageTitle: 'artemisApp.repository.title', + flushRepositoryCacheAfter: 900000, // 15 min + participationCache: {}, + repositoryCache: {}, + }, + canActivate: [UserRouteAccessService, LocalVCGuard], + }, { path: ':courseId/programming-exercises/:exerciseId/participations/:participationId/repository/commit-history/:commitHash', component: CommitDetailsViewComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index e5ece788a0fb..e83414219bb6 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -8,6 +8,7 @@ import { Result } from 'app/entities/result.model'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; import { createRequestOption } from 'app/shared/util/request.util'; import { Observable, map, tap } from 'rxjs'; +import { VcsAccessLogDTO } from 'app/entities/vcs-access-log-entry.model'; export interface IProgrammingExerciseParticipationService { getLatestResultWithFeedback: (participationId: number, withSubmission: boolean) => Observable; @@ -145,6 +146,33 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer return this.http.get(`${this.resourceUrlParticipations}${participationId}/commits-info`); } + /** + * Get the vcs access log for a given participation id. + * The current user needs to be at least an instructor in the course of the participation. + * @param participationId of the participation to get the vcs Access log + */ + getVcsAccessLogForParticipation(participationId: number): Observable { + return this.http + .get(`${this.resourceUrlParticipations}${participationId}/vcs-access-log`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); + } + + /** + * Get the vcs access log for a given exercise id and the repository type. + * The current user needs to be at least a instructor in the course of the participation. + * @param exerciseId of the exercise to get the vcs Access log + * @param repositoryType of the repository of the exercise, to get the vcs Access log + */ + getVcsAccessLogForRepository(exerciseId: number, repositoryType: string): Observable { + const params: { [key: string]: number | string } = {}; + if (repositoryType) { + params['repositoryType'] = repositoryType; + } + return this.http + .get(`${this.resourceUrl}${exerciseId}/vcs-access-log/${repositoryType}`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); + } + /** * Get the repository files with content for a given participation id at a specific commit hash. * The current user needs to be at least a student in the course of the participation. diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index e1257c9c467f..aff229febc88 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -270,6 +270,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } else { this.programmingExercise.buildConfig = new ProgrammingExerciseBuildConfig(); } + this.programmingExercise.customizeBuildPlanWithAeolus = language === ProgrammingLanguage.EMPTY; } // If we switch to another language which does not support static code analysis we need to reset options related to static code analysis diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html index 2f4b0b556c7f..bdabddb25dc8 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html @@ -56,6 +56,28 @@ }
} + @if (programmingExercise.programmingLanguage === ProgrammingLanguage.EMPTY) { +
+

+ + +

+
    +
  1. +
  2. +

    +
      +
    1. +
    2. +
    3. +
    +
  3. +
  4. +
  5. +
  6. +
+
+ } @if (programmingExercise.programmingLanguage && programmingExerciseCreationConfig.packageNameRequired && programmingExercise.projectType !== ProjectType.XCODE) {
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts index 696d75ce185f..26951968a7b2 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts @@ -1,6 +1,6 @@ import { AfterViewChecked, AfterViewInit, Component, EventEmitter, Input, OnDestroy, ViewChild } from '@angular/core'; import { ProgrammingExercise, ProgrammingLanguage, ProjectType } from 'app/entities/programming/programming-exercise.model'; -import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { PROFILE_AEOLUS, PROFILE_LOCALCI } from 'app/app.constants'; import { NgModel } from '@angular/forms'; @@ -32,10 +32,16 @@ export class ProgrammingExerciseLanguageComponent implements AfterViewChecked, A fieldSubscriptions: (Subscription | undefined)[] = []; - faQuestionCircle = faQuestionCircle; + faExclamationTriangle = faExclamationTriangle; protected readonly PROFILE_LOCALCI = PROFILE_LOCALCI; protected readonly PROFILE_AEOLUS = PROFILE_AEOLUS; + readonly DOCKER_REGISTRY_LINKS = { + ghcrLink: 'https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry', + dockerhubLink: 'https://hub.docker.com/', + }; + readonly DOCUMENTATION_LINK = 'https://docs.artemis.cit.tum.de/user/exercises/programming.html'; + ngAfterViewInit() { this.fieldSubscriptions.push(this.selectLanguageField.valueChanges?.subscribe(() => setTimeout(() => this.calculateFormValid()))); } diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.html b/src/main/webapp/app/localvc/repository-view/repository-view.component.html index b3e821dbf431..6a41335a4f51 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.html +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.html @@ -34,6 +34,13 @@

} + @if (vcsAccessLogRoute && enableVcsAccessLog && allowVcsAccessLog && localVcEnabled) { + + + + + } + @if (exercise?.allowOfflineIde) { { + this.localVcEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + }); } /** @@ -122,6 +133,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { this.participationCouldNotBeFetched = true; this.loadingParticipation = false; } + this.allowVcsAccessLog = this.accountService.isAtLeastInstructorInCourse(this.getCourseFromExercise(this.exercise)); }), ) .subscribe({ @@ -146,6 +158,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { this.domainService.setDomain([DomainType.PARTICIPATION, participationWithResults]); this.participation = participationWithResults; this.exercise = this.participation.exercise as ProgrammingExercise; + this.allowVcsAccessLog = this.accountService.isAtLeastInstructorInCourse(this.getCourseFromExercise(this.exercise)); this.repositoryUri = this.participation.repositoryUri!; }), ) diff --git a/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.html b/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.html new file mode 100644 index 000000000000..32f6eaaf0da6 --- /dev/null +++ b/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.html @@ -0,0 +1,34 @@ + +
+

+
+ +
+ + + + + + + + + + + + + + @for (entry of vcsAccessLogEntries(); track entry; let i = $index) { + + + + + + + + + + } + +
#
{{ i }}{{ entry.userId }}{{ entry.name + ', ' + entry.email }}{{ entry.repositoryActionType }}{{ entry.authenticationMechanism }}{{ entry.commitHash }}{{ entry.timestamp }}
+
+
diff --git a/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.ts b/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.ts new file mode 100644 index 000000000000..0f72191f8405 --- /dev/null +++ b/src/main/webapp/app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component.ts @@ -0,0 +1,65 @@ +import { Component, computed, effect, inject, signal } from '@angular/core'; +import { Observable, lastValueFrom } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { ProgrammingExerciseParticipationService } from 'app/exercises/programming/manage/services/programming-exercise-participation.service'; +import { VcsAccessLogDTO } from 'app/entities/vcs-access-log-entry.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; + +@Component({ + selector: 'jhi-vcs-repository-access-log-view', + templateUrl: './vcs-repository-access-log-view.component.html', + standalone: true, + imports: [TranslateDirective], +}) +export class VcsRepositoryAccessLogViewComponent { + private readonly route = inject(ActivatedRoute); + private readonly programmingExerciseParticipationService = inject(ProgrammingExerciseParticipationService); + private readonly alertService = inject(AlertService); + + protected readonly vcsAccessLogEntries = signal([]); + + private readonly params = toSignal(this.route.params, { requireSync: true }); + private readonly participationId = computed(() => { + const participationId = this.params().participationId; + if (participationId) { + return Number(participationId); + } + return undefined; + }); + private readonly exerciseId = computed(() => Number(this.params().exerciseId)); + private readonly repositoryType = computed(() => String(this.params().repositoryType)); + + constructor() { + effect( + async () => { + if (this.participationId()) { + await this.loadVcsAccessLogForParticipation(this.participationId()!); + } else { + await this.loadVcsAccessLog(this.exerciseId(), this.repositoryType()); + } + }, + { allowSignalWrites: true }, + ); + } + + public async loadVcsAccessLogForParticipation(participationId: number) { + await this.extractEntries(() => this.programmingExerciseParticipationService.getVcsAccessLogForParticipation(participationId)); + } + + public async loadVcsAccessLog(exerciseId: number, repositoryType: string) { + await this.extractEntries(() => this.programmingExerciseParticipationService.getVcsAccessLogForRepository(exerciseId, repositoryType)); + } + + private async extractEntries(fetchVcsAccessLogs: () => Observable) { + try { + const accessLogEntries = await lastValueFrom(fetchVcsAccessLogs()); + if (accessLogEntries) { + this.vcsAccessLogEntries.set(accessLogEntries); + } + } catch (error) { + this.alertService.error('artemisApp.repository.vcsAccessLog.error'); + } + } +} diff --git a/src/main/webapp/app/overview/course-dashboard/course-dashboard.service.ts b/src/main/webapp/app/overview/course-dashboard/course-dashboard.service.ts index aab8b86f6153..a6a1e09c0750 100644 --- a/src/main/webapp/app/overview/course-dashboard/course-dashboard.service.ts +++ b/src/main/webapp/app/overview/course-dashboard/course-dashboard.service.ts @@ -112,7 +112,7 @@ export class CourseDashboardService { return ExerciseType.QUIZ; case 'de.tum.cit.aet.artemis.text.domain.TextExercise': return ExerciseType.TEXT; - case 'de.tum.cit.aet.artemis.file.domain.FileUploadExercise': + case 'de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise': return ExerciseType.FILE_UPLOAD; default: throw new Error(`Unknown exercise type: ${type}`); @@ -121,15 +121,15 @@ export class CourseDashboardService { private mapToLectureUnitType(type: string): LectureUnitType { switch (type) { - case 'de.tum.cit.aet.artemis.domain.lecture.AttachmentUnit': + case 'de.tum.cit.aet.artemis.lecture.domain.AttachmentUnit': return LectureUnitType.ATTACHMENT; - case 'de.tum.cit.aet.artemis.domain.lecture.ExerciseUnit': + case 'de.tum.cit.aet.artemis.lecture.domain.ExerciseUnit': return LectureUnitType.EXERCISE; - case 'de.tum.cit.aet.artemis.domain.lecture.TextUnit': + case 'de.tum.cit.aet.artemis.lecture.domain.TextUnit': return LectureUnitType.TEXT; - case 'de.tum.cit.aet.artemis.domain.lecture.VideoUnit': + case 'de.tum.cit.aet.artemis.lecture.domain.VideoUnit': return LectureUnitType.VIDEO; - case 'de.tum.cit.aet.artemis.domain.lecture.OnlineUnit': + case 'de.tum.cit.aet.artemis.lecture.domain.OnlineUnit': return LectureUnitType.ONLINE; default: throw new Error(`Unknown lecture unit type: ${type}`); diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 4e493f7d4daf..9d67c6853e90 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -657,6 +657,19 @@ "template": "Vorlage", "history": "Commit-Historie", "result": "Ergebnis" + }, + "otherLanguageHints": { + "title": "Führe folgende Schritte aus, um eine andere Sprache zu benutzen. Siehe die Dokumentation für Details.", + "setImage": "Lege ein angepasstes Docker Image mit benötigten Abhängigkeiten fest (erstelle möglicherweise ein eigenes und lade es in die GitHub Container Registry oder auf den Docker Hub hoch).", + "buildScript": { + "title": "Passe das Build Skript an:", + "dependencies": "Optional: Installiere aufgabenspezifische Abhängigkeiten.", + "runTests": "Führe alle Tests aus. Sie sollten JUnit XML Dateien generieren.", + "moveResults": "Bewege die XML Dateien in das {{resultsDirectory}} Verzeichnis." + }, + "pushExercise": "Lade den Aufgaben-Code in das entsprechende Repository. Es wird in {{checkoutDirectory}} ausgecheckt.", + "pushSolution": "Lade den Lösungs-Code in das entsprechende Repository. Es wird nicht ausgecheckt.", + "pushTest": "Lade den Test-Code in das entsprechende Repository. Es wird in {{checkoutDirectory}} ausgecheckt." } }, "error": { diff --git a/src/main/webapp/i18n/de/repository.json b/src/main/webapp/i18n/de/repository.json index 241f5d8bc21d..6bd6d6d28842 100644 --- a/src/main/webapp/i18n/de/repository.json +++ b/src/main/webapp/i18n/de/repository.json @@ -20,6 +20,17 @@ "empty": "Keine Änderungen", "fileUnchanged": "Keine Änderungen am Dateiinhalt" } + }, + "vcsAccessLog": { + "title": "VCS-Zugriffsprotokoll", + "openVcsAccessLog": "Zugriffsprotokoll öffnen", + "userId": "Benutzer Id", + "author": "Autor", + "actionType": "Aktionstyp", + "authMechanism": "Zugriffsart", + "commitHash": "Commit Hash", + "timeStamp": "Zeitstempel", + "error": "VCS-Zugriffsprotokoll konnte nicht abgerufen werden" } } } diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 1883d8294abc..30712348289a 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -658,6 +658,19 @@ "template": "Template", "history": "Commit History", "result": "Result" + }, + "otherLanguageHints": { + "title": "To use another language, complete the following tasks. See the documentation for details.", + "setImage": "Specify a custom Docker image with the required dependencies (potentially create a custom one and upload it to the GitHub Container Registry or Docker Hub).", + "buildScript": { + "title": "Customize the build script to:", + "dependencies": "Optional: Install exercise specific dependencies.", + "runTests": "Run all tests. They should generate JUnit XML files.", + "moveResults": "Move the XML files into the {{resultsDirectory}} directory." + }, + "pushExercise": "Push the exercise code to its repository. It will be checked out at {{checkoutDirectory}}.", + "pushSolution": "Push the solution code to its repository. It will not be checked out.", + "pushTest": "Push the test code to its repository. It will be checked out in {{checkoutDirectory}}." } }, "error": { diff --git a/src/main/webapp/i18n/en/repository.json b/src/main/webapp/i18n/en/repository.json index 59b50fcb0481..8ed72d96e6ee 100644 --- a/src/main/webapp/i18n/en/repository.json +++ b/src/main/webapp/i18n/en/repository.json @@ -20,6 +20,17 @@ "empty": "No changes", "fileUnchanged": "No changes in the file content" } + }, + "vcsAccessLog": { + "title": "VCS Access Log", + "openVcsAccessLog": "Open Access Log", + "userId": "User Id", + "author": "Author", + "actionType": "Action Type", + "authMechanism": "Access Type", + "commitHash": "Commit Hash", + "timeStamp": "Timestamp", + "error": "VCS Access log could not be retrieved" } } } diff --git a/src/test/java/de/tum/cit/aet/artemis/JhiMetricsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/JhiMetricsIntegrationTest.java new file mode 100644 index 000000000000..a5ddcf176a6d --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/JhiMetricsIntegrationTest.java @@ -0,0 +1,45 @@ +package de.tum.cit.aet.artemis; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +class JhiMetricsIntegrationTest extends AbstractSpringIntegrationIndependentTest { + + @Autowired + private ObjectMapper objectMapper; + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void getMetricsTest() throws Exception { + var result = request.get("/management/jhimetrics", HttpStatus.OK, String.class); + + JsonNode rootNode = objectMapper.readTree(result); + + assertThat(rootNode.has("jvm")).isTrue(); + assertThat(rootNode.path("jvm").has("G1 Old Gen")).isTrue(); + assertThat(rootNode.path("jvm").path("G1 Old Gen").has("committed")).isTrue(); + assertThat(rootNode.path("jvm").path("G1 Old Gen").has("max")).isTrue(); + assertThat(rootNode.path("jvm").path("G1 Old Gen").has("used")).isTrue(); + + assertThat(rootNode.has("http.server.requests")).isTrue(); + assertThat(rootNode.path("http.server.requests").path("all").has("count")).isTrue(); + + assertThat(rootNode.has("cache")).isTrue(); + + assertThat(rootNode.has("garbageCollector")).isTrue(); + assertThat(rootNode.path("garbageCollector").has("jvm.gc.max.data.size")).isTrue(); + assertThat(rootNode.path("garbageCollector").path("jvm.gc.pause").has("max")).isTrue(); + + assertThat(rootNode.has("processMetrics")).isTrue(); + assertThat(rootNode.path("processMetrics").has("process.cpu.usage")).isTrue(); + + assertThat(rootNode.has("customMetrics")).isTrue(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIIntegrationTest.java index 10fea95376be..684439d104d9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIIntegrationTest.java @@ -444,7 +444,7 @@ void testBuildLogs() throws IOException { assertThat(resultBuildJobSet.iterator().next().buildJobId()).isEqualTo(buildJob.getBuildJobId()); // Assert that the corresponding build job are stored in the file system - assertThat(buildLogEntryService.buildJobHasLogFile(buildJob.getBuildJobId())).isTrue(); + assertThat(buildLogEntryService.buildJobHasLogFile(buildJob.getBuildJobId(), studentParticipation.getProgrammingExercise())).isTrue(); // Retrieve the build logs from the file system buildLogs = buildLogEntryService.retrieveBuildLogsFromFileForBuildJob(buildJob.getBuildJobId()); diff --git a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIResourceIntegrationTest.java index 0a553702716a..9c048bb7696a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalCIResourceIntegrationTest.java @@ -54,6 +54,8 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTe protected BuildJob finishedJob3; + protected BuildJob finishedJobForLogs; + @Autowired @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @@ -95,17 +97,22 @@ void createJobs() { buildConfig, null); BuildJobQueueItem finishedJobQueueItem3 = new BuildJobQueueItem("5", "job5", "address1", 5, course.getId() + 2, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, buildConfig, null); + BuildJobQueueItem finishedJobQueueItemForLogs = new BuildJobQueueItem("6", "job5", "address1", 5, course.getId(), programmingExercise.getId(), 1, 1, BuildStatus.FAILED, + repositoryInfo, jobTimingInfo3, buildConfig, null); var result1 = new Result().successful(true).rated(true).score(100D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var result2 = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var result3 = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); + var resultForLogs = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); resultRepository.save(result1); resultRepository.save(result2); resultRepository.save(result3); + resultRepository.save(resultForLogs); finishedJob1 = new BuildJob(finishedJobQueueItem1, BuildStatus.SUCCESSFUL, result1); finishedJob2 = new BuildJob(finishedJobQueueItem2, BuildStatus.FAILED, result2); finishedJob3 = new BuildJob(finishedJobQueueItem3, BuildStatus.FAILED, result3); + finishedJobForLogs = new BuildJob(finishedJobQueueItemForLogs, BuildStatus.FAILED, resultForLogs); queuedJobs = hazelcastInstance.getQueue("buildJobQueue"); processingJobs = hazelcastInstance.getMap("processingJobs"); @@ -321,13 +328,15 @@ void testGetBuildAgents_instructorAccessForbidden() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetBuildLogsForResult() throws Exception { try { + buildJobRepository.save(finishedJobForLogs); BuildLogEntry buildLogEntry = new BuildLogEntry(ZonedDateTime.now(), "Dummy log"); - buildLogEntryService.saveBuildLogsToFile(List.of(buildLogEntry), "0"); - var response = request.get("/api/build-log/0", HttpStatus.OK, String.class); + buildLogEntryService.saveBuildLogsToFile(List.of(buildLogEntry), "6", programmingExercise); + var response = request.get("/api/build-log/6", HttpStatus.OK, String.class); assertThat(response).contains("Dummy log"); } finally { - Path buildLogFile = Path.of("build-logs", "0.log"); + Path buildLogFile = Path.of("build-logs").resolve(programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName()) + .resolve(programmingExercise.getShortName()).resolve("6.log"); Files.deleteIfExists(buildLogFile); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java index 4ccddacc3fbf..1ac8510ab262 100644 --- a/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -14,9 +15,15 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.cit.aet.artemis.participation.ParticipationUtilService; +import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; +import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; +import de.tum.cit.aet.artemis.programming.dto.VcsAccessLogDTO; +import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; +import de.tum.cit.aet.artemis.programming.web.repository.RepositoryActionType; import de.tum.cit.aet.artemis.util.LocalRepository; class LocalVCLocalCIParticipationIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -26,6 +33,21 @@ class LocalVCLocalCIParticipationIntegrationTest extends AbstractSpringIntegrati @Autowired private ProgrammingExerciseUtilService programmingExerciseUtilService; + @Autowired + private VcsAccessLogRepository vcsAccessLogRepository; + + @Autowired + private ParticipationUtilService participationUtilService; + + private ProgrammingExercise programmingExercise; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, 4, 2, 0, 2); + Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); + programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testStartParticipation() throws Exception { @@ -63,4 +85,27 @@ void testStartParticipation() throws Exception { templateRepository.resetLocalRepo(); } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetVcsAccessLog() throws Exception { + var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "instructor1"); + var user = userRepository.getUser(); + vcsAccessLogRepository.save(new VcsAccessLog(user, participation, "instructor", "instructorMail@mail.de", RepositoryActionType.READ, AuthenticationMechanism.SSH, "", "")); + var li = request.getList("/api/programming-exercise-participations/" + participation.getId() + "/vcs-access-log", HttpStatus.OK, VcsAccessLogDTO.class); + assertThat(li.size()).isEqualTo(1); + assertThat(li.getFirst().userId()).isEqualTo(user.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetVcsAccessLogOfTemplateParticipation() throws Exception { + var user = userRepository.getUser(); + vcsAccessLogRepository.save(new VcsAccessLog(user, programmingExercise.getTemplateParticipation(), "instructor", "instructorMail@mail.de", RepositoryActionType.READ, + AuthenticationMechanism.SSH, "", "")); + var li = request.getList("/api/programming-exercise/" + programmingExercise.getId() + "/vcs-access-log/TEMPLATE", HttpStatus.OK, VcsAccessLogDTO.class); + assertThat(li.size()).isEqualTo(1); + assertThat(li.getFirst().userId()).isEqualTo(user.getId()); + } + } diff --git a/src/test/java/de/tum/cit/aet/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/service/ParticipationServiceTest.java index 7ba508ff7fef..89965b5a87e1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/service/ParticipationServiceTest.java @@ -142,7 +142,7 @@ void testGetBuildJobsForResultsOfParticipation() throws Exception { List results = resultRepository.findAllByParticipationIdOrderByCompletionDateDesc(participation.getId()); - Map resultBuildJobMap = resultService.getLogsAvailabilityForResults(results); + Map resultBuildJobMap = resultService.getLogsAvailabilityForResults(results, participation); assertThat(resultBuildJobMap).hasSize(0); assertThat(participation).isNotNull(); assertThat(participation.getSubmissions()).hasSize(1); diff --git a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts index 5f4fae4014a9..ff004d03c91b 100644 --- a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts +++ b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts @@ -16,6 +16,8 @@ import { HttpResponse } from '@angular/common/http'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { DueDateStat } from 'app/course/dashboards/due-date-stat.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { MockProfileService } from '../../helpers/mocks/service/mock-profile.service'; describe('RepositoryViewComponent', () => { let component: RepositoryViewComponent; @@ -39,6 +41,7 @@ describe('RepositoryViewComponent', () => { { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }, { provide: Router, useClass: MockRouter }, + { provide: ProfileService, useClass: MockProfileService }, ], }) .compileComponents() @@ -206,7 +209,16 @@ describe('RepositoryViewComponent', () => { const mockParticipation: ProgrammingExerciseStudentParticipation = { id: 2, repositoryUri: 'student-repo-uri', - exercise: { id: 1, numberOfAssessmentsOfCorrectionRounds: [new DueDateStat()], studentAssignedTeamIdComputed: true, secondCorrectionEnabled: true }, + exercise: { + id: 1, + numberOfAssessmentsOfCorrectionRounds: [new DueDateStat()], + studentAssignedTeamIdComputed: true, + secondCorrectionEnabled: true, + course: { + instructorGroupName: 'instructorGroup', + isAtLeastInstructor: true, + }, + }, results: [ { id: 3, successful: true, score: 100, rated: true, hasComplaint: false, exampleResult: false, testCaseCount: 10, passedTestCaseCount: 10, codeIssueCount: 0 }, { id: 4, successful: true, score: 100, rated: true, hasComplaint: false, exampleResult: false, testCaseCount: 10, passedTestCaseCount: 10, codeIssueCount: 0 }, diff --git a/src/test/javascript/spec/component/localvc/vcs-repository-access-log-view.component.spec.ts b/src/test/javascript/spec/component/localvc/vcs-repository-access-log-view.component.spec.ts new file mode 100644 index 000000000000..44d03a38d661 --- /dev/null +++ b/src/test/javascript/spec/component/localvc/vcs-repository-access-log-view.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProgrammingExerciseParticipationService } from 'app/exercises/programming/manage/services/programming-exercise-participation.service'; +import { ActivatedRoute } from '@angular/router'; +import { MockProgrammingExerciseParticipationService } from '../../helpers/mocks/service/mock-programming-exercise-participation.service'; +import dayjs from 'dayjs/esm'; +import { of } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service'; +import { VcsRepositoryAccessLogViewComponent } from 'app/localvc/vcs-repository-access-log-view/vcs-repository-access-log-view.component'; +import { VcsAccessLogDTO } from 'app/entities/vcs-access-log-entry.model'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { MockAlertService } from '../../helpers/mocks/service/mock-alert.service'; +import { MockProfileService } from '../../helpers/mocks/service/mock-profile.service'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; + +describe('VcsRepositoryAccessLogViewComponent', () => { + let fixture: ComponentFixture; + let programmingExerciseParticipationService: ProgrammingExerciseParticipationService; + const userId = 4; + let participationVcsAccessLogSpy: jest.SpyInstance; + let repositoryVcsAccessLogSpy: jest.SpyInstance; + + const mockVcsAccessLog: VcsAccessLogDTO[] = [ + { + id: 1, + userId: userId, + name: 'authorName', + email: 'authorEmail', + commitHash: 'abcde', + authenticationMechanism: 'SSH', + repositoryActionType: 'WRITE', + timestamp: dayjs('2021-01-02'), + }, + { + id: 2, + userId: userId, + name: 'authorName', + email: 'authorEmail', + commitHash: 'fffee', + authenticationMechanism: 'SSH', + repositoryActionType: 'READ', + timestamp: dayjs('2021-01-03'), + }, + ]; + + const route = { params: of({ participationId: '5' }) } as any as ActivatedRoute; + + function setupTestBed() { + fixture = TestBed.createComponent(VcsRepositoryAccessLogViewComponent); + programmingExerciseParticipationService = fixture.debugElement.injector.get(ProgrammingExerciseParticipationService); + repositoryVcsAccessLogSpy = jest.spyOn(programmingExerciseParticipationService, 'getVcsAccessLogForRepository').mockReturnValue(of(mockVcsAccessLog)); + participationVcsAccessLogSpy = jest.spyOn(programmingExerciseParticipationService, 'getVcsAccessLogForParticipation').mockReturnValue(of(mockVcsAccessLog)); + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VcsRepositoryAccessLogViewComponent], + providers: [ + { provide: ActivatedRoute, useValue: route }, + { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, + { provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }, + { provide: TranslateService, useClass: MockTranslateService }, + { provide: AlertService, useClass: MockAlertService }, + { provide: ProfileService, useClass: MockProfileService }, + ], + }).compileComponents(); + }); + + it('should load participation vcs access log', () => { + setupTestBed(); + fixture.detectChanges(); + + expect(participationVcsAccessLogSpy).toHaveBeenCalledOnce(); + }); + + it('should load template repository vcs access log', () => { + route.params = of({ exerciseId: '10', repositoryType: 'TEMPLATE' }); + TestBed.overrideProvider(ActivatedRoute, { useValue: route }); + + setupTestBed(); + fixture.detectChanges(); + + expect(repositoryVcsAccessLogSpy).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts index 8525ad43dc80..472de3e839b5 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts @@ -3,6 +3,7 @@ import { IProgrammingExerciseParticipationService } from 'app/exercises/programm import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { Result } from 'app/entities/result.model'; import { CommitInfo } from 'app/entities/programming/programming-submission.model'; +import { VcsAccessLogDTO } from 'app/entities/vcs-access-log-entry.model'; export class MockProgrammingExerciseParticipationService implements IProgrammingExerciseParticipationService { getLatestResultWithFeedback = (participationId: number, withSubmission: boolean) => of({} as Result); @@ -13,4 +14,6 @@ export class MockProgrammingExerciseParticipationService implements IProgramming getParticipationRepositoryFilesWithContentAtCommitForCommitDetailsView = (exerciseId: number, participationId: number, commitId: string, repositoryType: string) => of(new Map()); checkIfParticipationHasResult = (participationId: number) => of(true); + getVcsAccessLogForRepository = (exerciseId: number, repositoryType: string) => of([] as VcsAccessLogDTO[]); + getVcsAccessLogForParticipation = (participationId: number) => of([] as VcsAccessLogDTO[]); } diff --git a/src/test/javascript/spec/service/programming-exercise-participation.service.spec.ts b/src/test/javascript/spec/service/programming-exercise-participation.service.spec.ts index 92bff0034faa..1ef9b69501d1 100644 --- a/src/test/javascript/spec/service/programming-exercise-participation.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise-participation.service.spec.ts @@ -177,4 +177,21 @@ describe('ProgrammingExerciseParticipation Service', () => { req.flush(files); tick(); })); + + it('should make GET request to retrieve vcs access log for participation', fakeAsync(() => { + const participationId = 42; + service.getVcsAccessLogForParticipation(participationId).subscribe(); + const expectedURL = `${resourceUrlParticipations}${participationId}/vcs-access-log`; + httpMock.expectOne({ method: 'GET', url: expectedURL }); + tick(); + })); + + it('should make GET request to retrieve vcs access log for the template repository', fakeAsync(() => { + const exerciseId = 42; + const repositoryType = 'TEMPLATE'; + service.getVcsAccessLogForRepository(exerciseId, repositoryType).subscribe(); + const expectedURL = `${resourceUrl}${exerciseId}/vcs-access-log/${repositoryType}`; + httpMock.expectOne({ method: 'GET', url: expectedURL }); + tick(); + })); }); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f7d8bc06ea1b..742c904105e9 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -68,6 +68,8 @@ artemis: default: "~~invalid~~" rust: default: "~~invalid~~" + javascript: + default: "~~invalid~~" spring: application: diff --git a/supporting_scripts/course-scripts/quick-course-setup/requirements.txt b/supporting_scripts/course-scripts/quick-course-setup/requirements.txt index 55c9f479fae2..98b3cc9c6008 100644 --- a/supporting_scripts/course-scripts/quick-course-setup/requirements.txt +++ b/supporting_scripts/course-scripts/quick-course-setup/requirements.txt @@ -1,2 +1,2 @@ requests==2.32.3 -urllib3==2.2.2 +urllib3==2.2.3