├── .github ├── auto_assign.yml ├── check-md-links.json ├── dependabot.yml ├── labels.yml ├── linkspector.yml ├── release-drafter.yml └── workflows │ ├── assign-pr.yml │ ├── cd.yml │ ├── check-md-links.yml │ ├── codeql.yml │ ├── dogfood.yml │ ├── enforce-labels.yml │ ├── quality-monitor.yml │ ├── run-release-drafter.yml │ └── sync-labels.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── badges ├── branch-coverage.svg ├── bugs.svg ├── line-coverage.svg ├── mutation-coverage.svg ├── progress.svg └── style-warnings.svg ├── doc ├── components.puml ├── dependency-graph.puml └── deployment.puml ├── images ├── analysis-annotations.png ├── analysis.png ├── coverage-annotations.png ├── coverage.png ├── details.png ├── mutation-annotations.png ├── pr-comment.png └── tests.png ├── pom.xml └── src ├── main └── java │ └── edu │ └── hm │ └── hafner │ └── grading │ └── github │ ├── GitHubAnnotationsBuilder.java │ └── GitHubAutoGradingRunner.java └── test ├── java └── edu │ └── hm │ └── hafner │ └── grading │ └── github │ ├── GitHubAnnotationBuilderTest.java │ ├── GitHubAutoGradingRunnerDockerITest.java │ └── GitHubAutoGradingRunnerITest.java └── resources ├── checkstyle ├── checkstyle-ignores.xml └── checkstyle-result.xml ├── jacoco └── jacoco.xml ├── junit ├── TEST-Aufgabe3Test.xml ├── TEST-edu.hm.hafner.grading.AutoGradingActionTest.xml └── TEST-edu.hm.hafner.grading.ReportFinderTest.xml ├── pit └── mutations.xml ├── pmd ├── pmd-ignores.xml └── pmd.xml └── spotbugs └── spotbugsXml.xml /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: false 2 | addAssignees: true 3 | 4 | assignees: 5 | - uhafner 6 | 7 | skipKeywords: 8 | - wip 9 | 10 | numberOfAssignees: 0 11 | -------------------------------------------------------------------------------- /.github/check-md-links.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpHeaders": [ 3 | { 4 | "urls": ["https://github.com/", "https://guides.github.com/", "https://help.github.com/", "https://docs.github.com/", "https://classroom.github.com"], 5 | "headers": { 6 | "Accept-Encoding": "zstd, br, gzip, deflate" 7 | } 8 | } 9 | ], 10 | "aliveStatusCodes": [200, 500, 503, 429] 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "" 10 | ignore: 11 | - dependency-name: org.eclipse.collections:eclipse-collections 12 | versions: 13 | - ">= 10.a" 14 | - dependency-name: org.eclipse.collections:eclipse-collections-api 15 | versions: 16 | - ">= 10.a" 17 | - dependency-name: net.javacrumbs.json-unit:json-unit-assertj 18 | versions: 19 | - ">= 3.0.0" 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | commit-message: 24 | prefix: "" 25 | schedule: 26 | interval: "daily" 27 | 28 | - package-ecosystem: "npm" 29 | directory: "/" 30 | commit-message: 31 | prefix: "" 32 | schedule: 33 | interval: "daily" 34 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: bug 2 | description: Bugs or performance problems 3 | color: CC0000 4 | - name: dependencies 5 | description: Update of dependencies 6 | color: 0366d6 7 | - name: feature 8 | color: a4c6fb 9 | description: New features 10 | - name: enhancement 11 | description: Enhancement of existing functionality 12 | color: 94a6eb 13 | - name: deprecated 14 | description: Deprecating API 15 | color: f4c21d 16 | - name: removed 17 | description: Removing API 18 | color: e4b21d 19 | - name: tests 20 | description: Enhancement of tests 21 | color: 30cc62 22 | - name: documentation 23 | description: Enhancement of documentation 24 | color: bfafea 25 | - name: internal 26 | description: Internal changes without user or API impact 27 | color: e6e6e6 28 | -------------------------------------------------------------------------------- /.github/linkspector.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - README.md 3 | dirs: 4 | - ./ 5 | - doc 6 | useGitIgnore: true 7 | ignorePatterns: 8 | - pattern: '^http://localhost:.*$' 9 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🎁' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | template: | 5 | $CHANGES 6 | 7 | # Emoji reference: https://gitmoji.carloscuesta.me/ 8 | categories: 9 | - title: 💥 Removed 10 | label: removed 11 | - title: ⚠️ Deprecated 12 | label: deprecated 13 | - title: 🚀 New features 14 | labels: 15 | - feature 16 | - title: ✨ Improvements 17 | labels: 18 | - enhancement 19 | - title: 🐛 Bug Fixes 20 | labels: 21 | - bug 22 | - fix 23 | - bugfix 24 | - regression 25 | - title: 📝 Documentation updates 26 | label: documentation 27 | - title: 📦 Dependency updates 28 | label: dependencies 29 | - title: 🔧 Internal changes 30 | label: internal 31 | - title: 🚦 Tests 32 | labels: 33 | - test 34 | - tests 35 | 36 | version-resolver: 37 | major: 38 | labels: 39 | - 'removed' 40 | minor: 41 | labels: 42 | - 'feature' 43 | - 'enhancement' 44 | - 'deprecated' 45 | patch: 46 | labels: 47 | - 'dependencies' 48 | - 'documentation' 49 | - 'tests' 50 | - 'internal' 51 | default: minor 52 | -------------------------------------------------------------------------------- /.github/workflows/assign-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign PR' 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | assign-pr: 7 | name: 'Auto Assign PR' 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v2.0.0 11 | with: 12 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 13 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build and deploy to Docker hub 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up JDK 21 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'temurin' 18 | java-version: '21' 19 | check-latest: true 20 | cache: 'maven' 21 | - name: Set up Maven 22 | uses: stCarolas/setup-maven@v5 23 | with: 24 | maven-version: 3.9.9 25 | - name: Build and deploy 26 | env: 27 | DOCKER_IO_USERNAME: ${{ secrets.DOCKER_IO_USERNAME }} 28 | DOCKER_IO_PASSWORD: ${{ secrets.DOCKER_IO_PASSWORD }} 29 | run: mvn -ntp clean install -Pci 30 | -------------------------------------------------------------------------------- /.github/workflows/check-md-links.yml: -------------------------------------------------------------------------------- 1 | name: 'Link Checker' 2 | 3 | on: push 4 | 5 | jobs: 6 | check-markdown-links: 7 | name: 'Check Markdown links' 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: umbrelladocs/action-linkspector@v1.3.4 12 | with: 13 | github_token: ${{ secrets.github_token }} 14 | reporter: github-pr-check 15 | fail_on_error: true 16 | filter_mode: nofilter 17 | config_file: '.github/linkspector.yml' 18 | level: 'info' 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: "32 3 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze code 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ java ] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Setup Java 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 21 33 | cache: maven 34 | 35 | - name: Set up Maven 36 | uses: stCarolas/setup-maven@v5 37 | with: 38 | maven-version: 3.9.9 39 | 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | queries: +security-and-quality 45 | 46 | - name: Build with Maven 47 | run: mvn -V --color always -ntp clean verify -Pskip 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v3 51 | with: 52 | upload: false 53 | output: sarif-results 54 | category: "/language:${{ matrix.language }}" 55 | 56 | - name: Filter SARIF results 57 | uses: advanced-security/filter-sarif@v1 58 | with: 59 | patterns: | 60 | -**/*Assert* 61 | input: sarif-results/${{ matrix.language }}.sarif 62 | output: sarif-results/${{ matrix.language }}.sarif 63 | 64 | - name: Upload SARIF results 65 | uses: github/codeql-action/upload-sarif@v3 66 | with: 67 | sarif_file: sarif-results/${{ matrix.language }}.sarif 68 | -------------------------------------------------------------------------------- /.github/workflows/dogfood.yml: -------------------------------------------------------------------------------- 1 | name: Eat your own dog food 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CD"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | grade-test-data: 11 | name: Run autograding with test data 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up JDK 21 16 | uses: actions/setup-java@v4 17 | with: 18 | distribution: 'temurin' 19 | java-version: '21' 20 | check-latest: true 21 | cache: 'maven' 22 | - name: Set up Maven 23 | uses: stCarolas/setup-maven@v5 24 | with: 25 | maven-version: 3.9.9 26 | - name: Run Autograding with test data 27 | uses: uhafner/autograding-github-action@main 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | checks-name: "Autograding with Test Data" 31 | config: > 32 | { 33 | "tests": { 34 | "tools": [ 35 | { 36 | "id": "junit", 37 | "pattern": "**/src/test/resources/**/TEST*.xml" 38 | } 39 | ], 40 | "name": "Tests", 41 | "failureRateImpact": -1, 42 | "maxScore": 100 43 | }, 44 | "analysis": [ 45 | { 46 | "name": "Style", 47 | "id": "style", 48 | "tools": [ 49 | { 50 | "id": "checkstyle", 51 | "pattern": "**/src/test/resources/**/checkstyle*.xml" 52 | }, 53 | { 54 | "id": "pmd", 55 | "name": "PMD", 56 | "pattern": "**/src/test/resources/**/pmd*.xml" 57 | } 58 | ], 59 | "errorImpact": 1, 60 | "highImpact": 2, 61 | "normalImpact": 3, 62 | "lowImpact": 4, 63 | "maxScore": 100 64 | }, 65 | { 66 | "name": "Bugs", 67 | "id": "bugs", 68 | "icon": "bug", 69 | "tools": [ 70 | { 71 | "id": "spotbugs", 72 | "pattern": "**/src/test/resources/**/spotbugs*.xml" 73 | } 74 | ], 75 | "errorImpact": -11, 76 | "highImpact": -12, 77 | "normalImpact": -13, 78 | "lowImpact": -14, 79 | "maxScore": 100 80 | } 81 | ], 82 | "coverage": [ 83 | { 84 | "tools": [ 85 | { 86 | "id": "jacoco", 87 | "metric": "line", 88 | "pattern": "**/src/test/resources/**/jacoco.xml" 89 | }, 90 | { 91 | "id": "jacoco", 92 | "metric": "branch", 93 | "pattern": "**/src/test/resources/**/jacoco.xml" 94 | } 95 | ], 96 | "name": "JaCoCo", 97 | "maxScore": 100, 98 | "coveredPercentageImpact": 1, 99 | "missedPercentageImpact": -1 100 | }, 101 | { 102 | "tools": [ 103 | { 104 | "id": "pit", 105 | "metric": "mutation", 106 | "pattern": "**/src/test/resources/**/mutations.xml" 107 | } 108 | ], 109 | "name": "PIT", 110 | "maxScore": 100, 111 | "coveredPercentageImpact": 1, 112 | "missedPercentageImpact": -1 113 | } 114 | ] 115 | } 116 | grade-github-action: 117 | name: Run autograding for the GitHub Autograding Action 118 | runs-on: ubuntu-latest 119 | steps: 120 | - uses: actions/checkout@v4 121 | - name: Set up JDK 21 122 | uses: actions/setup-java@v4 123 | with: 124 | distribution: 'temurin' 125 | java-version: '21' 126 | check-latest: true 127 | cache: 'maven' 128 | - name: Set up Maven 129 | uses: stCarolas/setup-maven@v5 130 | with: 131 | maven-version: 3.9.9 132 | - name: Build 133 | run: mvn -ntp clean verify -Pci -Ppit -Pdepgraph 134 | - name: Run Autograding 135 | uses: uhafner/autograding-github-action@main 136 | with: 137 | github-token: ${{ secrets.GITHUB_TOKEN }} 138 | checks-name: "Autograding GitHub Action" 139 | config: | 140 | { 141 | "tests": { 142 | "name": "Tests", 143 | "id": "tests", 144 | "tools": [ 145 | { 146 | "id": "junit", 147 | "name": "JUnit Tests", 148 | "pattern": "**/target/*-reports/TEST*.xml" 149 | } 150 | ], 151 | "failureRateImpact": -1, 152 | "maxScore": 100 153 | }, 154 | "analysis": [ 155 | { 156 | "name": "Style", 157 | "id": "style", 158 | "tools": [ 159 | { 160 | "id": "checkstyle", 161 | "pattern": "**/target/**checkstyle-result.xml" 162 | }, 163 | { 164 | "id": "pmd", 165 | "pattern": "**/target/**pmd.xml" 166 | } 167 | ], 168 | "errorImpact": -1, 169 | "highImpact": -1, 170 | "normalImpact": -1, 171 | "lowImpact": -1, 172 | "maxScore": 100 173 | }, 174 | { 175 | "name": "Bugs", 176 | "id": "bugs", 177 | "icon": "bug", 178 | "tools": [ 179 | { 180 | "id": "spotbugs", 181 | "sourcePath": "src/main/java", 182 | "pattern": "**/target/spotbugsXml.xml" 183 | } 184 | ], 185 | "errorImpact": -3, 186 | "highImpact": -3, 187 | "normalImpact": -3, 188 | "lowImpact": -3, 189 | "maxScore": 100 190 | } 191 | ], 192 | "coverage": [ 193 | { 194 | "name": "Code Coverage", 195 | "tools": [ 196 | { 197 | "id": "jacoco", 198 | "metric": "line", 199 | "sourcePath": "src/main/java", 200 | "pattern": "**/target/site/jacoco/jacoco.xml" 201 | }, 202 | { 203 | "id": "jacoco", 204 | "metric": "branch", 205 | "sourcePath": "src/main/java", 206 | "pattern": "**/target/site/jacoco/jacoco.xml" 207 | } 208 | ], 209 | "maxScore": 100, 210 | "missedPercentageImpact": -1 211 | }, 212 | { 213 | "name": "Mutation Coverage", 214 | "tools": [ 215 | { 216 | "id": "pit", 217 | "metric": "mutation", 218 | "sourcePath": "src/main/java", 219 | "pattern": "**/target/pit-reports/mutations.xml" 220 | }, 221 | { 222 | "id": "pit", 223 | "metric": "test-strength", 224 | "sourcePath": "src/main/java", 225 | "pattern": "**/target/pit-reports/mutations.xml" 226 | } 227 | ], 228 | "maxScore": 100, 229 | "missedPercentageImpact": -1 230 | } 231 | ] 232 | } 233 | - name: Write metrics to GitHub output 234 | id: metrics 235 | run: | 236 | cat metrics.env >> "${GITHUB_OUTPUT}" 237 | mkdir -p badges 238 | - name: Generate the badge SVG image for the line coverage 239 | uses: emibcn/badge-action@v2.0.3 240 | with: 241 | label: 'Lines' 242 | status: ${{ steps.metrics.outputs.line }}% 243 | color: 'green' 244 | path: badges/line-coverage.svg 245 | - name: Generate the badge SVG image for the branch coverage 246 | uses: emibcn/badge-action@v2.0.3 247 | with: 248 | label: 'Branches' 249 | status: ${{ steps.metrics.outputs.branch }}% 250 | color: 'green' 251 | path: badges/branch-coverage.svg 252 | - name: Generate the badge SVG image for the mutation coverage 253 | uses: emibcn/badge-action@v2.0.3 254 | with: 255 | label: 'Mutations' 256 | status: ${{ steps.metrics.outputs.mutation }}% 257 | color: 'green' 258 | path: badges/mutation-coverage.svg 259 | - name: Generate the badge SVG image for the style warnings 260 | uses: emibcn/badge-action@v2.0.3 261 | with: 262 | label: 'Warnings' 263 | status: ${{ steps.metrics.outputs.style }} 264 | color: 'orange' 265 | path: badges/style-warnings.svg 266 | - name: Generate the badge SVG image for the potential bugs 267 | uses: emibcn/badge-action@v2.0.3 268 | with: 269 | label: 'Bugs' 270 | status: ${{ steps.metrics.outputs.bugs }} 271 | color: 'red' 272 | path: badges/bugs.svg 273 | - name: Commit updated badges 274 | continue-on-error: true 275 | run: | 276 | git config --local user.email "action@github.com" 277 | git config --local user.name "GitHub Action" 278 | git add badges/*.svg 279 | git commit -m "Update badges with results from latest autograding" || true 280 | git add doc/dependency-graph.puml 281 | git commit -m "Update dependency graph to latest versions from POM" || true 282 | - name: Push updated badges to GitHub repository 283 | uses: ad-m/github-push-action@master 284 | if: ${{ success() }} 285 | with: 286 | github_token: ${{ secrets.GITHUB_TOKEN }} 287 | branch: main 288 | -------------------------------------------------------------------------------- /.github/workflows/enforce-labels.yml: -------------------------------------------------------------------------------- 1 | name: Label Checker 2 | on: 3 | pull_request_target: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | jobs: 6 | enforce-labels: 7 | name: 'Enforce PR labels' 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: mheap/github-action-required-labels@v5 14 | with: 15 | mode: minimum 16 | count: 1 17 | labels: "bug,feature,enhancement,breaking,tests,documentation,internal,dependencies" 18 | message: "Maintainer needs to assign at least one label before merge" 19 | -------------------------------------------------------------------------------- /.github/workflows/quality-monitor.yml: -------------------------------------------------------------------------------- 1 | name: 'Quality Monitor PR' 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: [ubuntu-latest] 10 | name: Build, test and monitor quality on Ubuntu 11 | 12 | steps: 13 | - name: 'Checkout merge commit' 14 | uses: actions/checkout@v4 15 | with: 16 | ref: "${{ github.event.pull_request.merge_commit_sha }}" 17 | if: github.event.pull_request.merge_commit_sha != '' 18 | - name: 'Checkout PR head commit' 19 | uses: actions/checkout@v4 20 | with: 21 | ref: "${{ github.event.pull_request.head.sha }}" 22 | if: github.event.pull_request.merge_commit_sha == '' 23 | - name: Set up JDK 21 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'temurin' 27 | java-version: 21 28 | check-latest: true 29 | cache: 'maven' 30 | - name: Set up Maven 31 | uses: stCarolas/setup-maven@v5 32 | with: 33 | maven-version: 3.9.9 34 | - name: Cache the NVD database 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/.m2/repository/org/owasp/dependency-check-data 38 | key: dependency-check 39 | - name: Build with Maven 40 | env: 41 | BROWSER: chrome-container 42 | NVD_API_KEY: ${{ secrets.NVD_API_KEY }} 43 | run: mvn -V --color always -ntp clean verify -Pci -Powasp | tee maven.log 44 | - name: Extract pull request number 45 | uses: jwalton/gh-find-current-pr@v1 46 | id: pr 47 | - name: Run Quality Monitor 48 | uses: uhafner/quality-monitor@v2 49 | with: 50 | pr-number: ${{ steps.pr.outputs.number }} 51 | show-headers: true 52 | config: > 53 | { 54 | "tests": { 55 | "name": "Tests", 56 | "tools": [ 57 | { 58 | "id": "junit", 59 | "name": "JUnit Tests", 60 | "pattern": "**/target/*-reports/TEST*.xml" 61 | } 62 | ] 63 | }, 64 | "analysis": [ 65 | { 66 | "name": "Style", 67 | "id": "style", 68 | "tools": [ 69 | { 70 | "id": "checkstyle", 71 | "pattern": "**/target/checkstyle-*/checkstyle-result.xml" 72 | }, 73 | { 74 | "id": "pmd", 75 | "pattern": "**/target/pmd-*/pmd.xml" 76 | } 77 | ] 78 | }, 79 | { 80 | "name": "Bugs", 81 | "id": "bugs", 82 | "icon": "bug", 83 | "tools": [ 84 | { 85 | "id": "spotbugs", 86 | "sourcePath": "src/main/java", 87 | "pattern": "**/target/spotbugsXml.xml" 88 | }, 89 | { 90 | "id": "error-prone", 91 | "pattern": "**/maven.log" 92 | } 93 | ] 94 | }, 95 | { 96 | "name": "Vulnerabilities", 97 | "id": "vulnerabilities", 98 | "icon": "shield", 99 | "tools": [ 100 | { 101 | "id": "owasp-dependency-check", 102 | "pattern": "**/target/dependency-check-report.json" 103 | } 104 | ] 105 | } 106 | ], 107 | "coverage": [ 108 | { 109 | "name": "Code Coverage", 110 | "tools": [ 111 | { 112 | "id": "jacoco", 113 | "metric": "line", 114 | "sourcePath": "src/main/java", 115 | "pattern": "**/target/site/jacoco/jacoco.xml" 116 | }, 117 | { 118 | "id": "jacoco", 119 | "metric": "branch", 120 | "sourcePath": "src/main/java", 121 | "pattern": "**/target/site/jacoco/jacoco.xml" 122 | } 123 | ] 124 | } 125 | ], 126 | "metrics": 127 | { 128 | "name": "Software Metrics", 129 | "tools": [ 130 | { 131 | "id": "metrics", 132 | "pattern": "**/metrics/pmd.xml", 133 | "metric": "CYCLOMATIC_COMPLEXITY" 134 | }, 135 | { 136 | "id": "metrics", 137 | "pattern": "**/metrics/pmd.xml", 138 | "metric": "COGNITIVE_COMPLEXITY" 139 | }, 140 | { 141 | "id": "metrics", 142 | "pattern": "**/metrics/pmd.xml", 143 | "metric": "NPATH_COMPLEXITY" 144 | }, 145 | { 146 | "id": "metrics", 147 | "pattern": "**/metrics/pmd.xml", 148 | "metric": "LOC" 149 | }, 150 | { 151 | "id": "metrics", 152 | "pattern": "**/metrics/pmd.xml", 153 | "metric": "NCSS" 154 | }, 155 | { 156 | "id": "metrics", 157 | "pattern": "**/metrics/pmd.xml", 158 | "metric": "COHESION" 159 | }, 160 | { 161 | "id": "metrics", 162 | "pattern": "**/metrics/pmd.xml", 163 | "metric": "WEIGHT_OF_CLASS" 164 | } 165 | ] 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /.github/workflows/run-release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Drafter' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | update-release-draft: 11 | name: 'Update Release Draft' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: release-drafter/release-drafter@v6.1.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | paths: 8 | - .github/labels.yml 9 | - .github/workflows/sync-labels.yml 10 | 11 | jobs: 12 | sync-labels: 13 | name: Sync labels 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: micnncim/action-label-syncer@v1.3.0 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | manifest: .github/labels.yml 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node 2 | node_modules 3 | target 4 | *.iml 5 | .classpath 6 | .project 7 | .settings 8 | pom.xml.versionsBackup 9 | pom.xml.releaseBackup 10 | release.properties 11 | .DS_Store 12 | .idea 13 | /package-lock.json 14 | metrics.env 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Tobias Effner, Dr. Ullrich Hafner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autograding GitHub Action 2 | 3 | [![GitHub Actions](https://github.com/uhafner/autograding-github-action/workflows/CD/badge.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/cd.yml) 4 | [![CodeQL](https://github.com/uhafner/autograding-github-action/workflows/CodeQL/badge.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/codeql.yml) 5 | [![Line Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/line-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 6 | [![Branch Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/branch-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 7 | [![Mutation Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/mutation-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 8 | [![Style Warnings](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/style-warnings.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 9 | [![Potential Bugs](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/bugs.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 10 | 11 | This GitHub action autogrades projects based on a configurable set of metrics and gives feedback on pull requests (or single commits) in GitHub. I use this action to automatically grade student projects in my lectures at the Munich University of Applied Sciences. 12 | 13 | You can see the results of this action in two fake pull requests: 14 | - [perfect solution](https://github.com/uhafner/java2-assignment1/pull/57) and the associated [GitHub Checks output](https://github.com/uhafner/java2-assignment1/runs/39605686819). 15 | - [solution with errors](https://github.com/uhafner/java2-assignment1/pull/58) and the associated [GitHub Checks output](https://github.com/uhafner/java2-assignment1/runs/39606355912) 16 | 17 | ![Pull request comment](images/pr-comment.png) 18 | 19 | Please have a look at my [companion coding style](https://github.com/uhafner/codingstyle) and [Maven parent POM](https://github.com/uhafner/codingstyle-pom) to see how to create Java projects that can be graded using this GitHub action. If you want to use this action to visualize the coding quality without creating a grading percentage, then use my additional [Quality Monitor GitHub Action](https://github.com/uhafner/quality-monitor). If you are hosting your project on GitLab, then you might be interested in my [identical GitLab action](https://github.com/uhafner/autograding-gitlab-action) as well. 20 | 21 | Both actions are inspired by my Jenkins plugins: 22 | - [Jenkins Warnings plugin](https://plugins.jenkins.io/warnings-ng/) 23 | - [Jenkins Coverage plugin](https://plugins.jenkins.io/coverage) 24 | - [Jenkins Autograding plugin](https://plugins.jenkins.io/autograding) 25 | 26 | They work in the same way but are much more powerful and flexible and show the results additionally in Jenkins' UI. 27 | 28 | Please note that the action works on report files that are generated by other tools. It does not run the tests or static analysis tools itself. You need to run these tools in a previous step of your workflow. See the example below for details. This has the advantage that you can use a tooling you are already familiar with. So the action will run for any programming language that can generate the required report files. There are already more than [one hundred analysis formats](https://github.com/jenkinsci/analysis-model/blob/main/SUPPORTED-FORMATS.md) supported. Code and mutation coverage reports can use the JaCoCo, Cobertura, OpenCover and PIT formats, see the [coverage model](https://github.com/jenkinsci/coverage-model) for details. Test results can be provided in the JUnit, XUnit, or NUnit XML-formats. 29 | 30 | # GitHub Checks 31 | 32 | The details output of the action is shown in the GitHub Checks tab of the pull request: 33 | 34 | ![GitHub checks result](images/details.png) 35 | 36 | # Configuration 37 | 38 | The autograding action must be added as a separate step of your GitHub pipeline since it is packaged in a Docker container. This step should run after your normal build and testing steps so that it has access to all produced artifacts. Make sure to configure your build to produce the required report files (e.g., JUnit XML reports, JaCoCo XML reports, etc.) even when there are test failures or warnings found. Otherwise, the action will only show partial results. 39 | 40 | ```yaml 41 | name: Autograde project 42 | 43 | on: 44 | push 45 | 46 | jobs: 47 | grade-project: 48 | name: Autograde project 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up JDK 21 53 | uses: actions/setup-java@v3 54 | with: 55 | distribution: 'temurin' 56 | java-version: '21' 57 | check-latest: true 58 | cache: 'maven' 59 | - name: Set up Maven 60 | uses: stCarolas/setup-maven@v5 61 | with: 62 | maven-version: 3.9.6 63 | - name: Build # (compile, test with code and mutation coverage, and run static analysis) 64 | run: mvn -V --color always -ntp clean verify -Ppit -Dmaven.test.failure.ignore=true 65 | - name: Extract pull request number # (commenting on the pull request requires the PR number) 66 | uses: jwalton/gh-find-current-pr@v1 67 | id: pr 68 | - name: Run Autograding 69 | uses: uhafner/autograding-github-action@v3 70 | with: 71 | github-token: ${{ secrets.GITHUB_TOKEN }} 72 | pr-number: ${{ steps.pr.outputs.number }} 73 | checks-name: "Autograding GitHub Action" 74 | config: > # Override default configuration: just grade the test results 75 | { 76 | "tests": { 77 | "tools": [ 78 | { 79 | "id": "test", 80 | "name": "Unittests", 81 | "pattern": "**/target/*-reports/TEST*.xml" 82 | } 83 | ], 84 | "name": "JUnit", 85 | "skippedImpact": -1, 86 | "failureImpact": -5, 87 | "maxScore": 100 88 | } 89 | } 90 | ``` 91 | 92 | ## Action Parameters 93 | 94 | This action can be configured using the following parameters (see example above): 95 | - ``github-token: ${{ secrets.GITHUB_TOKEN }}``: mandatory GitHub access token. 96 | - ``config: "{...}"``: optional configuration, see sections above for details, or consult the [autograding-model](https://github.com/uhafner/autograding-model) project for the exact implementation. If not specified, a [default configuration](https://github.com/uhafner/autograding-model/blob/main/src/main/resources/default-config.json) will be used. 97 | - ``pr-number: ${{ steps.pr.outputs.number }}``: optional number of the pull request. If not set, then just the checks will be published but not a pull request comment. 98 | - ``checks-name: "Name of checks"``: optional name of GitHub checks (overwrites the default: "Autograding result"). 99 | - ``skip-annotations: true``: Optional flag to skip the creation of annotations (for warnings and missed coverage). 100 | - ``max-warning-comments: ``: Optional parameter to limit the number of warning comments at specific lines. By default, all line comments are created. 101 | - ``max-coverage-comments: ``: Optional parameter to limit the number of coverage comments at specific lines. By default, all line comments are created. 102 | 103 | ## Metrics Configuration 104 | 105 | The individual metrics can be configured by defining an appropriate `config` property (in JSON format) in your GitHub workflow. Currently, you can select from the metrics shown in the following sections. Each metric can be configured individually. All of these configurations are composed in the same way: you can define a list of tools that are used to collect the data, a name and icon for the metric, and a maximum score. All tools need to provide a pattern where the autograding action can find the result files in the workspace (e.g., JUnit XML reports). Additionally, each tool needs to provide the parser ID of the tool so that the underlying model can find the correct parser to read the results. See [analysis model](https://github.com/jenkinsci/analysis-model) and [coverage model](https://github.com/jenkinsci/coverage-model) for the list of supported parsers. 106 | 107 | Additionally, you can define the impact of each result (e.g., a failed test, a missed line in coverage) on the final score. The impact is a positive or negative number and will be multiplied with the actual value of the measured items during the evaluation. Negative values will be subtracted from the maximum score to compute the final score. Positive values will be directly used as the final score. You can choose the type of impact that matches your needs best. 108 | 109 | ## Test statistics (e.g., number of failed tests) 110 | 111 | ![Test statistics](images/tests.png) 112 | 113 | This metric can be configured using a JSON object `tests`, see the example below for details: 114 | 115 | ```json 116 | { 117 | "tests": { 118 | "tools": [ 119 | { 120 | "id": "test", 121 | "name": "Unittests", 122 | "pattern": "**/junit*.xml" 123 | } 124 | ], 125 | "name": "JUnit", 126 | "passedImpact": 10, 127 | "skippedImpact": -1, 128 | "failureImpact": -5, 129 | "maxScore": 100 130 | } 131 | } 132 | ``` 133 | 134 | You can either count passed tests as positive impact or failed tests as negative impact (or use a mix of both). Skipped tests will be listed individually. For failed tests, the test error message and stack trace will be shown directly after the summary in the pull request. 135 | 136 | ## Code or mutation coverage (e.g., line coverage percentage) 137 | 138 | ![Code coverage summary](images/coverage.png) 139 | 140 | This metric can be configured using a JSON object `coverage`, see the example below for details: 141 | 142 | ```json 143 | { 144 | "coverage": [ 145 | { 146 | "tools": [ 147 | { 148 | "id": "jacoco", 149 | "name": "Line Coverage", 150 | "metric": "line", 151 | "sourcePath": "src/main/java", 152 | "pattern": "**/jacoco.xml" 153 | }, 154 | { 155 | "id": "jacoco", 156 | "name": "Branch Coverage", 157 | "metric": "branch", 158 | "sourcePath": "src/main/java", 159 | "pattern": "**/jacoco.xml" 160 | } 161 | ], 162 | "name": "JaCoCo", 163 | "maxScore": 100, 164 | "coveredPercentageImpact": 1, 165 | "missedPercentageImpact": -1 166 | }, 167 | { 168 | "tools": [ 169 | { 170 | "id": "pit", 171 | "name": "Mutation Coverage", 172 | "metric": "mutation", 173 | "sourcePath": "src/main/java", 174 | "pattern": "**/mutations.xml" 175 | } 176 | ], 177 | "name": "PIT", 178 | "maxScore": 100, 179 | "coveredPercentageImpact": 1, 180 | "missedPercentageImpact": 0 181 | } 182 | ] 183 | } 184 | ``` 185 | 186 | You can either use the covered percentage as positive impact or the missed percentage as negative impact (a mix of both makes little sense but would work as well). Please make sure to define exactly a unique and supported metric for each tool. For example, JaCoCo provides `line` and `branch` coverage, so you need to define two tools for JaCoCo. PIT provides mutation coverage, so you need to define a tool for PIT that uses the metric `mutation`. 187 | 188 | Missed lines or branches as well as survived mutations will be shown as annotations in the pull request: 189 | 190 | ![Code coverage annotations](images/coverage-annotations.png) 191 | ![Mutation coverage annotations](images/mutation-annotations.png) 192 | 193 | 194 | ## Static analysis (e.g., number of warnings) 195 | 196 | ![Static analysis](images/analysis.png) 197 | 198 | This metric can be configured using a JSON object `analysis`, see the example below for details: 199 | 200 | ```json 201 | { 202 | "analysis": [ 203 | { 204 | "name": "Style", 205 | "id": "style", 206 | "tools": [ 207 | { 208 | "id": "checkstyle", 209 | "name": "CheckStyle", 210 | "pattern": "**/target/checkstyle-result.xml" 211 | }, 212 | { 213 | "id": "pmd", 214 | "name": "PMD", 215 | "pattern": "**/target/pmd.xml" 216 | } 217 | ], 218 | "errorImpact": 1, 219 | "highImpact": 2, 220 | "normalImpact": 3, 221 | "lowImpact": 4, 222 | "maxScore": 100 223 | }, 224 | { 225 | "name": "Bugs", 226 | "id": "bugs", 227 | "icon": "bug", 228 | "tools": [ 229 | { 230 | "id": "spotbugs", 231 | "name": "SpotBugs", 232 | "sourcePath": "src/main/java", 233 | "pattern": "**/target/spotbugsXml.xml" 234 | } 235 | ], 236 | "errorImpact": -11, 237 | "highImpact": -12, 238 | "normalImpact": -13, 239 | "lowImpact": -14, 240 | "maxScore": 100 241 | } 242 | ] 243 | } 244 | ``` 245 | 246 | Normally, you would only use a negative impact for this metric: each warning (of a given severity) will reduce the final score by the specified amount. You can define the impact of each severity level individually. 247 | 248 | All warnings will be shown as annotations in the pull request: 249 | 250 | ![Warning annotations](images/analysis-annotations.png ) 251 | 252 | ## Pull Request Comments 253 | 254 | The action writes a summary of the results to the pull request as well. Since the action cannot identify the correct pull request on its own, you need to provide the pull request as an action argument. 255 | 256 | ```yaml 257 | [... ] 258 | 259 | - name: Extract pull request number 260 | uses: jwalton/gh-find-current-pr@v1 261 | id: pr 262 | - name: Run Autograding 263 | uses: uhafner/autograding-github-action@v3 264 | with: 265 | github-token: ${{ secrets.GITHUB_TOKEN }} 266 | pr-number: ${{ steps.pr.outputs.number }} 267 | checks-name: "Autograding GitHub Action" 268 | config: {...} 269 | 270 | [... ] 271 | ``` 272 | 273 | Configuring the action in this way will produce an additional comment of the form: 274 | 275 | ![Pull request comment](images/pr-comment.png) 276 | 277 | ## Automatic Badge Creation 278 | 279 | [![Line Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/line-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 280 | [![Branch Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/branch-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 281 | [![Mutation Coverage](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/mutation-coverage.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 282 | [![Style Warnings](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/style-warnings.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 283 | [![Potential Bugs](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/badges/bugs.svg)](https://github.com/uhafner/autograding-github-action/actions/workflows/dogfood.yml) 284 | 285 | 286 | The results of the action can be used to create various badges that show the current status of the project. The action writes the results of the action to a file called `metrics.env` in the workspace. This file can be used to create badges using the [GitHub Badge Action](https://github.com/marketplace/actions/badge-action). The following snippet shows how to create several badges for your project, the full example is visible in [my autograding workflow](https://raw.githubusercontent.com/uhafner/autograding-github-action/main/.github/workflows/dogfood.yml). 287 | 288 | ```yaml 289 | [... Autograding, see above ... ] 290 | - name: Write metrics to GitHub output 291 | id: metrics 292 | run: | 293 | cat metrics.env >> "${GITHUB_OUTPUT}" 294 | mkdir -p badges 295 | - name: Generate the badge SVG image for the line coverage 296 | uses: emibcn/badge-action@v2.0.2 297 | with: 298 | label: 'Line coverage' 299 | status: ${{ steps.metrics.outputs.line }}% 300 | color: 'green' 301 | path: badges/line-coverage.svg 302 | - name: Generate the badge SVG image for the branch coverage 303 | uses: emibcn/badge-action@v2.0.2 304 | with: 305 | label: 'Branch coverage' 306 | status: ${{ steps.metrics.outputs.branch }}% 307 | color: 'green' 308 | path: badges/branch-coverage.svg 309 | - name: Generate the badge SVG image for the mutation coverage 310 | uses: emibcn/badge-action@v2.0.2 311 | with: 312 | label: 'Mutation coverage' 313 | status: ${{ steps.metrics.outputs.mutation }}% 314 | color: 'green' 315 | path: badges/mutation-coverage.svg 316 | - name: Generate the badge SVG image for the style warnings 317 | uses: emibcn/badge-action@v2.0.2 318 | with: 319 | label: 'Style warnings' 320 | status: ${{ steps.metrics.outputs.style }} 321 | color: 'orange' 322 | path: badges/style-warnings.svg 323 | - name: Generate the badge SVG image for the potential bugs 324 | uses: emibcn/badge-action@v2.0.2 325 | with: 326 | label: 'Potential Bugs' 327 | status: ${{ steps.metrics.outputs.bugs }} 328 | color: 'red' 329 | path: badges/bugs.svg 330 | - name: Commit updated badges 331 | continue-on-error: true 332 | run: | 333 | git config --local user.email "action@github.com" 334 | git config --local user.name "GitHub Action" 335 | git add badges/*.svg 336 | git commit -m "Update badges with results from latest autograding" || true 337 | - name: Push updated badges to GitHub repository 338 | uses: ad-m/github-push-action@master 339 | if: ${{ success() }} 340 | with: 341 | github_token: ${{ secrets.GITHUB_TOKEN }} 342 | branch: main 343 | ``` 344 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Autograding Action 2 | description: Action that autogrades assignments based on configurable quality criteria. 3 | 4 | inputs: 5 | config: 6 | description: "Autograding configuration (if not set the default configuration will be used)" 7 | required: false 8 | checks-name: 9 | description: "Name of the GitHub checks (if not set the default name will be used)" 10 | required: false 11 | pr-number: 12 | description: "Pull request number (if not set, PR comments will be skipped)" 13 | required: false 14 | github-token: 15 | description: "GitHub authentication token (GITHUB_TOKEN)" 16 | required: true 17 | skip-annotations: 18 | description: "Skip the creation of annotations (for warnings and missed coverage) if not empty" 19 | required: false 20 | max-warning-annotations: 21 | description: "Limit the number of warning annotations at specific lines. By default, all annotations are created." 22 | required: false 23 | max-coverage-annotations: 24 | description: "Limit the number of coverage annotations at specific lines. By default, all annotations are created." 25 | required: false 26 | 27 | runs: 28 | using: 'docker' 29 | image: 'docker://uhafner/autograding-github-action:5.3.0-SNAPSHOT' 30 | env: 31 | CONFIG: ${{ inputs.config }} 32 | CHECKS_NAME: ${{ inputs.checks-name }} 33 | PR_NUMBER: ${{ inputs.pr-number }} 34 | GITHUB_TOKEN: ${{ inputs.github-token }} 35 | SKIP_ANNOTATIONS: ${{ inputs.skip-annotations }} 36 | MAX_WARNING_ANNOTATIONS: ${{ inputs.max-warning-annotations }} 37 | MAX_COVERAGE_ANNOTATIONS: ${{ inputs.max-coverage-annotations }} 38 | 39 | branding: 40 | icon: check-square 41 | color: green 42 | -------------------------------------------------------------------------------- /badges/branch-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Branches: 100% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/bugs.svg: -------------------------------------------------------------------------------- 1 | 2 | Bugs: 0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/line-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Lines: 100% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/mutation-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Mutations: 100% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/progress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 92% 6 | 32 | 33 | -------------------------------------------------------------------------------- /badges/style-warnings.svg: -------------------------------------------------------------------------------- 1 | 2 | Warnings: 0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /doc/components.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | component "autograding-github-action" as autograding_github_action { 4 | } 5 | 6 | component "autograding-model" as autograding_model { 7 | } 8 | 9 | component "analysis-model" <> as analysis_model { 10 | class Issue { 11 | } 12 | 13 | class Severity { 14 | } 15 | Issue .> Severity : <> 16 | 17 | class Report { 18 | } 19 | Report o- Issue : issues 20 | 21 | abstract class ReaderFactory { 22 | } 23 | 24 | class FileReaderFactory { 25 | } 26 | ReaderFactory <|-- FileReaderFactory 27 | 28 | abstract class ParserDescriptor { 29 | } 30 | 31 | class ParserRegistry { 32 | } 33 | ParserRegistry o- ParserDescriptor : descriptors 34 | 35 | class IssueFilterBuilder { 36 | } 37 | Report ..> IssueFilterBuilder : <> 38 | 39 | abstract class AbstractViolationAdapter { 40 | } 41 | 42 | class PitAdapter { 43 | } 44 | AbstractViolationAdapter <|-- PitAdapter 45 | 46 | class JUnitAdapter { 47 | } 48 | AbstractViolationAdapter <|-- JUnitAdapter 49 | 50 | } 51 | 52 | component "github-api" as github_api { 53 | } 54 | 55 | component "commons-io" as commons_io { 56 | } 57 | 58 | component "commons-lang3" as commons_lang3 { 59 | } 60 | 61 | autograding_github_action -[#000000].> autograding_model 62 | autograding_github_action -[#000000].> analysis_model 63 | autograding_github_action -[#000000].> github_api 64 | autograding_github_action -[#000000].> commons_io 65 | autograding_github_action -[#000000].> commons_lang3 66 | 67 | @enduml -------------------------------------------------------------------------------- /doc/dependency-graph.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam defaultTextAlignment center 3 | skinparam rectangle { 4 | BackgroundColor<> beige 5 | BackgroundColor<> lightGreen 6 | BackgroundColor<> lightBlue 7 | BackgroundColor<> lightGray 8 | } 9 | rectangle "analysis-model\n\n13.3.0" as edu_hm_hafner_analysis_model_jar 10 | rectangle "jsoup\n\n1.19.1" as org_jsoup_jsoup_jar 11 | rectangle "commons-digester3\n\n3.2" as org_apache_commons_commons_digester3_jar 12 | rectangle "cglib\n\n2.2.2" as cglib_cglib_jar 13 | rectangle "commons-logging\n\n1.3.5" as commons_logging_commons_logging_jar 14 | rectangle "commons-beanutils\n\n1.10.1" as commons_beanutils_commons_beanutils_jar 15 | rectangle "commons-collections\n\n3.2.2" as commons_collections_commons_collections_jar 16 | rectangle "commons-text\n\n1.13.0" as org_apache_commons_commons_text_jar 17 | rectangle "violations-lib\n\n1.158.0" as se_bjurr_violations_violations_lib_jar 18 | rectangle "j2html\n\n1.4.0" as com_j2html_j2html_jar 19 | rectangle "xercesImpl\n\n2.12.2" as xerces_xercesImpl_jar 20 | rectangle "xml-apis\n\n1.4.01" as xml_apis_xml_apis_jar 21 | rectangle "spotbugs\n\n4.9.3" as com_github_spotbugs_spotbugs_jar 22 | rectangle "asm\n\n9.7.1" as org_ow2_asm_asm_jar 23 | rectangle "asm-analysis\n\n9.7.1" as org_ow2_asm_asm_analysis_jar 24 | rectangle "asm-commons\n\n9.7.1" as org_ow2_asm_asm_commons_jar 25 | rectangle "asm-tree\n\n9.7.1" as org_ow2_asm_asm_tree_jar 26 | rectangle "asm-util\n\n9.7.1" as org_ow2_asm_asm_util_jar 27 | rectangle "bcel\n\n6.10.0" as org_apache_bcel_bcel_jar 28 | rectangle "jcip-annotations\n\n1.0-1" as com_github_stephenc_jcip_jcip_annotations_jar 29 | rectangle "dom4j\n\n2.1.4" as org_dom4j_dom4j_jar 30 | rectangle "gson\n\n2.12.1" as com_google_code_gson_gson_jar 31 | rectangle "pmd-core\n\n7.11.0" as net_sourceforge_pmd_pmd_core_jar 32 | rectangle "slf4j-api\n\n2.0.17" as org_slf4j_slf4j_api_jar 33 | rectangle "antlr4-runtime\n\n4.9.3" as org_antlr_antlr4_runtime_jar 34 | rectangle "checker-qual\n\n3.49.0" as org_checkerframework_checker_qual_jar 35 | rectangle "pcollections\n\n4.0.2" as org_pcollections_pcollections_jar 36 | rectangle "nice-xml-messages\n\n3.1" as com_github_oowekyala_ooxml_nice_xml_messages_jar 37 | rectangle "pmd-java\n\n7.11.0" as net_sourceforge_pmd_pmd_java_jar 38 | rectangle "json\n\n20240303" as org_json_json_jar 39 | rectangle "json-smart\n\n2.5.2" as net_minidev_json_smart_jar 40 | rectangle "accessors-smart\n\n2.5.2" as net_minidev_accessors_smart_jar 41 | rectangle "asm\n\n3.3.1" as asm_asm_jar 42 | rectangle "xmlresolver\n\n5.2.2" as org_xmlresolver_xmlresolver_jar 43 | rectangle "xmlresolver\ndata\n5.2.2" as org_xmlresolver_xmlresolver_jar_data 44 | rectangle "autograding-model\n\n6.3.0" as edu_hm_hafner_autograding_model_jar 45 | rectangle "coverage-model\n\n0.53.1" as edu_hm_hafner_coverage_model_jar 46 | rectangle "commons-math3\n\n3.6.1" as org_apache_commons_commons_math3_jar 47 | rectangle "jackson-databind\n\n2.18.3" as com_fasterxml_jackson_core_jackson_databind_jar 48 | rectangle "jackson-annotations\n\n2.18.3" as com_fasterxml_jackson_core_jackson_annotations_jar 49 | rectangle "jackson-core\n\n2.18.3" as com_fasterxml_jackson_core_jackson_core_jar 50 | rectangle "autograding-github-action\n\n5.3.0-SNAPSHOT" as edu_hm_hafner_autograding_github_action_jar 51 | rectangle "github-api\n\n1.327" as org_kohsuke_github_api_jar 52 | rectangle "codingstyle\n\n5.13.0" as edu_hm_hafner_codingstyle_jar 53 | rectangle "spotbugs-annotations\n\n4.9.3" as com_github_spotbugs_spotbugs_annotations_jar 54 | rectangle "error_prone_annotations\n\n2.38.0" as com_google_errorprone_error_prone_annotations_jar 55 | rectangle "commons-lang3\n\n3.17.0" as org_apache_commons_commons_lang3_jar 56 | rectangle "commons-io\n\n2.19.0" as commons_io_commons_io_jar 57 | rectangle "streamex\n\n0.8.3" as one_util_streamex_jar 58 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_jsoup_jsoup_jar 59 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_apache_commons_commons_digester3_jar 60 | edu_hm_hafner_analysis_model_jar -[#000000]-> cglib_cglib_jar 61 | edu_hm_hafner_analysis_model_jar -[#000000]-> commons_logging_commons_logging_jar 62 | edu_hm_hafner_analysis_model_jar -[#000000]-> commons_beanutils_commons_beanutils_jar 63 | edu_hm_hafner_analysis_model_jar -[#000000]-> commons_collections_commons_collections_jar 64 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_apache_commons_commons_text_jar 65 | edu_hm_hafner_analysis_model_jar -[#000000]-> se_bjurr_violations_violations_lib_jar 66 | edu_hm_hafner_analysis_model_jar -[#000000]-> com_j2html_j2html_jar 67 | edu_hm_hafner_analysis_model_jar -[#000000]-> xerces_xercesImpl_jar 68 | edu_hm_hafner_analysis_model_jar -[#000000]-> xml_apis_xml_apis_jar 69 | edu_hm_hafner_analysis_model_jar -[#000000]-> com_github_spotbugs_spotbugs_jar 70 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_ow2_asm_asm_jar 71 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_ow2_asm_asm_analysis_jar 72 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_ow2_asm_asm_commons_jar 73 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_ow2_asm_asm_tree_jar 74 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_ow2_asm_asm_util_jar 75 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_apache_bcel_bcel_jar 76 | edu_hm_hafner_analysis_model_jar -[#000000]-> com_github_stephenc_jcip_jcip_annotations_jar 77 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_dom4j_dom4j_jar 78 | edu_hm_hafner_analysis_model_jar -[#000000]-> com_google_code_gson_gson_jar 79 | edu_hm_hafner_analysis_model_jar -[#000000]-> net_sourceforge_pmd_pmd_core_jar 80 | edu_hm_hafner_analysis_model_jar .[#ABABAB].> org_slf4j_slf4j_api_jar 81 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_antlr_antlr4_runtime_jar 82 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_checkerframework_checker_qual_jar 83 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_pcollections_pcollections_jar 84 | edu_hm_hafner_analysis_model_jar -[#000000]-> com_github_oowekyala_ooxml_nice_xml_messages_jar 85 | edu_hm_hafner_analysis_model_jar -[#000000]-> net_sourceforge_pmd_pmd_java_jar 86 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_json_json_jar 87 | edu_hm_hafner_analysis_model_jar -[#000000]-> net_minidev_json_smart_jar 88 | edu_hm_hafner_analysis_model_jar -[#000000]-> net_minidev_accessors_smart_jar 89 | edu_hm_hafner_analysis_model_jar -[#000000]-> asm_asm_jar 90 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_xmlresolver_xmlresolver_jar 91 | edu_hm_hafner_analysis_model_jar -[#000000]-> org_xmlresolver_xmlresolver_jar_data 92 | edu_hm_hafner_autograding_model_jar -[#000000]-> edu_hm_hafner_analysis_model_jar 93 | edu_hm_hafner_autograding_model_jar -[#000000]-> edu_hm_hafner_coverage_model_jar 94 | edu_hm_hafner_autograding_model_jar -[#000000]-> org_apache_commons_commons_math3_jar 95 | com_fasterxml_jackson_core_jackson_databind_jar -[#000000]-> com_fasterxml_jackson_core_jackson_annotations_jar 96 | com_fasterxml_jackson_core_jackson_databind_jar -[#000000]-> com_fasterxml_jackson_core_jackson_core_jar 97 | edu_hm_hafner_autograding_model_jar -[#000000]-> com_fasterxml_jackson_core_jackson_databind_jar 98 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> edu_hm_hafner_autograding_model_jar 99 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> org_kohsuke_github_api_jar 100 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> org_slf4j_slf4j_api_jar 101 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> edu_hm_hafner_codingstyle_jar 102 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> com_github_spotbugs_spotbugs_annotations_jar 103 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> com_google_errorprone_error_prone_annotations_jar 104 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> org_apache_commons_commons_lang3_jar 105 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> commons_io_commons_io_jar 106 | edu_hm_hafner_autograding_github_action_jar -[#000000]-> one_util_streamex_jar 107 | @enduml -------------------------------------------------------------------------------- /doc/deployment.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | node ":GitHub Server" <> as github_server { 4 | artifact "Git Repository" <> as repo { 5 | } 6 | 7 | artifact "actions/checkout" <> as checkout { 8 | } 9 | 10 | artifact "actions/setup-java" <> as setup_java { 11 | } 12 | 13 | artifact "actions/cache" <> as cache { 14 | } 15 | 16 | artifact "uhafner/autograding-github-action" <> as autograding_action { 17 | } 18 | } 19 | 20 | node ":HM Server" <> as hm_server { 21 | node "**:VM**" <> as vm { 22 | node "**:Ubuntu**" <> as ubuntu { 23 | node "**:Docker**" <> as docker { 24 | node "**:OpenJDK**" <> as jdk { 25 | } 26 | mvn .> jdk 27 | 28 | node "**:Maven**" <> as mvn { 29 | } 30 | mvn ..> autograding_model 31 | mvn ..> analysis_model 32 | mvn ..> github_api 33 | mvn ..> commons_io 34 | mvn ..> commons_lang3 35 | } 36 | docker ..> ubuntu_img 37 | docker ..> autograding_img 38 | } 39 | ubuntu - github_server 40 | } 41 | } 42 | 43 | node ":Docker Hub" <> as docker_hub { 44 | artifact "uhafner/autograding-github-action" <> as autograding_img { 45 | } 46 | 47 | artifact "ubuntu" <> as ubuntu_img { 48 | } 49 | } 50 | 51 | node ":Maven Repository Manager" <> as mvn_repo { 52 | artifact "autograding-model" <> as autograding_model { 53 | } 54 | 55 | artifact "analysis-model" <> as analysis_model { 56 | } 57 | 58 | artifact "github-api" <> as github_api { 59 | } 60 | 61 | artifact "commons-io" <> as commons_io { 62 | } 63 | 64 | artifact "commons-lang3" <> as commons_lang3 { 65 | } 66 | } 67 | 68 | @enduml -------------------------------------------------------------------------------- /images/analysis-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/analysis-annotations.png -------------------------------------------------------------------------------- /images/analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/analysis.png -------------------------------------------------------------------------------- /images/coverage-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/coverage-annotations.png -------------------------------------------------------------------------------- /images/coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/coverage.png -------------------------------------------------------------------------------- /images/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/details.png -------------------------------------------------------------------------------- /images/mutation-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/mutation-annotations.png -------------------------------------------------------------------------------- /images/pr-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/pr-comment.png -------------------------------------------------------------------------------- /images/tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/autograding-github-action/313fa38d37a06c5537281f49725b38e404cd8165/images/tests.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | edu.hm.hafner 7 | codingstyle-pom 8 | 5.22.0 9 | 10 | 11 | 12 | edu.hm.hafner 13 | autograding-github-action 14 | 5.3.0-SNAPSHOT 15 | jar 16 | 17 | Autograding GitHub Action 18 | 19 | This GitHub action autogrades projects based on a configurable set of metrics 20 | and gives feedback on pull requests (or single commits) in GitHub. 21 | I use this action to automatically grade student projects in my lectures 22 | at the Munich University of Applied Sciences. 23 | 24 | 25 | 26 | scm:git:https://github.com/uhafner/autograding-github-action.git 27 | scm:git:git@github.com:uhafner/autograding-github-action.git 28 | https://github.com/uhafner/autograding-github-action 29 | HEAD 30 | 31 | 32 | 33 | ${project.groupId}.autograding.github.action 34 | ${project.version} 35 | 36 | 6.3.0 37 | 1.327 38 | 1.21.1 39 | 40 | 3.4.5 41 | 42 | 43 | 44 | 45 | edu.hm.hafner 46 | autograding-model 47 | ${autograding-model.version} 48 | 49 | 50 | com.google.errorprone 51 | error_prone_annotations 52 | 53 | 54 | com.github.spotbugs 55 | spotbugs-annotations 56 | 57 | 58 | org.apache.commons 59 | commons-lang3 60 | 61 | 62 | commons-io 63 | commons-io 64 | 65 | 66 | codingstyle 67 | edu.hm.hafner 68 | 69 | 70 | streamex 71 | one.util 72 | 73 | 74 | 75 | 76 | 77 | org.kohsuke 78 | github-api 79 | ${github-api.version} 80 | 81 | 82 | org.apache.commons 83 | commons-lang3 84 | 85 | 86 | commons-io 87 | commons-io 88 | 89 | 90 | com.fasterxml.jackson.core 91 | jackson-databind 92 | 93 | 94 | 95 | 96 | 97 | org.testcontainers 98 | testcontainers 99 | ${testcontainers.version} 100 | test 101 | 102 | 103 | jackson-annotations 104 | com.fasterxml.jackson.core 105 | 106 | 107 | com.fasterxml.jackson.core 108 | jackson-annotations 109 | 110 | 111 | 112 | 113 | org.testcontainers 114 | junit-jupiter 115 | ${testcontainers.version} 116 | test 117 | 118 | 119 | 120 | 121 | 122 | ${project.artifactId} 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-failsafe-plugin 127 | 128 | @{argLine} --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED 129 | 130 | 131 | 132 | com.google.cloud.tools 133 | jib-maven-plugin 134 | ${jib-maven-plugin.version} 135 | 136 | 137 | local-docker 138 | pre-integration-test 139 | 140 | dockerBuild 141 | 142 | 143 | 144 | docker-io 145 | install 146 | 147 | build 148 | 149 | 150 | 151 | 152 | 153 | docker.io/uhafner/autograding-github-action 154 | 155 | ${docker-image-tag} 156 | v${docker-image-tag} 157 | 158 | 159 | ${env.DOCKER_IO_USERNAME} 160 | ${env.DOCKER_IO_PASSWORD} 161 | 162 | 163 | 164 | maven:3.9.9-eclipse-temurin-21-alpine 165 | 166 | 167 | amd64 168 | linux 169 | 170 | 171 | arm64 172 | linux 173 | 174 | 175 | 176 | 177 | 178 | 179 | org.revapi 180 | revapi-maven-plugin 181 | 182 | true 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/grading/github/GitHubAnnotationsBuilder.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.grading.github; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import edu.hm.hafner.grading.CommentBuilder; 6 | import edu.hm.hafner.util.FilteredLog; 7 | 8 | import org.kohsuke.github.GHCheckRun.AnnotationLevel; 9 | import org.kohsuke.github.GHCheckRunBuilder.Annotation; 10 | import org.kohsuke.github.GHCheckRunBuilder.Output; 11 | 12 | /** 13 | * Creates GitHub annotations for static analysis warnings, for lines with missing coverage, and for lines with 14 | * survived mutations. 15 | * 16 | * @author Ullrich Hafner 17 | */ 18 | class GitHubAnnotationsBuilder extends CommentBuilder { 19 | private static final String GITHUB_WORKSPACE_REL = "/github/workspace/./"; 20 | private static final String GITHUB_WORKSPACE_ABS = "/github/workspace/"; 21 | 22 | private final Output output; 23 | private final FilteredLog log; 24 | private final int maxWarningComments; 25 | private final int maxCoverageComments; 26 | 27 | GitHubAnnotationsBuilder(final Output output, final String prefix, final FilteredLog log) { 28 | super(prefix, GITHUB_WORKSPACE_REL, GITHUB_WORKSPACE_ABS); 29 | 30 | this.output = output; 31 | this.log = log; 32 | 33 | maxWarningComments = getIntegerEnvironment("MAX_WARNING_ANNOTATIONS"); 34 | maxCoverageComments = getIntegerEnvironment("MAX_COVERAGE_ANNOTATIONS"); 35 | } 36 | 37 | @Override 38 | protected final int getMaxWarningComments() { 39 | return maxWarningComments; 40 | } 41 | 42 | @Override 43 | protected final int getMaxCoverageComments() { 44 | return maxCoverageComments; 45 | } 46 | 47 | private int getIntegerEnvironment(final String key) { 48 | var value = getIntegerEnvironmentWithDefault(key); 49 | log.logInfo(">>>> %s: %d", key, value); 50 | return value; 51 | } 52 | 53 | private int getIntegerEnvironmentWithDefault(final String key) { 54 | var value = getEnv(key); 55 | try { 56 | return Integer.parseInt(value); 57 | } 58 | catch (NumberFormatException exception) { 59 | if (StringUtils.isEmpty(value)) { 60 | log.logInfo(">>>> Environment variable %s not set, falling back to default Integer.MAX_VALUE", key); 61 | } 62 | else { 63 | log.logError(">>>> Error: no integer value in environment variable key %s: %s", key, value); 64 | } 65 | 66 | return Integer.MAX_VALUE; 67 | } 68 | } 69 | 70 | private String getEnv(final String name) { 71 | return StringUtils.defaultString(System.getenv(name)); 72 | } 73 | 74 | @Override 75 | @SuppressWarnings("checkstyle:ParameterNumber") 76 | protected void createComment(final CommentType commentType, final String relativePath, 77 | final int lineStart, final int lineEnd, 78 | final String message, final String title, 79 | final int columnStart, final int columnEnd, 80 | final String details, final String markDownDetails) { 81 | Annotation annotation = new Annotation(relativePath, 82 | lineStart, lineEnd, AnnotationLevel.WARNING, message).withTitle(title); 83 | 84 | if (lineStart == lineEnd) { 85 | annotation.withStartColumn(columnStart).withEndColumn(columnEnd); 86 | } 87 | if (StringUtils.isNotBlank(details)) { 88 | annotation.withRawDetails(details); 89 | } 90 | 91 | output.add(annotation); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/grading/github/GitHubAutoGradingRunner.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.grading.github; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import edu.hm.hafner.grading.AggregatedScore; 6 | import edu.hm.hafner.grading.AutoGradingRunner; 7 | import edu.hm.hafner.grading.GradingReport; 8 | import edu.hm.hafner.util.FilteredLog; 9 | import edu.hm.hafner.util.VisibleForTesting; 10 | 11 | import java.io.IOException; 12 | import java.io.PrintStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Paths; 15 | import java.time.Instant; 16 | import java.util.Date; 17 | import java.util.Locale; 18 | 19 | import org.kohsuke.github.GHCheckRun; 20 | import org.kohsuke.github.GHCheckRun.Conclusion; 21 | import org.kohsuke.github.GHCheckRun.Status; 22 | import org.kohsuke.github.GHCheckRunBuilder; 23 | import org.kohsuke.github.GHCheckRunBuilder.Output; 24 | import org.kohsuke.github.GitHub; 25 | import org.kohsuke.github.GitHubBuilder; 26 | 27 | /** 28 | * GitHub action entrypoint for the autograding action. 29 | * 30 | * @author Tobias Effner 31 | * @author Ullrich Hafner 32 | */ 33 | public class GitHubAutoGradingRunner extends AutoGradingRunner { 34 | /** 35 | * Public entry point for the GitHub action in the docker container, simply calls the action. 36 | * 37 | * @param unused 38 | * not used 39 | */ 40 | public static void main(final String... unused) { 41 | new GitHubAutoGradingRunner().run(); 42 | } 43 | 44 | /** 45 | * Creates a new instance of {@link GitHubAutoGradingRunner}. 46 | */ 47 | public GitHubAutoGradingRunner() { 48 | super(); 49 | } 50 | 51 | @VisibleForTesting 52 | protected GitHubAutoGradingRunner(final PrintStream printStream) { 53 | super(printStream); 54 | } 55 | 56 | @Override 57 | protected String getDisplayName() { 58 | return "GitHub Autograding Action"; 59 | } 60 | 61 | @Override 62 | protected void publishGradingResult(final AggregatedScore score, final FilteredLog log) { 63 | var errors = createErrorMessageMarkdown(log); 64 | 65 | var results = new GradingReport(); 66 | addComment(score, 67 | results.getTextSummary(score, getChecksName()), 68 | results.getMarkdownDetails(score, getChecksName()) + errors, 69 | results.getSubScoreDetails(score) + errors, 70 | results.getMarkdownSummary(score, getChecksName()) + errors, 71 | errors.isBlank() ? Conclusion.SUCCESS : Conclusion.FAILURE, log); 72 | 73 | try { 74 | var environmentVariables = createEnvironmentVariables(score, log); 75 | Files.writeString(Paths.get("metrics.env"), environmentVariables); 76 | } 77 | catch (IOException exception) { 78 | log.logException(exception, "Can't write environment variables to 'metrics.env'"); 79 | } 80 | 81 | log.logInfo("GitHub Action has finished"); 82 | } 83 | 84 | @Override 85 | protected void publishError(final AggregatedScore score, final FilteredLog log, final Throwable exception) { 86 | var results = new GradingReport(); 87 | 88 | var markdownErrors = results.getMarkdownErrors(score, exception); 89 | addComment(score, results.getTextSummary(score, getChecksName()), 90 | markdownErrors, markdownErrors, markdownErrors, Conclusion.FAILURE, log); 91 | } 92 | 93 | private void addComment(final AggregatedScore score, final String textSummary, 94 | final String markdownDetails, final String markdownSummary, final String prSummary, 95 | final Conclusion conclusion, final FilteredLog log) { 96 | try { 97 | var repository = getEnv("GITHUB_REPOSITORY", log); 98 | if (repository.isBlank()) { 99 | log.logError("No GITHUB_REPOSITORY defined - skipping"); 100 | 101 | return; 102 | } 103 | String oAuthToken = getEnv("GITHUB_TOKEN", log); 104 | if (oAuthToken.isBlank()) { 105 | log.logError("No valid GITHUB_TOKEN found - skipping"); 106 | 107 | return; 108 | } 109 | 110 | String sha = getEnv("GITHUB_SHA", log); 111 | 112 | GitHub github = new GitHubBuilder().withAppInstallationToken(oAuthToken).build(); 113 | GHCheckRunBuilder check = github.getRepository(repository) 114 | .createCheckRun(getChecksName(), sha) 115 | .withStatus(Status.COMPLETED) 116 | .withStartedAt(Date.from(Instant.now())) 117 | .withConclusion(conclusion); 118 | 119 | var summaryWithFooter = markdownSummary + "\n\nCreated by " + getVersionLink(log); 120 | Output output = new Output(textSummary, summaryWithFooter).withText(markdownDetails); 121 | 122 | if (getEnv("SKIP_ANNOTATIONS", log).isEmpty()) { 123 | var annotationBuilder = new GitHubAnnotationsBuilder( 124 | output, computeAbsolutePathPrefixToRemove(log), log); 125 | annotationBuilder.createAnnotations(score); 126 | } 127 | 128 | check.add(output); 129 | 130 | GHCheckRun run = check.create(); 131 | log.logInfo("Successfully created check " + run); 132 | 133 | var prNumber = getEnv("PR_NUMBER", log); 134 | if (!prNumber.isBlank()) { // optional PR comment 135 | var footer = "Created by %s. More details are shown in the [GitHub Checks Result](%s)." 136 | .formatted(getVersionLink(log), run.getDetailsUrl().toString()); 137 | github.getRepository(repository) 138 | .getPullRequest(Integer.parseInt(prNumber)) 139 | .comment(prSummary + "\n\n" + footer + "\n"); 140 | log.logInfo("Successfully commented PR#" + prNumber); 141 | } 142 | } 143 | catch (IOException exception) { 144 | log.logException(exception, "Could not create check"); 145 | } 146 | } 147 | 148 | private String getVersionLink(final FilteredLog log) { 149 | var version = readVersion(log); 150 | var sha = readSha(log); 151 | return "[%s](https://github.com/uhafner/autograding-github-action/releases/tag/v%s) v%s (#%s)" 152 | .formatted(getDisplayName(), version, version, sha); 153 | } 154 | 155 | String createEnvironmentVariables(final AggregatedScore score, final FilteredLog log) { 156 | var metrics = new StringBuilder(); 157 | score.getMetrics().forEach((metric, value) -> metrics.append( 158 | String.format(Locale.ENGLISH, "%s=%d%n", metric, value))); 159 | log.logInfo("---------------"); 160 | log.logInfo("Metrics Summary"); 161 | log.logInfo("---------------"); 162 | log.logInfo(metrics.toString()); 163 | return metrics.toString(); 164 | } 165 | 166 | private String getChecksName() { 167 | return StringUtils.defaultIfBlank(System.getenv("CHECKS_NAME"), getDisplayName()); 168 | } 169 | 170 | private String computeAbsolutePathPrefixToRemove(final FilteredLog log) { 171 | return String.format("%s/%s/", getEnv("RUNNER_WORKSPACE", log), 172 | StringUtils.substringAfter(getEnv("GITHUB_REPOSITORY", log), "/")); 173 | } 174 | 175 | private String getEnv(final String key, final FilteredLog log) { 176 | String value = StringUtils.defaultString(System.getenv(key)); 177 | log.logInfo(">>>> " + key + ": " + value); 178 | return value; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/grading/github/GitHubAnnotationBuilderTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.grading.github; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import edu.hm.hafner.grading.AggregatedScore; 6 | import edu.hm.hafner.util.FilteredLog; 7 | 8 | import org.kohsuke.github.GHCheckRunBuilder.Annotation; 9 | import org.kohsuke.github.GHCheckRunBuilder.Output; 10 | 11 | import static org.mockito.Mockito.*; 12 | 13 | class GitHubAnnotationBuilderTest { 14 | @Test 15 | void shouldSkipAnnotationsWhenEmpty() { 16 | var log = new FilteredLog("unused"); 17 | var output = mock(Output.class); 18 | 19 | new GitHubAnnotationsBuilder(output, "/tmp", log).createAnnotations( 20 | new AggregatedScore(log)); 21 | 22 | verify(output, never()).add(any(Annotation.class)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/grading/github/GitHubAutoGradingRunnerDockerITest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.grading.github; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.testcontainers.containers.GenericContainer; 5 | import org.testcontainers.containers.output.ToStringConsumer; 6 | import org.testcontainers.containers.output.WaitingConsumer; 7 | import org.testcontainers.utility.DockerImageName; 8 | import org.testcontainers.utility.MountableFile; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.TimeoutException; 15 | 16 | import static org.assertj.core.api.Assertions.*; 17 | 18 | /** 19 | * Integration test for the grading action. Starts the container and checks if the grading runs as expected. 20 | * 21 | * @author Ullrich Hafner 22 | */ 23 | public class GitHubAutoGradingRunnerDockerITest { 24 | private static final String CONFIGURATION = """ 25 | { 26 | "tests": { 27 | "tools": [ 28 | { 29 | "id": "junit", 30 | "name": "Unittests", 31 | "pattern": "**/target/*-reports/TEST*.xml" 32 | } 33 | ], 34 | "name": "JUnit", 35 | "passedImpact": 10, 36 | "skippedImpact": -1, 37 | "failureImpact": -5, 38 | "maxScore": 100 39 | }, 40 | "analysis": [ 41 | { 42 | "name": "Style", 43 | "id": "style", 44 | "tools": [ 45 | { 46 | "id": "checkstyle", 47 | "name": "CheckStyle", 48 | "pattern": "**/checkstyle*.xml" 49 | }, 50 | { 51 | "id": "pmd", 52 | "name": "PMD", 53 | "pattern": "**/pmd*.xml" 54 | } 55 | ], 56 | "errorImpact": 1, 57 | "highImpact": 2, 58 | "normalImpact": 3, 59 | "lowImpact": 4, 60 | "maxScore": 100 61 | }, 62 | { 63 | "name": "Bugs", 64 | "id": "bugs", 65 | "tools": [ 66 | { 67 | "id": "spotbugs", 68 | "name": "SpotBugs", 69 | "pattern": "**/spotbugs*.xml" 70 | } 71 | ], 72 | "errorImpact": -11, 73 | "highImpact": -12, 74 | "normalImpact": -13, 75 | "lowImpact": -14, 76 | "maxScore": 100 77 | } 78 | ], 79 | "coverage": [ 80 | { 81 | "tools": [ 82 | { 83 | "id": "jacoco", 84 | "name": "Line Coverage", 85 | "metric": "line", 86 | "pattern": "**/jacoco.xml" 87 | }, 88 | { 89 | "id": "jacoco", 90 | "name": "Branch Coverage", 91 | "metric": "branch", 92 | "pattern": "**/jacoco.xml" 93 | } 94 | ], 95 | "name": "JaCoCo", 96 | "maxScore": 100, 97 | "coveredPercentageImpact": 1, 98 | "missedPercentageImpact": -1 99 | }, 100 | { 101 | "tools": [ 102 | { 103 | "id": "pit", 104 | "name": "Mutation Coverage", 105 | "metric": "mutation", 106 | "pattern": "**/mutations.xml" 107 | } 108 | ], 109 | "name": "PIT", 110 | "maxScore": 100, 111 | "coveredPercentageImpact": 1, 112 | "missedPercentageImpact": -1 113 | } 114 | ] 115 | } 116 | """; 117 | private static final String WS = "/github/workspace/target/"; 118 | private static final String LOCAL_METRICS_FILE = "target/metrics.env"; 119 | 120 | @Test 121 | void shouldGradeInDockerContainer() throws TimeoutException, IOException { 122 | try (var container = createContainer()) { 123 | container.withEnv("CONFIG", CONFIGURATION); 124 | startContainerWithAllFiles(container); 125 | 126 | var metrics = new String[] { 127 | "tests=1", 128 | "line=11", 129 | "branch=10", 130 | "mutation=8", 131 | "bugs=1", 132 | "spotbugs=1", 133 | "style=2", 134 | "pmd=1", 135 | "checkstyle=1"}; 136 | 137 | assertThat(readStandardOut(container)) 138 | .contains("Obtaining configuration from environment variable CONFIG") 139 | .contains(metrics) 140 | .contains(new String[] { 141 | "Processing 1 test configuration(s)", 142 | "-> Unittests Total: TESTS: 1", 143 | "JUnit Score: 10 of 100", 144 | "Processing 2 coverage configuration(s)", 145 | "-> Line Coverage Total: LINE: 10.93% (33/302)", 146 | "-> Branch Coverage Total: BRANCH: 9.52% (4/42)", 147 | "=> JaCoCo Score: 20 of 100", 148 | "-> Mutation Coverage Total: MUTATION: 7.86% (11/140)", 149 | "=> PIT Score: 16 of 100", 150 | "Processing 2 static analysis configuration(s)", 151 | "-> CheckStyle (checkstyle): 1 warning (normal: 1)", 152 | "-> PMD (pmd): 1 warning (normal: 1)", 153 | "=> Style Score: 6 of 100", 154 | "-> SpotBugs (spotbugs): 1 bug (low: 1)", 155 | "=> Bugs Score: 86 of 100", 156 | "Autograding score - 138 of 500"}); 157 | 158 | container.copyFileFromContainer("/github/workspace/metrics.env", LOCAL_METRICS_FILE); 159 | assertThat(Files.readString(Path.of(LOCAL_METRICS_FILE))) 160 | .contains(metrics); 161 | } 162 | } 163 | 164 | @Test 165 | void shouldUseDefaultConfiguration() throws TimeoutException { 166 | try (var container = createContainer()) { 167 | startContainerWithAllFiles(container); 168 | 169 | assertThat(readStandardOut(container)) 170 | .contains("No configuration provided (environment variable CONFIG not set), using default configuration") 171 | .contains(new String[] { 172 | "Processing 1 test configuration(s)", 173 | "-> JUnit Tests Total: TESTS: 1", 174 | "Tests Score: 100 of 100", 175 | "Processing 2 coverage configuration(s)", 176 | "-> Line Coverage Total: LINE: 10.93% (33/302)", 177 | "-> Branch Coverage Total: BRANCH: 9.52% (4/42)", 178 | "=> Code Coverage Score: 10 of 100", 179 | "-> Mutation Coverage Total: MUTATION: 7.86% (11/140)", 180 | "-> Test Strength Total: TEST_STRENGTH: 84.62% (11/13)", 181 | "=> Mutation Coverage Score: 46 of 100", 182 | "Processing 2 static analysis configuration(s)", 183 | "-> CheckStyle (checkstyle): 1 warning (normal: 1)", 184 | "-> PMD (pmd): 1 warning (normal: 1)", 185 | "=> Style Score: 98 of 100", 186 | "-> SpotBugs (spotbugs): 1 bug (low: 1)", 187 | "=> Bugs Score: 97 of 100", 188 | "Autograding score - 351 of 500 (70%)"}); 189 | } 190 | } 191 | 192 | @Test 193 | void shouldShowErrors() throws TimeoutException { 194 | try (var container = createContainer()) { 195 | container.withWorkingDirectory("/github/workspace").start(); 196 | assertThat(readStandardOut(container)) 197 | .contains(new String[] { 198 | "Processing 1 test configuration(s)", 199 | "Configuration error for 'JUnit Tests'?", 200 | "Tests Score: 100 of 100", 201 | "Processing 2 coverage configuration(s)", 202 | "=> Code Coverage Score: 100 of 100", 203 | "Configuration error for 'Line Coverage'?", 204 | "Configuration error for 'Branch Coverage'?", 205 | "=> Mutation Coverage Score: 100 of 100", 206 | "Configuration error for 'Mutation Coverage'?", 207 | "Processing 2 static analysis configuration(s)", 208 | "Configuration error for 'CheckStyle'?", 209 | "Configuration error for 'PMD'?", 210 | "Configuration error for 'SpotBugs'?", 211 | "-> CheckStyle (checkstyle): No warnings", 212 | "-> PMD (pmd): No warnings", 213 | "=> Style Score: 100 of 100", 214 | "-> SpotBugs (spotbugs): No warnings", 215 | "=> Bugs Score: 100 of 100", 216 | "Autograding score - 500 of 500"}); 217 | } 218 | } 219 | 220 | private GenericContainer createContainer() { 221 | return new GenericContainer<>(DockerImageName.parse("uhafner/autograding-github-action:5.3.0-SNAPSHOT")); 222 | } 223 | 224 | private String readStandardOut(final GenericContainer> container) throws TimeoutException { 225 | var waitingConsumer = new WaitingConsumer(); 226 | var toStringConsumer = new ToStringConsumer(); 227 | 228 | var composedConsumer = toStringConsumer.andThen(waitingConsumer); 229 | container.followOutput(composedConsumer); 230 | waitingConsumer.waitUntil(frame -> frame.getUtf8String().contains("End GitHub Autograding"), 60, TimeUnit.SECONDS); 231 | 232 | return toStringConsumer.toUtf8String(); 233 | } 234 | 235 | private void startContainerWithAllFiles(final GenericContainer container) { 236 | container.withWorkingDirectory("/github/workspace") 237 | .withCopyFileToContainer(read("checkstyle/checkstyle-result.xml"), WS + "checkstyle-result.xml") 238 | .withCopyFileToContainer(read("jacoco/jacoco.xml"), WS + "site/jacoco/jacoco.xml") 239 | .withCopyFileToContainer(read("junit/TEST-edu.hm.hafner.grading.AutoGradingActionTest.xml"), WS + "surefire-reports/TEST-Aufgabe3Test.xml") 240 | .withCopyFileToContainer(read("pit/mutations.xml"), WS + "pit-reports/mutations.xml") 241 | .withCopyFileToContainer(read("pmd/pmd.xml"), WS + "pmd.xml") 242 | .withCopyFileToContainer(read("spotbugs/spotbugsXml.xml"), WS + "spotbugsXml.xml") 243 | .start(); 244 | } 245 | 246 | private MountableFile read(final String resourceName) { 247 | return MountableFile.forClasspathResource("/" + resourceName); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/grading/github/GitHubAutoGradingRunnerITest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.grading.github; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junitpioneer.jupiter.SetEnvironmentVariable; 5 | 6 | import edu.hm.hafner.util.ResourceTest; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.PrintStream; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | import static org.assertj.core.api.Assertions.*; 13 | 14 | /** 15 | * Integration test for the grading action. Runs the action locally in the filesystem. 16 | * 17 | * @author Ullrich Hafner 18 | */ 19 | public class GitHubAutoGradingRunnerITest extends ResourceTest { 20 | private static final String CONFIGURATION = """ 21 | { 22 | "tests": { 23 | "tools": [ 24 | { 25 | "id": "junit", 26 | "name": "Unittests", 27 | "pattern": "**/src/**/TEST*.xml" 28 | } 29 | ], 30 | "name": "JUnit", 31 | "passedImpact": 10, 32 | "skippedImpact": -1, 33 | "failureImpact": -5, 34 | "maxScore": 100 35 | }, 36 | "analysis": [ 37 | { 38 | "name": "Style", 39 | "id": "style", 40 | "tools": [ 41 | { 42 | "id": "checkstyle", 43 | "name": "CheckStyle", 44 | "pattern": "**/src/**/checkstyle*.xml" 45 | }, 46 | { 47 | "id": "pmd", 48 | "name": "PMD", 49 | "pattern": "**/src/**/pmd*.xml" 50 | } 51 | ], 52 | "errorImpact": 1, 53 | "highImpact": 2, 54 | "normalImpact": 3, 55 | "lowImpact": 4, 56 | "maxScore": 100 57 | }, 58 | { 59 | "name": "Bugs", 60 | "id": "bugs", 61 | "tools": [ 62 | { 63 | "id": "spotbugs", 64 | "name": "SpotBugs", 65 | "pattern": "**/src/**/spotbugs*.xml" 66 | } 67 | ], 68 | "errorImpact": -11, 69 | "highImpact": -12, 70 | "normalImpact": -13, 71 | "lowImpact": -14, 72 | "maxScore": 100 73 | } 74 | ], 75 | "coverage": [ 76 | { 77 | "tools": [ 78 | { 79 | "id": "jacoco", 80 | "name": "Line Coverage", 81 | "metric": "line", 82 | "pattern": "**/src/**/jacoco.xml" 83 | }, 84 | { 85 | "id": "jacoco", 86 | "name": "Branch Coverage", 87 | "metric": "branch", 88 | "pattern": "**/src/**/jacoco.xml" 89 | } 90 | ], 91 | "name": "JaCoCo", 92 | "maxScore": 100, 93 | "coveredPercentageImpact": 1, 94 | "missedPercentageImpact": -1 95 | }, 96 | { 97 | "tools": [ 98 | { 99 | "id": "pit", 100 | "name": "Mutation Coverage", 101 | "metric": "mutation", 102 | "pattern": "**/src/**/mutations.xml" 103 | } 104 | ], 105 | "name": "PIT", 106 | "maxScore": 100, 107 | "coveredPercentageImpact": 1, 108 | "missedPercentageImpact": -1 109 | } 110 | ] 111 | } 112 | """; 113 | 114 | @Test 115 | @SetEnvironmentVariable(key = "CONFIG", value = CONFIGURATION) 116 | void shouldGradeWithConfigurationFromEnvironment() { 117 | assertThat(runAutoGrading()) 118 | .contains("Obtaining configuration from environment variable CONFIG") 119 | .contains(new String[] { 120 | "Processing 1 test configuration(s)", 121 | "-> Unittests Total: TESTS: 37", 122 | "JUnit Score: 100 of 100", 123 | "Processing 2 coverage configuration(s)", 124 | "-> Line Coverage Total: LINE: 10.93% (33/302)", 125 | "-> Branch Coverage Total: BRANCH: 9.52% (4/42)", 126 | "=> JaCoCo Score: 20 of 100", 127 | "-> Mutation Coverage Total: MUTATION: 7.86% (11/140)", 128 | "=> PIT Score: 16 of 100", 129 | "Processing 2 static analysis configuration(s)", 130 | "-> CheckStyle (checkstyle): 19 warnings (normal: 19)", 131 | "-> PMD (pmd): 41 warnings (normal: 41)", 132 | "=> Style Score: 100 of 100", 133 | "-> SpotBugs (spotbugs): 1 bug (low: 1)", 134 | "=> Bugs Score: 86 of 100", 135 | "Autograding score - 322 of 500"}); 136 | } 137 | 138 | private static final String CONFIGURATION_WRONG_PATHS = """ 139 | { 140 | "tests": { 141 | "tools": [ 142 | { 143 | "id": "junit", 144 | "name": "Unittests", 145 | "pattern": "**/does-not-exist/TEST*.xml" 146 | } 147 | ], 148 | "name": "JUnit", 149 | "passedImpact": 10, 150 | "skippedImpact": -1, 151 | "failureImpact": -5, 152 | "maxScore": 100 153 | }, 154 | "analysis": [ 155 | { 156 | "name": "Style", 157 | "id": "style", 158 | "tools": [ 159 | { 160 | "id": "checkstyle", 161 | "name": "CheckStyle", 162 | "pattern": "**/does-not-exist/checkstyle*.xml" 163 | }, 164 | { 165 | "id": "pmd", 166 | "name": "PMD", 167 | "pattern": "**/does-not-exist/pmd*.xml" 168 | } 169 | ], 170 | "errorImpact": 1, 171 | "highImpact": 2, 172 | "normalImpact": 3, 173 | "lowImpact": 4, 174 | "maxScore": 100 175 | }, 176 | { 177 | "name": "Bugs", 178 | "id": "bugs", 179 | "tools": [ 180 | { 181 | "id": "spotbugs", 182 | "name": "SpotBugs", 183 | "pattern": "**/does-not-exist/spotbugs*.xml" 184 | } 185 | ], 186 | "errorImpact": -11, 187 | "highImpact": -12, 188 | "normalImpact": -13, 189 | "lowImpact": -14, 190 | "maxScore": 100 191 | } 192 | ], 193 | "coverage": [ 194 | { 195 | "tools": [ 196 | { 197 | "id": "jacoco", 198 | "name": "Line Coverage", 199 | "metric": "line", 200 | "pattern": "**/does-not-exist/jacoco.xml" 201 | }, 202 | { 203 | "id": "jacoco", 204 | "name": "Branch Coverage", 205 | "metric": "branch", 206 | "pattern": "**/does-not-exist/jacoco.xml" 207 | } 208 | ], 209 | "name": "JaCoCo", 210 | "maxScore": 100, 211 | "coveredPercentageImpact": 1, 212 | "missedPercentageImpact": -1 213 | }, 214 | { 215 | "tools": [ 216 | { 217 | "id": "pit", 218 | "name": "Mutation Coverage", 219 | "metric": "mutation", 220 | "pattern": "**/does-not-exist/mutations.xml" 221 | } 222 | ], 223 | "name": "PIT", 224 | "maxScore": 100, 225 | "coveredPercentageImpact": 1, 226 | "missedPercentageImpact": -1 227 | } 228 | ] 229 | } 230 | """; 231 | 232 | @Test 233 | @SetEnvironmentVariable(key = "CONFIG", value = CONFIGURATION_WRONG_PATHS) 234 | void shouldShowErrors() { 235 | assertThat(runAutoGrading()) 236 | .contains(new String[] { 237 | "Processing 1 test configuration(s)", 238 | "Configuration error for 'Unittests'?", 239 | "JUnit Score: 100 of 100", 240 | "Processing 2 coverage configuration(s)", 241 | "=> JaCoCo Score: 100 of 100", 242 | "Configuration error for 'Line Coverage'?", 243 | "Configuration error for 'Branch Coverage'?", 244 | "=> PIT Score: 100 of 100", 245 | "Configuration error for 'Mutation Coverage'?", 246 | "Processing 2 static analysis configuration(s)", 247 | "Configuration error for 'CheckStyle'?", 248 | "Configuration error for 'PMD'?", 249 | "Configuration error for 'SpotBugs'?", 250 | "-> CheckStyle (checkstyle): No warnings", 251 | "-> PMD (pmd): No warnings", 252 | "=> Style Score: 0 of 100", 253 | "-> SpotBugs (spotbugs): No warnings", 254 | "=> Bugs Score: 100 of 100", 255 | "Autograding score - 400 of 500"}); 256 | } 257 | 258 | private String runAutoGrading() { 259 | var outputStream = new ByteArrayOutputStream(); 260 | var runner = new GitHubAutoGradingRunner(new PrintStream(outputStream)); 261 | runner.run(); 262 | return outputStream.toString(StandardCharsets.UTF_8); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/test/resources/checkstyle/checkstyle-ignores.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/test/resources/checkstyle/checkstyle-result.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/test/resources/junit/TEST-Aufgabe3Test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 70 | to be equal to: 71 | <[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], []]]]> 72 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 73 | Comparators used: 74 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 75 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 76 | but was not. 77 | at Aufgabe3Test.shouldSplitToEmptyRight(Aufgabe3Test.java:254) 78 | ]]> 79 | 80 | 81 | 85 | to be equal to: 86 | <[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], []]]]> 87 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 88 | Comparators used: 89 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 90 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 91 | but was not. 92 | at Aufgabe3Test.shouldSplitToEmptyRight(Aufgabe3Test.java:254) 93 | ]]> 94 | 95 | 96 | 100 | to be equal to: 101 | <[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], []]]]> 102 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 103 | Comparators used: 104 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 105 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 106 | but was not. 107 | at Aufgabe3Test.shouldSplitToEmptyRight(Aufgabe3Test.java:254) 108 | ]]> 109 | 110 | 111 | 112 | 113 | 118 | 119 | 120 | 121 | 125 | to be equal to: 126 | <[[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]]> 127 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 128 | Comparators used: 129 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 130 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 131 | but was not. 132 | at Aufgabe3Test.shouldSplit(Aufgabe3Test.java:219) 133 | ]]> 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 153 | to contain exactly (and in same order): 154 | <[1]> 155 | but some elements were not found: 156 | <[1]> 157 | and others were not expected: 158 | <[0, 0]> 159 | 160 | at Aufgabe3Test.shouldRemoveDuplicates(Aufgabe3Test.java:299) 161 | ]]> 162 | 163 | 164 | 165 | 169 | to be equal to: 170 | <[10, 1]> 171 | when recursively comparing field by field, but found the following 2 differences: 172 | 173 | field/property 'value' differ: 174 | - actual value : 10 175 | - expected value : 1 176 | 177 | field/property 'value' differ: 178 | - actual value : 1 179 | - expected value : 10 180 | 181 | The recursive comparison was performed with this configuration: 182 | - no overridden equals methods were used in the comparison (except for java types) 183 | - these types were compared with the following comparators: 184 | - java.lang.Double -> DoubleComparator[precision=1.0E-15] 185 | - java.lang.Float -> FloatComparator[precision=1.0E-6] 186 | - actual and expected objects and their fields were compared field by field recursively even if they were not of the same type, this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior). 187 | 188 | at Aufgabe3Test.shouldInsertBeforeAdditional(Aufgabe3Test.java:152) 189 | ]]> 190 | 191 | 192 | 193 | 194 | 195 | 199 | to be equal to: 200 | <[[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]]> 201 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 202 | Comparators used: 203 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 204 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 205 | but was not. 206 | at Aufgabe3Test.shouldSplitToEmptyLeft(Aufgabe3Test.java:242) 207 | ]]> 208 | 209 | 210 | 214 | to be equal to: 215 | <[[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]]> 216 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 217 | Comparators used: 218 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 219 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 220 | but was not. 221 | at Aufgabe3Test.shouldSplitToEmptyLeft(Aufgabe3Test.java:242) 222 | ]]> 223 | 224 | 225 | 229 | to be equal to: 230 | <[[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]]> 231 | when comparing elements using recursive field/property by field/property comparator on all fields/properties 232 | Comparators used: 233 | - for elements fields (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 234 | - for elements (by type): {Double -> DoubleComparator[precision=1.0E-15], Float -> FloatComparator[precision=1.0E-6]} 235 | but was not. 236 | at Aufgabe3Test.shouldSplitToEmptyLeft(Aufgabe3Test.java:242) 237 | ]]> 238 | 239 | 240 | 244 | to contain exactly (and in same order): 245 | <[1, 2, 3]> 246 | but some elements were not found: 247 | <[1, 2, 3]> 248 | and others were not expected: 249 | <[-1, -1, -1]> 250 | 251 | at Aufgabe3Test.shouldCopyValues(Aufgabe3Test.java:63) 252 | ]]> 253 | 254 | 255 | 256 | is not final in (Integers.java:0) 259 | ]]> 260 | 261 | -------------------------------------------------------------------------------- /src/test/resources/junit/TEST-edu.hm.hafner.grading.AutoGradingActionTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | -------------------------------------------------------------------------------- /src/test/resources/junit/TEST-edu.hm.hafner.grading.ReportFinderTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/test/resources/pmd/pmd-ignores.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 6 | 7 | 8 | 9 | 10 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 11 | 12 | 13 | 14 | 15 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 16 | 17 | 18 | 19 | 20 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 21 | 22 | 23 | 24 | 25 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 26 | 27 | 28 | 29 | 30 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 31 | 32 | 33 | 34 | 35 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 36 | 37 | 38 | 39 | 40 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 41 | 42 | 43 | 44 | 45 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 46 | 47 | 48 | 49 | 50 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 51 | 52 | 53 | 54 | 55 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 56 | 57 | 58 | 59 | 60 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 61 | 62 | 63 | 64 | 65 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 66 | 67 | 68 | 69 | 70 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 71 | 72 | 73 | Consider using varargs for methods or constructors which take an array the last parameter. 74 | 75 | 76 | 77 | 78 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 79 | 80 | 81 | The method 'main(String...)' has a cognitive complexity of 16, current threshold is 15 82 | 83 | 84 | These nested if statements could be combined 85 | 86 | 87 | 88 | 89 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 90 | 91 | 92 | Avoid using Literals in Conditional Statements 93 | 94 | 95 | Consider using varargs for methods or constructors which take an array the last parameter. 96 | 97 | 98 | 99 | 100 | All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning. 101 | 102 | 103 | 104 | 105 | This abstract class does not have any abstract methods 106 | 107 | 108 | 109 | 110 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 111 | 112 | 113 | 114 | 115 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 116 | 117 | 118 | 119 | 120 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 121 | 122 | 123 | 124 | 125 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 126 | 127 | 128 | 129 | 130 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 131 | 132 | 133 | 134 | 135 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 136 | 137 | 138 | 139 | 140 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 141 | 142 | 143 | 144 | 145 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 146 | 147 | 148 | 149 | 150 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 151 | 152 | 153 | 154 | 155 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 156 | 157 | 158 | 159 | 160 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 161 | 162 | 163 | 164 | 165 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 166 | 167 | 168 | 169 | 170 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 171 | 172 | 173 | 174 | 175 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 176 | 177 | 178 | 179 | 180 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 181 | 182 | 183 | 184 | 185 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 186 | 187 | 188 | 189 | 190 | Linguistics Antipattern - The method 'shouldSolveAssignment' indicates linguistically it returns a boolean, but it returns 'Stream' 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/test/resources/pmd/pmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This abstract class does not have any abstract methods 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | --------------------------------------------------------------------------------