├── CNAME ├── .java-version ├── project ├── build.properties ├── plugins.sbt └── Platform.scala ├── src ├── test │ ├── resources │ │ ├── invalid-report.xml │ │ ├── test-paths-with-different-paths.xml │ │ ├── non-git-report.xml │ │ └── dotcover-example.xml │ └── scala │ │ └── com │ │ └── codacy │ │ ├── model │ │ └── configuration │ │ │ └── CommitUUIDSpec.scala │ │ ├── transformation │ │ ├── PathPrefixerSpec.scala │ │ ├── FileNameMatcherSpec.scala │ │ └── GitFileNameUpdaterAndFilterSpec.scala │ │ └── rules │ │ └── commituuid │ │ ├── CommitUUIDProviderSpec.scala │ │ └── providers │ │ └── GitHubActionProviderSpec.scala ├── main │ ├── scala │ │ └── com │ │ │ └── codacy │ │ │ ├── transformation │ │ │ ├── Transformation.scala │ │ │ ├── PathPrefixer.scala │ │ │ ├── GitFileNameUpdaterAndFilter.scala │ │ │ └── FileNameMatcher.scala │ │ │ ├── rules │ │ │ ├── file │ │ │ │ └── GitFileFetcher.scala │ │ │ └── commituuid │ │ │ │ ├── providers │ │ │ │ ├── DockerProvider.scala │ │ │ │ ├── JenkinsProvider.scala │ │ │ │ ├── ArgoCDProvider.scala │ │ │ │ ├── ShippableCIProvider.scala │ │ │ │ ├── SolanoCIProvider.scala │ │ │ │ ├── GitlabProvider.scala │ │ │ │ ├── GreenhouseCIProvider.scala │ │ │ │ ├── AzurePipelinesProvider.scala │ │ │ │ ├── AWSCodeBuildProvider.scala │ │ │ │ ├── MagnumCIProvider.scala │ │ │ │ ├── CircleCIProvider.scala │ │ │ │ ├── CodefreshCIProvider.scala │ │ │ │ ├── WerckerCIProvider.scala │ │ │ │ ├── AppveyorProvider.scala │ │ │ │ ├── CodeshipCIProvider.scala │ │ │ │ ├── BitriseCIProvider.scala │ │ │ │ ├── BuildkiteCIProvider.scala │ │ │ │ ├── HerokuCIProvider.scala │ │ │ │ ├── SemaphoreCIProvider.scala │ │ │ │ ├── DroneCIProvider.scala │ │ │ │ ├── TravisCIProvider.scala │ │ │ │ ├── BitbucketCloudProvider.scala │ │ │ │ ├── TeamCityProvider.scala │ │ │ │ └── GitHubActionProvider.scala │ │ │ │ └── CommitUUIDProvider.scala │ │ │ ├── di │ │ │ └── Components.scala │ │ │ ├── CodacyCoverageReporter.scala │ │ │ └── model │ │ │ └── configuration │ │ │ └── Configuration.scala │ └── resources │ │ └── logback.xml └── it │ └── scala │ └── com │ └── codacy │ └── CodacyCoverageReporterSpec.scala ├── .github ├── CODEOWNERS ├── codacy-coverage-reporter.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── create_issue_on_label.yml │ ├── create_issue.yml │ └── comment_issue.yml ├── coverage-parser └── src │ ├── test │ ├── resources │ │ ├── TestSourceFile.scala │ │ ├── TestSourceFile2.scala │ │ ├── invalid_report.lcov │ │ ├── test_go.out │ │ ├── test_go_gh.out │ │ ├── test_lcov.lcov │ │ ├── invalid_report.xml │ │ ├── phpunitxml │ │ │ ├── Api │ │ │ │ └── Api.php.xml │ │ │ ├── CodacyPhpCoverage.php.xml │ │ │ ├── Git │ │ │ │ └── GitClient.php.xml │ │ │ ├── Report │ │ │ │ ├── CoverageReport.php.xml │ │ │ │ ├── JsonProducer.php.xml │ │ │ │ └── FileReport.php.xml │ │ │ ├── Config.php.xml │ │ │ └── Parser │ │ │ │ ├── Parser.php.xml │ │ │ │ └── PhpUnitXmlParser.php.xml │ │ ├── go │ │ │ ├── original_package.out │ │ │ └── changed_package_name.out │ │ ├── test_clover_with_paths.xml │ │ ├── thousand_sep_cobertura.xml │ │ ├── windows_paths_cobertura.xml │ │ ├── test_cobertura.xml │ │ ├── test_opencover.xml │ │ └── test_dotcover.xml │ └── scala │ │ └── com │ │ └── codacy │ │ └── parsers │ │ ├── JacocoParserTest.scala │ │ ├── CoverageParserTest.scala │ │ ├── LCOVParserTest.scala │ │ ├── OpenCoverParserTest.scala │ │ ├── CoverageParserFactoryTest.scala │ │ ├── GoParserTest.scala │ │ ├── DotCoverParserTest.scala │ │ ├── CoberturaParserTest.scala │ │ ├── PhpUnitXmlParserTest.scala │ │ └── CloverParserTest.scala │ └── main │ └── scala │ └── com │ └── codacy │ └── parsers │ ├── util │ ├── MathUtils.scala │ ├── TextUtils.scala │ └── XMLoader.scala │ ├── XmlReportParser.scala │ ├── CoverageParser.scala │ └── implementation │ ├── JacocoParser.scala │ ├── CoberturaParser.scala │ ├── DotcoverParser.scala │ ├── OpenCoverParser.scala │ ├── LCOVParser.scala │ ├── PhpUnitXmlParser.scala │ ├── GoParser.scala │ └── CloverParser.scala ├── orbs ├── @orb.yml ├── examples │ └── codacy-coverage-report.yml └── commands │ └── send_report.yml ├── api-scala └── src │ ├── main │ └── scala │ │ └── com │ │ └── codacy │ │ └── api │ │ ├── client │ │ ├── Request.scala │ │ ├── RequestTimeout.scala │ │ ├── RequestSuccess.scala │ │ ├── RequestResponse.scala │ │ └── CodacyClient.scala │ │ ├── util │ │ └── JsonOps.scala │ │ ├── helpers │ │ ├── FileHelper.scala │ │ └── vcs │ │ │ └── GitClient.scala │ │ ├── CoverageReport.scala │ │ └── service │ │ └── CoverageServices.scala │ └── test │ └── scala │ └── com │ └── codacy │ └── api │ └── GitClientTest.scala ├── integration-tests ├── mock-server-error-config.json └── mock-server-config.json ├── Dockerfile ├── SECURITY.md ├── LICENSE ├── .gitignore ├── .scalafmt.conf ├── graalvm └── build-deps.sh └── README.md /CNAME: -------------------------------------------------------------------------------- 1 | coverage.codacy.com -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | oracle64-1.8.0.25 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /src/test/resources/invalid-report.xml: -------------------------------------------------------------------------------- 1 | invalid report 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @codacy/toss 2 | README.md @codacy/techwriters 3 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/TestSourceFile.scala: -------------------------------------------------------------------------------- 1 | class TestSourceFile() 2 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/TestSourceFile2.scala: -------------------------------------------------------------------------------- 1 | class TestSourceFile2() 2 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/invalid_report.lcov: -------------------------------------------------------------------------------- 1 | DA:1,1 2 | SF:foobar.scala 3 | DA:1,0 4 | -------------------------------------------------------------------------------- /orbs/@orb.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | description: > 4 | Orb to send Codacy coverage reports 5 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_go.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | example.com/m/v2/hello.go:5.13,7.2 1 0 3 | example.com/m/v2/hello.go:11.30,14.2 2 1 4 | example.com/m/v2/hello.go:17.35,19.2 1 1 5 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/client/Request.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.client 2 | 3 | case class Request[T](endpoint: String, classType: Class[T], queryParameters: Map[String, String] = Map.empty) 4 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_go_gh.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | github.com/orgname/reponame/src/hello.go:5.13,7.2 1 0 3 | github.com/orgname/reponame/src/hello.go:11.30,14.2 2 1 4 | github.com/orgname/reponame/src/hello.go:17.35,19.2 1 1 5 | -------------------------------------------------------------------------------- /integration-tests/mock-server-error-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpRequest": { 3 | "path": "/2.0/coverage.*" 4 | }, 5 | "httpResponse": { 6 | "statusCode": 400, 7 | "body": { "error": "Error posting coverage" } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/client/RequestTimeout.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.client 2 | 3 | /** 4 | * The socket connection and read timeouts in milliseconds. 5 | */ 6 | case class RequestTimeout(connTimeoutMs: Int, readTimeoutMs: Int) 7 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_lcov.lcov: -------------------------------------------------------------------------------- 1 | SF:coverage-parser/src/test/resources/TestSourceFile.scala 2 | DA:3,0 3 | DA:4,1 4 | DA:5,1 5 | DA:6,2 6 | SF:coverage-parser/src/test/resources/TestSourceFile2.scala 7 | DA:1,1 8 | DA:2,1 9 | DA:3,1 10 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/transformation/Transformation.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import com.codacy.api.CoverageReport 4 | 5 | trait Transformation { 6 | 7 | def execute(report: CoverageReport): CoverageReport 8 | 9 | } 10 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/invalid_report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15.3 2 | 3 | ARG nativeImageLocation=target/graalvm-native-image/codacy-coverage-reporter 4 | 5 | COPY ${nativeImageLocation} /app/codacy-coverage-reporter 6 | 7 | WORKDIR /code 8 | 9 | ENTRYPOINT [ "/app/codacy-coverage-reporter" ] 10 | -------------------------------------------------------------------------------- /integration-tests/mock-server-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpRequest": { 3 | "body": {"fileReports":[{"filename":".circleci/config.yml","coverage":{"10":1,"21":1,"9":1,"13":1,"17":1,"19":0,"15":0}}]} 4 | }, 5 | "httpResponse": { 6 | "statusCode": 200, 7 | "body": {"success": "Report sent!"} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/client/RequestSuccess.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.client 2 | 3 | import play.api.libs.json.{Json, Reads} 4 | 5 | case class RequestSuccess(success: String) 6 | 7 | object RequestSuccess { 8 | implicit val requestSuccessReads: Reads[RequestSuccess] = Json.reads[RequestSuccess] 9 | } 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") 2 | addSbtPlugin("com.codacy" % "codacy-sbt-plugin" % "25.1.1") 3 | 4 | // Publish 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.4") 6 | 7 | // Coverage 8 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") 9 | 10 | libraryDependencySchemes ++= Seq("org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always) 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Supported versions receive security patches. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 13.x.y | :white_check_mark: | 10 | | <= 12 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please follow [Codacy's Responsible Disclosure Policy](https://security.codacy.com/#responsible-disclosure) 15 | -------------------------------------------------------------------------------- /.github/codacy-coverage-reporter.yml: -------------------------------------------------------------------------------- 1 | name: codacy-coverage-reporter 2 | 3 | on: ["push"] 4 | 5 | jobs: 6 | codacy-coverage-reporter: 7 | runs-on: ubuntu-latest 8 | name: codacy-coverage-reporter 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Run codacy-coverage-reporter 12 | uses: codacy/codacy-coverage-reporter-action@master 13 | with: 14 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 15 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/util/JsonOps.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.util 2 | 3 | import play.api.libs.json.{JsError, JsPath, Json, JsonValidationError} 4 | 5 | import scala.collection 6 | 7 | object JsonOps { 8 | 9 | def handleConversionFailure(error: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): String = { 10 | val jsonError = Json.stringify(JsError.toJson(error.toList)) 11 | s"Json conversion error: $jsonError" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/util/MathUtils.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.util 2 | 3 | object MathUtils { 4 | 5 | implicit class ParseIntOps(val s: String) extends AnyVal { 6 | def toIntOrMaxValue: Int = BigInt(s).toIntOrMaxValue 7 | } 8 | 9 | implicit class BigIntOps(val bigInt: BigInt) extends AnyVal { 10 | 11 | def toIntOrMaxValue: Int = 12 | if (bigInt.isValidInt) bigInt.toInt 13 | else Int.MaxValue 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /orbs/examples/codacy-coverage-report.yml: -------------------------------------------------------------------------------- 1 | description: Send coverage report to Codacy 2 | usage: 3 | jobs: 4 | codacy-coverage-report: 5 | docker: 6 | - image: circleci/openjdk:8-jdk 7 | steps: 8 | - checkout 9 | - "run commands to generate the coverage result" 10 | - coverage-reporter/send_report: 11 | coverage-reports: "report.xml,coverage.info" 12 | project-token: $CODACY_PROJECT_TOKEN 13 | version: 2.1 14 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | %date %-16level %-10.-10logger %message %n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/transformation/PathPrefixer.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import com.codacy.api.CoverageReport 4 | 5 | class PathPrefixer(prefix: String) extends Transformation { 6 | 7 | override def execute(report: CoverageReport): CoverageReport = { 8 | val fileReports = report.fileReports.map { fileReport => 9 | fileReport.copy(filename = prefix + fileReport.filename) 10 | } 11 | 12 | report.copy(fileReports = fileReports) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/helpers/FileHelper.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.helpers 2 | 3 | import java.io.{File, PrintWriter} 4 | 5 | import play.api.libs.json._ 6 | 7 | import scala.util.Try 8 | 9 | object FileHelper { 10 | 11 | def writeJsonToFile[A](file: File, value: A)(implicit writes: Writes[A]): Boolean = { 12 | val reportJson = Json.stringify(Json.toJson(value)) 13 | val printWriter = new PrintWriter(file) 14 | val result = Try(printWriter.write(reportJson)).isSuccess 15 | printWriter.close() 16 | result 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Codacy. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /logs/ 2 | project/project/ 3 | project/target/ 4 | target/ 5 | tmp 6 | .history 7 | /dist/ 8 | /.idea 9 | /*.iml 10 | .cache/ 11 | /out/ 12 | /.idea_modules/ 13 | .classpath 14 | .project 15 | /RUNNING_PID 16 | /.settings 17 | .DS_Store 18 | *.iml 19 | codacy.pylint.conf 20 | npm-debug.log 21 | config 22 | .bloop 23 | .metals/ 24 | .vscode/ 25 | .codacy.json 26 | codacy-coverage-reporter-linux-* 27 | codacy-coverage-reporter-darwin-* 28 | classes/ 29 | src/test/resources/codacy-coverage.json 30 | /.codacy-coverage/ 31 | musl.tar.gz 32 | src/graal/bundle 33 | project/metals.sbt 34 | site 35 | .bsp -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/file/GitFileFetcher.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.file 2 | 3 | import com.codacy.api.helpers.vcs.GitClient 4 | 5 | import java.io.File 6 | import scala.util.{Failure, Success} 7 | 8 | class GitFileFetcher { 9 | 10 | def forCommit(commitSha: String): Either[String, Seq[String]] = { 11 | 12 | new GitClient(new File(System.getProperty("user.dir"))).getRepositoryFileNames(commitSha) match { 13 | case Failure(e) => 14 | Left(s"Could not retrieve files from local Git directory, error message: ${e.getMessage}") 15 | case Success(files) => Right(files) 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /api-scala/src/test/scala/com/codacy/api/GitClientTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api 2 | 3 | import com.codacy.api.helpers.vcs.GitClient 4 | import java.nio.file.Paths 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | import org.scalatest.OptionValues._ 8 | 9 | class GitClientTest extends AnyFlatSpec with Matchers { 10 | 11 | "GitClient" should "latestCommitUuid" in { 12 | 13 | val file = Paths.get("").toAbsolutePath.toFile 14 | 15 | val latest: Option[String] = new GitClient(file).latestCommitUuid() 16 | 17 | latest shouldNot be(None) 18 | 19 | latest.value shouldNot be(Symbol("empty")) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/DockerProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Docker provider */ 7 | object DockerProvider extends CommitUUIDProvider { 8 | val name: String = "Docker" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.contains("DOCKER_REPO") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("SOURCE_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/JenkinsProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Jenkins CI provider */ 7 | object JenkinsProvider extends CommitUUIDProvider { 8 | val name: String = "Jenkins CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.contains("JENKINS_URL") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("GIT_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/ArgoCDProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /* Argo CD Provider */ 7 | object ArgoCDProvider extends CommitUUIDProvider { 8 | val name: String = "Argo CD" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.contains("ARGOCD_APP_SOURCE_REPO_URL") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("ARGOCD_APP_REVISION")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/ShippableCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Shippable CI provider */ 7 | object ShippableCIProvider extends CommitUUIDProvider { 8 | val name: String = "Shippable CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("SHIPPABLE").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/SolanoCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Solano CI provider */ 7 | object SolanoCIProvider extends CommitUUIDProvider { 8 | val name: String = "Solano CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("TDDIUM").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("TDDIUM_CURRENT_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/GitlabProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Gitlab CI provider */ 7 | object GitlabProvider extends CommitUUIDProvider { 8 | val name: String = "Gitlab CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.contains("GITLAB_CI") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CI_COMMIT_SHA") orElse environment.get("CI_BUILD_REF")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/GreenhouseCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Greenhouse CI provider */ 7 | object GreenhouseCIProvider extends CommitUUIDProvider { 8 | val name: String = "Greenhouse CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("GREENHOUSE").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("GREENHOUSE_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/AzurePipelinesProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Azure Pipelines provider */ 7 | object AzurePipelinesProvider extends CommitUUIDProvider { 8 | val name: String = "Azure Pipelines" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("TF_BUILD").contains("True") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("BUILD_SOURCEVERSION")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/AWSCodeBuildProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** AWS CodeBuild provider */ 7 | object AWSCodeBuildProvider extends CommitUUIDProvider { 8 | val name: String = "AWS CodeBuild" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CODEBUILD_BUILD_ID").isDefined 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CODEBUILD_RESOLVED_SOURCE_VERSION")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/MagnumCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Magnum CI provider */ 7 | object MagnumCIProvider extends CommitUUIDProvider { 8 | val name: String = "Magnum CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("MAGNUM").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CI_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/CircleCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Circle CI provider */ 7 | object CircleCIProvider extends CommitUUIDProvider { 8 | val name: String = "Circle CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("CIRCLECI").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CIRCLE_SHA1")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/CodefreshCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Codefresh CI provider */ 7 | object CodefreshCIProvider extends CommitUUIDProvider { 8 | val name: String = "Codefresh CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.contains("CF_BUILD_URL") && environment.contains("CF_BUILD_ID") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CF_REVISION")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/WerckerCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Wercker CI provider */ 7 | object WerckerCIProvider extends CommitUUIDProvider { 8 | val name: String = "Wercker CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.contains("WERCKER_GIT_BRANCH") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("WERCKER_GIT_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/util/TextUtils.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.util 2 | 3 | import java.text.NumberFormat 4 | import java.util.Locale 5 | 6 | import scala.util.Try 7 | 8 | object TextUtils { 9 | 10 | def asFloat(str: String): Float = { 11 | Try(str.toFloat).getOrElse { 12 | // The french locale uses the comma as a sep. 13 | val instance = NumberFormat.getInstance(Locale.FRANCE) 14 | val number = instance.parse(str) 15 | number.floatValue() 16 | } 17 | } 18 | 19 | def sanitiseFilename(filename: String): String = { 20 | filename 21 | .replaceAll("""\\/""", "/") // Fix for paths with \/ 22 | .replace("\\", "/") // Fix for paths with \ 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/AppveyorProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Appveyor provider */ 7 | object AppveyorProvider extends CommitUUIDProvider { 8 | val name: String = "Appveyor CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("True") && environment.get("APPVEYOR").contains("True") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("APPVEYOR_REPO_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/CodeshipCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Codeship CI provider */ 7 | object CodeshipCIProvider extends CommitUUIDProvider { 8 | val name: String = "Codeship CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("CI_NAME").contains("codeship") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("CI_COMMIT_ID")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/BitriseCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Bitrise CI provider */ 7 | object BitriseCIProvider extends CommitUUIDProvider { 8 | val name: String = "Bitrise CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("BITRISE_IO").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("GIT_CLONE_COMMIT_HASH")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/BuildkiteCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Buildkite CI provider */ 7 | object BuildkiteCIProvider extends CommitUUIDProvider { 8 | val name: String = "Buildkite CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("BUILDKITE").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("BUILDKITE_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/HerokuCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Heroku CI provider */ 7 | object HerokuCIProvider extends CommitUUIDProvider { 8 | val name: String = "Heroku CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.contains("HEROKU_TEST_RUN_ID") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("HEROKU_TEST_RUN_COMMIT_VERSION")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/SemaphoreCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Semaphore CI provider */ 7 | object SemaphoreCIProvider extends CommitUUIDProvider { 8 | val name: String = "Semaphore CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("SEMAPHORE").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("SEMAPHORE_GIT_SHA")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/DroneCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /* Drone CI Provider */ 7 | object DroneCIProvider extends CommitUUIDProvider { 8 | val name: String = "Drone CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("DRONE").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("DRONE_COMMIT") orElse environment.get("DRONE_COMMIT_SHA")) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/TravisCIProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Travis CI provider */ 7 | object TravisCIProvider extends CommitUUIDProvider { 8 | val name: String = "Travis CI" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | environment.get("CI").contains("true") && environment.get("TRAVIS").contains("true") 12 | } 13 | 14 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 15 | parseEnvironmentVariable(environment.get("TRAVIS_PULL_REQUEST_SHA") orElse environment.get("TRAVIS_COMMIT")) 16 | } 17 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/util/XMLoader.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.util 2 | 3 | import scala.xml.factory.XMLLoader 4 | import javax.xml.parsers.SAXParserFactory 5 | import scala.xml.{Elem, SAXParser} 6 | 7 | object XMLoader extends XMLLoader[Elem] { 8 | 9 | override def parser: SAXParser = { 10 | val f = SAXParserFactory.newInstance() 11 | f.setNamespaceAware(false) 12 | f.setValidating(false) 13 | f.setFeature("http://xml.org/sax/features/namespaces", false) 14 | f.setFeature("http://xml.org/sax/features/validation", false) 15 | f.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false) 16 | f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) 17 | f.newSAXParser() 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/model/configuration/CommitUUIDSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.model.configuration 2 | 3 | import org.scalatest.EitherValues 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class CommitUUIDSpec extends AnyWordSpec with Matchers with EitherValues { 8 | "isValid" should { 9 | "approve commit uuids with 40 hexadecimal chars" in { 10 | CommitUUID.fromString("ce280928adbdc852b8eefb0c57cf400f5f95db01") shouldBe Symbol("right") 11 | CommitUUID.fromString("ce280928adbdc852b8eefb0c57cf400f5f95db0") shouldBe Symbol("left") 12 | CommitUUID.fromString("ce280928adbdc852b8eefb0c57cf400f5f95db012") shouldBe Symbol("left") 13 | CommitUUID.fromString("40 random characters that are not hexa!!") shouldBe Symbol("left") 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/CoverageReport.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api 2 | 3 | import play.api.libs.json.{JsNumber, JsObject, Json, Writes} 4 | 5 | case class CoverageFileReport(filename: String, coverage: Map[Int, Int]) 6 | 7 | case class CoverageReport(fileReports: Seq[CoverageFileReport]) 8 | 9 | object CoverageReport { 10 | implicit val mapWrites: Writes[Map[Int, Int]] = Writes[Map[Int, Int]] { (map: Map[Int, Int]) => 11 | JsObject(map.map { 12 | case (key, value) => (key.toString, JsNumber(value)) 13 | }) 14 | } 15 | implicit val coverageFileReportWrites: Writes[CoverageFileReport] = Json.writes[CoverageFileReport] 16 | implicit val coverageReportWrites: Writes[CoverageReport] = Json.writes[CoverageReport] 17 | } 18 | 19 | object OrganizationProvider extends Enumeration { 20 | val manual, gh, bb, ghe, bbe, gl, gle = Value 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/BitbucketCloudProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** Bitbucket Cloud Pipeline provider */ 7 | object BitbucketCloudProvider extends CommitUUIDProvider { 8 | val name: String = "Bitbucket Cloud Pipeline" 9 | 10 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 11 | // CI is a bit generic and could be used by other CI Providers as well 12 | // Check on Bitbucket Build Number as well 13 | environment.contains("CI") && environment.contains("BITBUCKET_BUILD_NUMBER") 14 | } 15 | 16 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = 17 | parseEnvironmentVariable(environment.get("BITBUCKET_COMMIT")) 18 | } 19 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Api/Api.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/CodacyPhpCoverage.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/client/RequestResponse.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.client 2 | 3 | sealed trait RequestResponse[+A] 4 | 5 | case class SuccessfulResponse[A](value: A) extends RequestResponse[A] 6 | 7 | case class FailedResponse(message: String) extends RequestResponse[Nothing] 8 | 9 | object RequestResponse { 10 | 11 | def success[A](a: A): RequestResponse[A] = SuccessfulResponse(a) 12 | 13 | def failure[A](message: String): RequestResponse[A] = FailedResponse(message: String) 14 | 15 | def apply[A](r1: RequestResponse[Seq[A]], r2: RequestResponse[Seq[A]]): RequestResponse[Seq[A]] = { 16 | r1 match { 17 | case SuccessfulResponse(v1) => 18 | r2 match { 19 | case SuccessfulResponse(v2) => 20 | SuccessfulResponse(v1 ++ v2) 21 | case f @ FailedResponse(_) => f 22 | } 23 | case f @ FailedResponse(_) => f 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "1.5.1" 2 | style = IntelliJ 3 | 4 | align = none 5 | assumeStandardLibraryStripMargin = false 6 | binPack.literalArgumentLists = true 7 | binPack.parentConstructors = false 8 | continuationIndent.defnSite = 4 9 | danglingParentheses = true 10 | docstrings = ScalaDoc 11 | includeCurlyBraceInSelectChains = true 12 | lineEndings = unix 13 | maxColumn = 120 14 | newlines.alwaysBeforeTopLevelStatements = true 15 | newlines.penalizeSingleSelectMultiArgList = false 16 | newlines.sometimesBeforeColonInMethodReturnType = true 17 | optIn.breakChainOnFirstMethodDot = true 18 | project.git = true 19 | rewrite.rules = [ SortImports, PreferCurlyFors ] 20 | spaces.afterKeywordBeforeParen = true 21 | 22 | project.includeFilters = [".*\\.sbt$", ".*\\.scala$"] 23 | project.excludeFilters = [".*\\.scala.html$", "target/.*", "modules/admin/target/.*"] 24 | onTestFailure = "To fix this, run `scalafmt` within sbt or `sbt scalafmt` on the project base directory" 25 | -------------------------------------------------------------------------------- /graalvm/build-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e +o pipefail 4 | 5 | if [ "$ARCH" == "arm" ]; 6 | then 7 | mkdir -p $HOME/.gcc 8 | cd $HOME/.gcc 9 | curl https://ftp.gnu.org/gnu/gcc/gcc-11.2.0/gcc-11.2.0.tar.xz --output gcc.tgz 10 | tar -xf gcc.tgz 11 | TOOLCHAIN_DIR=$HOME/.gcc/gcc-11.2.0 12 | else 13 | mkdir -p $HOME/.musl 14 | cd $HOME/.musl 15 | echo "http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz --output musl.tgz" 16 | curl http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz --output musl.tgz 17 | tar -xf musl.tgz 18 | TOOLCHAIN_DIR=$HOME/.musl/x86_64-linux-musl-native 19 | export CC=$TOOLCHAIN_DIR/bin/gcc 20 | fi 21 | 22 | 23 | zlib='zlib-1.2.13' 24 | zlibtargz=$zlib.tar.gz 25 | curl https://zlib.net/fossils/$zlibtargz --output $zlibtargz 26 | tar -xf $zlibtargz 27 | 28 | ( 29 | cd $zlib 30 | ./configure --prefix=$TOOLCHAIN_DIR --static 31 | make -j "$(nproc)" 32 | sudo make install 33 | ) 34 | 35 | echo "$TOOLCHAIN_DIR/bin" -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/TeamCityProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import com.codacy.model.configuration.CommitUUID 4 | import com.codacy.rules.commituuid.CommitUUIDProvider 5 | 6 | /** TeamCity CI provider */ 7 | object TeamCityProvider extends CommitUUIDProvider { 8 | val name: String = "TeamCity CI" 9 | 10 | override val commitNotFoundMessage: String = s"""Can't find $name commit UUID in the environment. 11 | |TEAMCITY_BUILD_COMMIT or BUILD_VCS_NUMBER is not set. Add this to your config: 12 | | env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number% 13 | |""".stripMargin 14 | 15 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 16 | environment.contains("TEAMCITY_VERSION") 17 | } 18 | 19 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = { 20 | parseEnvironmentVariable(environment.get("TEAMCITY_BUILD_COMMIT") orElse environment.get("BUILD_VCS_NUMBER")) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/di/Components.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.di 2 | 3 | import com.codacy.api.client.CodacyClient 4 | import com.codacy.api.service.CoverageServices 5 | import com.codacy.model.configuration.{ApiTokenAuthenticationConfig, Configuration, ProjectTokenAuthenticationConfig} 6 | import com.codacy.rules.ReportRules 7 | import com.codacy.rules.file.GitFileFetcher 8 | 9 | class Components(private val validatedConfig: Configuration) { 10 | lazy val reportRules = new ReportRules(coverageServices, new GitFileFetcher) 11 | 12 | lazy private val (projectToken, apiToken) = validatedConfig.baseConfig.authentication match { 13 | case ProjectTokenAuthenticationConfig(projectToken) => 14 | (Some(projectToken), None) 15 | case ApiTokenAuthenticationConfig(apiToken, _, _, _) => 16 | (None, Some(apiToken)) 17 | } 18 | 19 | lazy val codacyClient = new CodacyClient( 20 | Some(validatedConfig.baseConfig.codacyApiBaseUrl), 21 | apiToken, 22 | projectToken, 23 | validatedConfig.baseConfig.skipSslVerification 24 | ) 25 | 26 | lazy val coverageServices = new CoverageServices(codacyClient) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/transformation/PathPrefixerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import com.codacy.api.{CoverageFileReport, CoverageReport} 4 | import org.scalatest._ 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class PathPrefixerSpec extends AnyWordSpec with Matchers { 9 | 10 | val report = CoverageReport( 11 | Seq(CoverageFileReport("Filename.scala", Map.empty), CoverageFileReport("OtherFile.scala", Map.empty)) 12 | ) 13 | 14 | "PathPrefixer" should { 15 | "prefix all filenames in a report" in { 16 | val prefixer = new PathPrefixer("folder/") 17 | 18 | prefixer.execute(report).fileReports.foreach { fileReport => 19 | fileReport.filename should startWith("folder/") 20 | } 21 | } 22 | } 23 | 24 | it should { 25 | "do nothing if no prefix is specified" in { 26 | val prefixer = new PathPrefixer("") 27 | 28 | prefixer.execute(report).fileReports.map(_.filename) should contain("Filename.scala") 29 | prefixer.execute(report).fileReports.map(_.filename) should contain("OtherFile.scala") 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/test-paths-with-different-paths.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 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/transformation/GitFileNameUpdaterAndFilter.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | import com.codacy.api.CoverageReport 3 | import com.codacy.transformation.FileNameMatcher.getFilenameFromPath 4 | import wvlet.log.LogSupport 5 | 6 | class GitFileNameUpdaterAndFilter(acceptableFileNamesMap: Map[String, Seq[String]]) 7 | extends Transformation 8 | with LogSupport { 9 | override def execute(report: CoverageReport): CoverageReport = { 10 | val fileReports = for { 11 | fileReport <- report.fileReports 12 | fileName <- matchAndReturnName(fileReport.filename) 13 | 14 | newFileReport = fileReport.copy(filename = fileName) 15 | } yield newFileReport 16 | report.copy(fileReports = fileReports) 17 | } 18 | 19 | private def matchAndReturnName(filename: String): Option[String] = { 20 | val maybeFilename = FileNameMatcher 21 | .matchAndReturnName(filename, acceptableFileNamesMap.getOrElse(getFilenameFromPath(filename), Seq.empty)) 22 | 23 | if (maybeFilename.isEmpty) 24 | logger 25 | .warn(s"File: Ignoring $filename for coverage calculation. No matching file found in the repository.") 26 | 27 | maybeFilename 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/transformation/FileNameMatcherSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import org.scalatest.wordspec.AnyWordSpec 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.OptionValues 6 | 7 | class FileNameMatcherSpec extends AnyWordSpec with Matchers with OptionValues { 8 | 9 | "matchAndReturnName" should { 10 | "return name from closest match" in { 11 | // ARRANGE 12 | val filenames = Seq("src/folder/package/file.txt", "src/folder/package/another-package/file.txt") 13 | val filename = "package/file.txt" 14 | 15 | // ACT 16 | val newFilename = FileNameMatcher.matchAndReturnName(filename, filenames) 17 | 18 | // ASSERT 19 | newFilename.isDefined shouldBe true 20 | newFilename.value shouldBe "src/folder/package/file.txt" 21 | } 22 | 23 | "return empty when name doesn't match any filenames" in { 24 | // ARRANGE 25 | val filenames = Seq("src/folder/package/file.txt", "src/folder/package/another-package/file.txt") 26 | val filename = "another-package/non-existent-file.txt" 27 | 28 | // ACT 29 | val newFilename = FileNameMatcher.matchAndReturnName(filename, filenames) 30 | 31 | // ASSERT 32 | newFilename.isDefined shouldBe false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/XmlReportParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.CoverageReport 6 | import com.codacy.parsers.util.XMLoader 7 | 8 | import scala.util.{Failure, Success, Try} 9 | import scala.xml.{Elem, NodeSeq} 10 | 11 | trait XmlReportParser { 12 | def validateSchema(xml: Elem): Boolean 13 | 14 | def getRootNode(xml: Elem): NodeSeq 15 | 16 | def loadXmlReport(reportFile: File, schemaErrorMessage: String): Either[String, NodeSeq] = { 17 | Try(XMLoader.loadFile(reportFile)) match { 18 | case Success(xml) if validateSchema(xml) => 19 | Right(getRootNode(xml)) 20 | 21 | case Success(_) => 22 | Left(s"Invalid report. $schemaErrorMessage.") 23 | 24 | case Failure(ex) => 25 | Left(s"Unparseable report. ${ex.getMessage}") 26 | } 27 | } 28 | 29 | private def getFailedParseMessage(ex: Throwable) = 30 | s"Failed to parse report with error: ${ex.getMessage}" 31 | 32 | def parseReport(reportFile: File, schemaErrorMessage: String)( 33 | parseReport: NodeSeq => Either[String, CoverageReport] 34 | ): Either[String, CoverageReport] = { 35 | loadXmlReport(reportFile, schemaErrorMessage) 36 | .flatMap { report => 37 | Try(parseReport(report)) match { 38 | case Success(coverageReport) => coverageReport 39 | case Failure(ex) => Left(getFailedParseMessage(ex)) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/go/original_package.out: -------------------------------------------------------------------------------- 1 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:31.101,35.16 3 1 2 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:39.2,39.41 1 1 3 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:47.2,47.41 1 1 4 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:56.2,58.71 2 1 5 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:62.2,62.18 1 1 6 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:35.16,38.3 2 1 7 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:39.41,46.3 2 1 8 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:47.41,54.3 2 1 9 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:58.71,60.3 1 1 10 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:70.104,72.9 2 1 11 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:76.2,76.60 1 1 12 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:81.2,82.16 2 1 13 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:92.2,92.15 1 1 14 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:96.2,96.12 1 1 15 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:72.9,74.3 1 0 16 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:76.60,78.3 1 1 17 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:82.16,90.3 2 1 18 | github.com/codacy/pulse/modules/entity/src/internal/api/api.go:92.15,94.3 1 1 -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_clover_with_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/go/changed_package_name.out: -------------------------------------------------------------------------------- 1 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:31.101,35.16 3 1 2 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:39.2,39.41 1 1 3 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:47.2,47.41 1 1 4 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:56.2,58.71 2 1 5 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:62.2,62.18 1 1 6 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:35.16,38.3 2 1 7 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:39.41,46.3 2 1 8 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:47.41,54.3 2 1 9 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:58.71,60.3 1 1 10 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:70.104,72.9 2 1 11 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:76.2,76.60 1 1 12 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:81.2,82.16 2 1 13 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:92.2,92.15 1 1 14 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:96.2,96.12 1 1 15 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:72.9,74.3 1 0 16 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:76.60,78.3 1 1 17 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:82.16,90.3 2 1 18 | github.com/codacy/pulse/modules/entity/src/internal/apiv1/api.go:92.15,94.3 1 1 -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/thousand_sep_cobertura.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/windows_paths_cobertura.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 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/transformation/FileNameMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import java.nio.file.Paths 4 | import scala.util.Try 5 | 6 | object FileNameMatcher { 7 | 8 | def matchAndReturnName(filename: String, fileNames: Seq[String]): Option[String] = { 9 | fileNames 10 | .filter(name => isTheSameFile(filename.toLowerCase, name.toLowerCase)) 11 | .sortBy(name => Math.abs(filename.length - name.length)) 12 | .headOption 13 | } 14 | 15 | def getFilenameFromPath(filename: String): String = { 16 | Try(Paths.get(filename).getFileName.toString.toLowerCase).getOrElse(filename.toLowerCase) 17 | } 18 | 19 | private def normalizePath(path: String): String = { 20 | path.replace("\\", "/") 21 | } 22 | 23 | private def haveSameName(file: String, covFile: String): Boolean = 24 | getFilenameFromPath(file) == getFilenameFromPath(covFile) 25 | 26 | private def haveSamePath(file: String, covFile: String): Boolean = 27 | normalizePath(file) == normalizePath(covFile) 28 | 29 | private def fileEndsWithReportPath(file: String, covFile: String): Boolean = 30 | normalizePath(file).endsWith(normalizePath(covFile)) 31 | 32 | private def reportEndsWithFilePath(file: String, covFile: String): Boolean = 33 | normalizePath(covFile).endsWith(normalizePath(file)) 34 | 35 | private def isTheSameFile(file: String, covFile: String): Boolean = { 36 | haveSameName(file, covFile) && (haveSamePath(file, covFile) || 37 | fileEndsWithReportPath(file, covFile) || 38 | reportEndsWithFilePath(file, covFile)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/JacocoParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api._ 6 | import com.codacy.parsers.implementation.JacocoParser 7 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 8 | import org.scalatest.wordspec.AnyWordSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class JacocoParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 12 | 13 | "JacocoParser" should { 14 | 15 | "identify if report is invalid" in { 16 | val reader = 17 | JacocoParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_cobertura.xml")) 18 | reader.isLeft shouldBe true 19 | } 20 | 21 | "identify if report is valid" in { 22 | val reader = 23 | JacocoParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_jacoco.xml")) 24 | reader.isRight shouldBe true 25 | } 26 | 27 | "return a valid report" in { 28 | 29 | val reader = JacocoParser 30 | .parse(new File("."), new File("coverage-parser/src/test/resources/test_jacoco.xml")) 31 | 32 | val testReport = CoverageReport( 33 | List( 34 | CoverageFileReport( 35 | "org/eluder/coverage/sample/InnerClassCoverage.java", 36 | Map(10 -> 1, 6 -> 1, 9 -> 1, 13 -> 1, 22 -> 1, 27 -> 0, 12 -> 1, 3 -> 1, 16 -> 1, 26 -> 0, 19 -> 1) 37 | ), 38 | CoverageFileReport("org/eluder/coverage/sample/SimpleCoverage.java", Map(3 -> 1, 6 -> 1, 10 -> 0, 11 -> 0)) 39 | ) 40 | ) 41 | 42 | reader.value should equal(testReport) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/CoverageParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | import com.codacy.parsers.implementation._ 5 | 6 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 7 | import org.scalatest.wordspec.AnyWordSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class CoverageParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 11 | private val coberturaReportPath = "coverage-parser/src/test/resources/test_cobertura.xml" 12 | private val cloverReportPath = "coverage-parser/src/test/resources/test_clover.xml" 13 | 14 | "parse" should { 15 | "return the specific error" when { 16 | "the file cannot be parsed with a specific parser" in { 17 | val reader = CoverageParser.parse(new File("."), new File(coberturaReportPath), Some(CloverParser)) 18 | 19 | reader shouldBe Symbol("left") 20 | } 21 | "the file cannot be parsed with another specific parser" in { 22 | val reader = CoverageParser.parse(new File("."), new File(coberturaReportPath), Some(LCOVParser)) 23 | 24 | reader shouldBe Symbol("left") 25 | } 26 | } 27 | "return a valid result" when { 28 | "file and format are matching cobertura" in { 29 | val reader = 30 | CoverageParser.parse(new File("."), new File(coberturaReportPath), Some(CoberturaParser)) 31 | 32 | reader shouldBe Symbol("right") 33 | } 34 | "file and format are matching clover" in { 35 | val reader = CoverageParser.parse(new File("."), new File(cloverReportPath), Some(CloverParser)) 36 | 37 | reader shouldBe Symbol("right") 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Git/GitClient.php.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_cobertura.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 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/CodacyCoverageReporter.scala: -------------------------------------------------------------------------------- 1 | package com.codacy 2 | 3 | import com.codacy.configuration.parser.{CommandConfiguration, ConfigurationParsingApp} 4 | import com.codacy.di.Components 5 | import com.codacy.model.configuration.{Configuration, FinalConfig, ReportConfig} 6 | import com.codacy.rules.ConfigurationRules 7 | import wvlet.airframe.log 8 | import wvlet.log.{LogSupport, Logger} 9 | 10 | object CodacyCoverageReporter extends ConfigurationParsingApp with LogSupport { 11 | log.initNoColor 12 | 13 | def run(commandConfig: CommandConfiguration): Int = { 14 | val configRules = new ConfigurationRules(commandConfig, sys.env) 15 | 16 | val noAvailableTokens = 17 | configRules.getProjectToken(commandConfig.baseConfig).isEmpty && 18 | configRules.getApiToken(commandConfig.baseConfig).isEmpty 19 | if (commandConfig.baseConfig.skipValue && noAvailableTokens) { 20 | logger.info("Skip reporting coverage") 21 | 0 22 | } else { 23 | sendReport(configRules.validatedConfig) match { 24 | case Right(message) => 25 | logger.info(message) 26 | 0 27 | case Left(message) => 28 | logger.error(message) 29 | 1 30 | } 31 | } 32 | } 33 | 34 | private def sendReport(validatedConfig: Either[String, Configuration]) = { 35 | validatedConfig.flatMap { validatedConfig => 36 | val components = new Components(validatedConfig) 37 | 38 | if (validatedConfig.baseConfig.debug) { 39 | Logger("com.codacy").setLogLevel(wvlet.log.LogLevel.DEBUG) 40 | } 41 | 42 | validatedConfig match { 43 | case config: ReportConfig => 44 | components.reportRules.codacyCoverage(config) 45 | 46 | case config: FinalConfig => 47 | components.reportRules.finalReport(config) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/rules/commituuid/CommitUUIDProviderSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid 2 | 3 | import org.scalatest._ 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class CommitUUIDProviderSpec extends AnyWordSpec with Matchers with EitherValues { 8 | "getFromEnvironment" should { 9 | "provide a valid commit uuid" in { 10 | val envVars = 11 | Map("JENKINS_URL" -> "https://jenkins.example.com/", "GIT_COMMIT" -> "ad7ce1b9973d31a2794565f892b6ae4cab575d7c") 12 | val commitUuid = CommitUUIDProvider.getFromEnvironment(envVars) 13 | 14 | commitUuid should be(Symbol("right")) 15 | commitUuid.value.value should be("ad7ce1b9973d31a2794565f892b6ae4cab575d7c") 16 | } 17 | 18 | "provide the first valid commit uuid, in provider order" in { 19 | val envVars = 20 | Map( 21 | "JENKINS_URL" -> "https://jenkins.example.com/", 22 | "GIT_COMMIT" -> "ad7ce1b9973d31a2794565f892b6ae4cab575d7c", 23 | "GITLAB_CI" -> "true", 24 | "CI_COMMIT_SHA" -> "1b097ecbbdd0204f908087d6fe1b94dc3453eaf9" 25 | ) 26 | val commitUuid = CommitUUIDProvider.getFromEnvironment(envVars) 27 | 28 | commitUuid should be(Symbol("right")) 29 | commitUuid.value.value should be("1b097ecbbdd0204f908087d6fe1b94dc3453eaf9") 30 | } 31 | 32 | "not provide a commit uuid if the environment is empty" in { 33 | val commitUuid = CommitUUIDProvider.getFromEnvironment(Map.empty) 34 | commitUuid should be(Symbol("left")) 35 | } 36 | 37 | "not provide a commit uuid if the environment has no valid commits" in { 38 | val envVars = 39 | Map("JENKINS_URL" -> "https://jenkins.example.com/", "GIT_COMMIT" -> "Commit UUID") 40 | val commitUuid = CommitUUIDProvider.getFromEnvironment(envVars) 41 | 42 | commitUuid should be(Symbol("left")) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /project/Platform.scala: -------------------------------------------------------------------------------- 1 | object Platform { 2 | sealed abstract class OS(val string: String) extends Product with Serializable 3 | 4 | object OS { 5 | case object Windows extends OS("windows") 6 | case object MacOS extends OS("osx") 7 | case object Linux extends OS("linux") 8 | case object Unknown extends OS("unknown") 9 | 10 | val all: List[OS] = List(Windows, MacOS, Linux, Unknown) 11 | 12 | def detect(osNameProp: String): OS = normalise(osNameProp) match { 13 | case p if p.startsWith("linux") => OS.Linux 14 | case p if p.startsWith("windows") => OS.Windows 15 | case p if p.startsWith("osx") || p.startsWith("macosx") => OS.MacOS 16 | case _ => OS.Unknown 17 | } 18 | } 19 | 20 | sealed abstract class Arch extends Product with Serializable {} 21 | 22 | object Arch { 23 | case object Intel extends Arch {} 24 | case object Arm extends Arch {} 25 | 26 | val all: List[Arch] = List(Intel, Arm) 27 | 28 | def detect(osArchProp: String): Arch = normalise(osArchProp) match { 29 | case "amd64" | "x64" | "x8664" | "x86" => Intel 30 | case "aarch64" | "arm64" => Arm 31 | } 32 | } 33 | 34 | sealed abstract class Bits extends Product with Serializable 35 | 36 | object Bits { 37 | case object x32 extends Bits 38 | case object x64 extends Bits 39 | 40 | def detect(sunArchProp: String): Bits = 41 | sunArchProp match { 42 | case "64" => x64 43 | case "32" => x32 44 | } 45 | } 46 | 47 | case class Target(os: OS, arch: Arch, bits: Bits) 48 | 49 | lazy val os: OS = OS.detect(sys.props.getOrElse("os.name", "")) 50 | lazy val arch: Arch = Arch.detect(sys.props.getOrElse("os.arch", "")) 51 | lazy val bits: Bits = Bits.detect(sys.props.getOrElse("sun.arch.data.model", "")) 52 | lazy val target: Target = Target(os, arch, bits) 53 | 54 | private def normalise(s: String) = 55 | s.toLowerCase(java.util.Locale.US).replaceAll("[^a-z0-9]+", "") 56 | 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Are you looking for help? 2 | 3 | This is an issue tracker, used to manage and track the development of this [Codacy](https://www.codacy.com/) project. 4 | 5 | It is not a platform support system. If think your problem is related with our platform at https://www.codacy.com/, please contact us through our [contact form](https://www.codacy.com/contact) or our internal chat application, visible after you login on the bottom right corner. 6 | 7 | Keep in mind that this issue tracker is for specific problems of this project. 8 | 9 | ### Scala Version (2.10.x / etc) 10 | 11 | 12 | ### Operating System (Ubuntu 15.10 / MacOS 10.10 / Windows 10) 13 | 14 | Use `uname -a` if on Linux. 15 | 16 | ### JDK (Oracle 1.8.0_72, OpenJDK 1.8.x, Azul Zing) 17 | 18 | Paste the output from `java -version` at the command line. 19 | 20 | ### Library Dependencies 21 | 22 | If this is an issue that involves integration with another system, include the exact version and OS of the other system, including any intermediate drivers or APIs i.e. if you connect to a PostgreSQL database, include both the version / OS of PostgreSQL and the JDBC driver version used to connect to the database. 23 | 24 | ### Expected Behavior 25 | 26 | Please describe the expected behavior of the issue, starting from the first action. 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 32 | ### Actual Behavior 33 | 34 | Please provide a description of what actually happens, working from the same starting point. 35 | 36 | Be descriptive: "it doesn't work" does not describe what the behavior actually is -- instead, say "when sending the coverage with the command (...) it returns the output error (...)" 37 | 38 | 1. 39 | 2. 40 | 3. 41 | 42 | ### Reproducible Test Case 43 | 44 | Please provide a some information on how to reproduce the bug. A PR with a failing test would be awesome, if possible. 45 | 46 | If the issue is more complex or requires configuration, please provide a link to a project on Github/Codacy that reproduces the issue. 47 | -------------------------------------------------------------------------------- /orbs/commands/send_report.yml: -------------------------------------------------------------------------------- 1 | description: "Download Codacy's coverage reporter and run it" 2 | 3 | parameters: 4 | tool_version: 5 | type: string 6 | default: '' 7 | description: "Specify Codacy's coverage reporter tool version" 8 | project-token: 9 | type: string 10 | default: ${CODACY_PROJECT_TOKEN} 11 | description: Specify Codacy's project token 12 | coverage-reports: 13 | type: string 14 | description: 'Optional comma separated list of coverage reports to send to Codacy' 15 | default: '' 16 | skip: 17 | type: boolean 18 | description: Skip if token isn't defined. Useful to let forks CI pass without passing secrets 19 | default: false 20 | 21 | steps: 22 | - run: 23 | name: Upload Coverage Results to Codacy 24 | command: | 25 | export CODACY_REPORTER_VERSION=<< parameters.tool_version >> 26 | 27 | export CODACY_PROJECT_TOKEN=<< parameters.project-token >> 28 | # comma separated list of report files 29 | # reference from https://unix.stackexchange.com/a/191125 30 | # plus https://stackoverflow.com/a/20323801/7898052 31 | report_array=$(printf "<< parameters.coverage-reports >>" | cut -d',' -f1) 32 | 33 | params='' 34 | for report in $report_array 35 | do 36 | if [ ! -z "$report" ] 37 | then 38 | params="$params -r $report" 39 | fi 40 | done 41 | 42 | if << parameters.skip >>; then 43 | skip_option="--skip" 44 | else 45 | skip_option="" 46 | fi 47 | 48 | 49 | if [ -x "$(which curl)" ]; then 50 | curl -Ls https://coverage.codacy.com/get.sh > get.sh 51 | elif [ -x "$(which wget)" ] ; then 52 | wget -qO - https://coverage.codacy.com/get.sh > get.sh 53 | else 54 | printf "Could not find curl or wget, please install one." 55 | fi 56 | 57 | source get.sh report $params --partial $skip_option &&\ 58 | source get.sh final $skip_option 59 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/LCOVParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api._ 6 | import com.codacy.parsers.implementation.LCOVParser 7 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 8 | import org.scalatest.wordspec.AnyWordSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class LCOVParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 12 | 13 | "LCOVParser" should { 14 | 15 | "identify if report is invalid" in { 16 | val reader = LCOVParser.parse(new File("."), new File("coverage-parser/src/test/resources/invalid_report.lcov")) 17 | reader.isLeft shouldBe true 18 | } 19 | 20 | "identify if report is invalid beacuse is cobertura format" in { 21 | val reader = LCOVParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_cobertura.xml")) 22 | reader.isLeft shouldBe true 23 | } 24 | 25 | "identify if report is invalid beacuse is clover format" in { 26 | val reader = LCOVParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_clover.xml")) 27 | reader.isLeft shouldBe true 28 | } 29 | 30 | "identify if report is valid" in { 31 | val reader = LCOVParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_lcov.lcov")) 32 | reader.isRight shouldBe true 33 | } 34 | 35 | "return a valid report" in { 36 | val reader = LCOVParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_lcov.lcov")) 37 | 38 | val testReport = CoverageReport( 39 | List( 40 | CoverageFileReport("coverage-parser/src/test/resources/TestSourceFile2.scala", Map(1 -> 1, 2 -> 1, 3 -> 1)), 41 | CoverageFileReport( 42 | "coverage-parser/src/test/resources/TestSourceFile.scala", 43 | Map(3 -> 0, 4 -> 1, 5 -> 1, 6 -> 2) 44 | ) 45 | ) 46 | ) 47 | 48 | reader.value should equal(testReport) 49 | } 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/OpenCoverParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.implementation.OpenCoverParser 7 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 8 | import org.scalatest.wordspec.AnyWordSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class OpenCoverParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 12 | private val openCoverReportPath = "coverage-parser/src/test/resources/test_opencover.xml" 13 | private val nonExistentReportPath = "coverage-parser/src/test/resources/non_existent.xml" 14 | private val coberturaReportPath = "coverage-parser/src/test/resources/test_cobertura.xml" 15 | "parse" should { 16 | "return an invalid report" when { 17 | "report file does not exist" in { 18 | val reader = OpenCoverParser.parse(new File("."), new File(nonExistentReportPath)) 19 | reader shouldBe Symbol("left") 20 | } 21 | 22 | "report file has a different format" in { 23 | val reader = OpenCoverParser.parse(new File("."), new File(coberturaReportPath)) 24 | reader shouldBe Symbol("left") 25 | } 26 | } 27 | 28 | "return a valid report" in { 29 | val reader = OpenCoverParser.parse(new File("."), new File(openCoverReportPath)) 30 | reader shouldBe Symbol("right") 31 | } 32 | 33 | "return the expected files" in { 34 | val reader = OpenCoverParser.parse(new File("."), new File(openCoverReportPath)) 35 | 36 | reader.value.fileReports.map(_.filename).sorted shouldBe Seq("bar.cs", "foo.cs", "foobar.cs").sorted 37 | } 38 | 39 | "return the expected report" in { 40 | val reader = OpenCoverParser.parse(new File("."), new File(openCoverReportPath)) 41 | 42 | reader.value shouldBe CoverageReport( 43 | List( 44 | CoverageFileReport("foo.cs", Map(10 -> 1)), 45 | CoverageFileReport("bar.cs", Map(10 -> 0)), 46 | CoverageFileReport("foobar.cs", Map(10 -> 0, 20 -> 1)) 47 | ) 48 | ) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/helpers/vcs/GitClient.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.helpers.vcs 2 | 3 | import java.io.File 4 | import java.util.Date 5 | import org.eclipse.jgit.api.Git 6 | import org.eclipse.jgit.lib.{Repository, RepositoryBuilder} 7 | import org.eclipse.jgit.revwalk.RevWalk 8 | import org.eclipse.jgit.treewalk.TreeWalk 9 | 10 | import scala.collection.mutable.ListBuffer 11 | import scala.jdk.CollectionConverters.* 12 | import scala.util.Try 13 | 14 | case class CommitInfo(uuid: String, authorName: String, authorEmail: String, date: Date) 15 | 16 | class GitClient(workDirectory: File) { 17 | 18 | val repositoryTry: Try[Repository] = Try(new RepositoryBuilder().findGitDir(workDirectory).readEnvironment().build()) 19 | 20 | val repository: Option[Repository] = repositoryTry.toOption 21 | 22 | def latestCommitUuid(): Option[String] = { 23 | repositoryTry 24 | .map { rep => 25 | val git = new Git(rep) 26 | val headRev = git.log().setMaxCount(1).call().asScala.head 27 | headRev.getName 28 | } 29 | .toOption 30 | .filter(_.trim.nonEmpty) 31 | } 32 | 33 | def latestCommitInfo: Try[CommitInfo] = { 34 | repositoryTry.map { rep => 35 | val git = new Git(rep) 36 | val headRev = git.log().setMaxCount(1).call().asScala.head 37 | val authorIdent = headRev.getAuthorIdent 38 | 39 | CommitInfo(headRev.getName, authorIdent.getName, authorIdent.getEmailAddress, authorIdent.getWhen) 40 | } 41 | } 42 | 43 | def getRepositoryFileNames(commitSha: String): Try[Seq[String]] = { 44 | repositoryTry.map { rep => 45 | val git = new Git(rep) 46 | val repo = git.getRepository 47 | val commitId = repo.resolve(commitSha) 48 | val revWalk = new RevWalk(repo) 49 | val commit = revWalk.parseCommit(commitId) 50 | val tree = commit.getTree 51 | val treeWalk = new TreeWalk(repo) 52 | treeWalk.addTree(tree) 53 | treeWalk.setRecursive(true) 54 | 55 | val buffer = new ListBuffer[String] 56 | 57 | while (treeWalk.next) { 58 | buffer.addOne(treeWalk.getPathString) 59 | } 60 | buffer.toList 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/CoverageParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.CoverageReport 6 | import com.codacy.parsers.implementation._ 7 | 8 | import scala.util.Try 9 | 10 | trait CoverageParser { 11 | val name: String 12 | 13 | def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] 14 | } 15 | 16 | object CoverageParser { 17 | 18 | final case class CoverageParserResult(report: CoverageReport, parser: CoverageParser) 19 | 20 | val allParsers: List[CoverageParser] = 21 | List( 22 | CoberturaParser, 23 | JacocoParser, 24 | CloverParser, 25 | OpenCoverParser, 26 | DotcoverParser, 27 | PhpUnitXmlParser, 28 | LCOVParser, 29 | GoParser 30 | ) 31 | 32 | def parse(projectRoot: File, reportFile: File): Either[String, CoverageReport] = { 33 | parse(projectRoot = projectRoot, reportFile = reportFile, None).map(_.report) 34 | } 35 | 36 | def parse( 37 | projectRoot: File, 38 | reportFile: File, 39 | forceParser: Option[CoverageParser] 40 | ): Either[String, CoverageParserResult] = { 41 | val isEmptyReport = { 42 | // Just starting by detecting the simplest case: a single report file 43 | Try(reportFile.isFile && reportFile.length() == 0).getOrElse(false) 44 | } 45 | 46 | val parsers = forceParser match { 47 | case Some(parser) => List(parser) 48 | case _ => allParsers 49 | } 50 | 51 | object ParsedCoverage { 52 | def unapply(parser: CoverageParser): Option[CoverageParserResult] = { 53 | parser.parse(projectRoot, reportFile).toOption.map(CoverageParserResult(_, parser)) 54 | } 55 | } 56 | 57 | if (isEmptyReport) { 58 | Left(s"Report file ${reportFile.getCanonicalPath} is empty") 59 | } else { 60 | parsers.view 61 | .collectFirst { 62 | case ParsedCoverage(value: CoverageParserResult) => Right(value) 63 | } 64 | .getOrElse( 65 | Left(s"Could not parse report, unrecognized report format (tried: ${parsers.map(_.name).mkString(", ")})") 66 | ) 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/CoverageParserFactoryTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import org.scalatest.{BeforeAndAfterAll} 7 | import org.scalatest.wordspec.AnyWordSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class CoverageParserFactoryTest extends AnyWordSpec with BeforeAndAfterAll with Matchers { 11 | 12 | "CoverageParserFactory" should { 13 | 14 | "get report with unspecified parser" in { 15 | val expectedReport = CoverageReport( 16 | List( 17 | CoverageFileReport( 18 | "coverage-parser/src/test/resources/TestSourceFile.scala", 19 | Map(5 -> 1, 10 -> 1, 6 -> 2, 9 -> 1, 3 -> 0, 4 -> 1, 7 -> 1, 8 -> 3, 9 -> Int.MaxValue) 20 | ), 21 | CoverageFileReport("coverage-parser/src/test/resources/TestSourceFile2.scala", Map(1 -> 1, 2 -> 1, 3 -> 1)) 22 | ) 23 | ) 24 | 25 | CoverageParser 26 | .parse(new File("."), new File("coverage-parser/src/test/resources/test_cobertura.xml")) shouldEqual Right( 27 | expectedReport 28 | ) 29 | } 30 | 31 | "get report with jacoco parser" in { 32 | val expectedReport = CoverageReport( 33 | List( 34 | CoverageFileReport( 35 | "org/eluder/coverage/sample/InnerClassCoverage.java", 36 | Map(10 -> 1, 6 -> 1, 9 -> 1, 13 -> 1, 22 -> 1, 27 -> 0, 12 -> 1, 3 -> 1, 16 -> 1, 26 -> 0, 19 -> 1) 37 | ), 38 | CoverageFileReport("org/eluder/coverage/sample/SimpleCoverage.java", Map(3 -> 1, 6 -> 1, 10 -> 0, 11 -> 0)) 39 | ) 40 | ) 41 | 42 | CoverageParser 43 | .parse(new File("."), new File("coverage-parser/src/test/resources/test_jacoco.xml")) shouldEqual Right( 44 | expectedReport 45 | ) 46 | } 47 | 48 | "fail to get invalid report" in { 49 | CoverageParser.parse(new File("."), new File("invalid_report.xml")) shouldEqual Left( 50 | "Could not parse report, unrecognized report format (tried: Cobertura, Jacoco, Clover, OpenCover, DotCover, PHPUnit, LCOV, Go)" 51 | ) 52 | } 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/JacocoParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | import com.codacy.api._ 5 | import com.codacy.parsers.util.TextUtils 6 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 7 | 8 | import scala.xml.{Elem, Node, NodeSeq} 9 | 10 | private case class LineCoverage(missedInstructions: Int, coveredInstructions: Int) 11 | 12 | object JacocoParser extends CoverageParser with XmlReportParser { 13 | 14 | override val name: String = "Jacoco" 15 | 16 | private val ReportTag = "report" 17 | 18 | override def parse(projectRoot: File, reportFile: File): Either[String, CoverageReport] = 19 | parseReport(reportFile, s"Could not find top level <$ReportTag> tag") { 20 | parseReportNode(projectRoot, _) 21 | } 22 | 23 | override def validateSchema(xml: Elem): Boolean = getRootNode(xml).nonEmpty 24 | 25 | override def getRootNode(xml: Elem): NodeSeq = xml \\ ReportTag 26 | 27 | private def parseReportNode(projectRoot: File, report: NodeSeq): Either[String, CoverageReport] = { 28 | val projectRootStr: String = TextUtils.sanitiseFilename(projectRoot.getAbsolutePath) 29 | val filesCoverage = for { 30 | pkg <- report \\ "package" 31 | packageName = (pkg \@ "name") 32 | sourceFile <- pkg \\ "sourcefile" 33 | } yield { 34 | val filename = 35 | TextUtils 36 | .sanitiseFilename(s"$packageName/${(sourceFile \@ "name")}") 37 | .stripPrefix(projectRootStr) 38 | .stripPrefix("/") 39 | lineCoverage(filename, sourceFile) 40 | } 41 | Right(CoverageReport(filesCoverage)) 42 | } 43 | 44 | private def lineCoverage(filename: String, fileNode: Node): CoverageFileReport = { 45 | val lineHitMap: Map[Int, Int] = (fileNode \\ "line").view 46 | .map { line => 47 | (line \@ "nr").toInt -> LineCoverage((line \@ "mi").toInt, (line \@ "ci").toInt) 48 | } 49 | .collect { 50 | case (key, lineCoverage) if lineCoverage.missedInstructions + lineCoverage.coveredInstructions > 0 => 51 | key -> (if (lineCoverage.coveredInstructions > 0) 1 else 0) 52 | } 53 | .toMap 54 | 55 | CoverageFileReport(filename, lineHitMap) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/CoberturaParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.util.MathUtils._ 7 | import com.codacy.parsers.util.TextUtils 8 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 9 | 10 | import scala.collection.mutable 11 | import scala.xml.{Elem, NodeSeq} 12 | 13 | object CoberturaParser extends CoverageParser with XmlReportParser { 14 | 15 | override val name: String = "Cobertura" 16 | 17 | private val CoverageTag = "coverage" 18 | private val LineRateAttribute = "line-rate" 19 | 20 | override def parse(projectRoot: File, reportFile: File): Either[String, CoverageReport] = { 21 | parseReport(reportFile, s"Could not find top level <$CoverageTag> tag") { node => 22 | Right(parseReportNode(projectRoot, node)) 23 | } 24 | } 25 | 26 | // restricting the schema to 27 | // ensures this will not consider Clover reports which also have a tag 28 | override def validateSchema(xml: Elem): Boolean = (xml \\ CoverageTag \ s"@$LineRateAttribute").nonEmpty 29 | 30 | override def getRootNode(xml: Elem): NodeSeq = xml \\ CoverageTag 31 | 32 | private def parseReportNode(projectRoot: File, report: NodeSeq) = { 33 | val projectRootStr: String = TextUtils.sanitiseFilename(projectRoot.getAbsolutePath) 34 | 35 | val fileReports: List[CoverageFileReport] = (for { 36 | (filename, classes) <- (report \\ "class").groupBy(c => c \@ "filename") 37 | } yield { 38 | val cleanFilename = TextUtils.sanitiseFilename(filename).stripPrefix(projectRootStr).stripPrefix("/") 39 | lineCoverage(cleanFilename, classes) 40 | }).toList 41 | 42 | CoverageReport(fileReports) 43 | } 44 | 45 | private def lineCoverage(sourceFilename: String, classes: NodeSeq): CoverageFileReport = { 46 | val map = mutable.Map.empty[Int, Int] 47 | 48 | for { 49 | xClass <- classes 50 | line <- xClass \\ "line" 51 | } { 52 | val key = (line \@ "number").toInt 53 | val value = (line \@ "hits").toIntOrMaxValue 54 | val sum = map.get(key).getOrElse(0) + BigInt(value) 55 | 56 | map(key) = sum.toIntOrMaxValue 57 | } 58 | 59 | CoverageFileReport(sourceFilename, map.toMap) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/GoParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api._ 6 | 7 | import com.codacy.parsers.implementation.GoParser 8 | import org.scalatest.{EitherValues} 9 | import org.scalatest.wordspec.AnyWordSpec 10 | import org.scalatest.matchers.should.Matchers 11 | 12 | class GoParserTest extends AnyWordSpec with Matchers with EitherValues { 13 | 14 | "parse" should { 15 | 16 | "fail to parse an invalid report" when { 17 | 18 | "the report file does not exist" in { 19 | // Arrange 20 | val nonExistentReportPath = "coverage-parser/src/test/resources/non-existent.xml" 21 | 22 | // Act 23 | val parseResult = GoParser.parse(new File("."), new File(nonExistentReportPath)) 24 | 25 | // Assert 26 | parseResult shouldBe Left("Can't load report file.") 27 | } 28 | } 29 | 30 | "return a valid report from github" in { 31 | val reader = GoParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_go_gh.out")) 32 | 33 | val testReport = CoverageReport( 34 | List( 35 | CoverageFileReport( 36 | "src/hello.go", 37 | Map(5 -> 0, 14 -> 1, 6 -> 0, 13 -> 1, 17 -> 1, 12 -> 1, 7 -> 0, 18 -> 1, 11 -> 1, 19 -> 1) 38 | ) 39 | ) 40 | ) 41 | 42 | reader.value should equal(testReport) 43 | } 44 | 45 | "return a valid report" in { 46 | val reader = GoParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_go.out")) 47 | 48 | val testReport = CoverageReport( 49 | List( 50 | CoverageFileReport( 51 | "example.com/m/v2/hello.go", 52 | Map(5 -> 0, 14 -> 1, 6 -> 0, 13 -> 1, 17 -> 1, 12 -> 1, 7 -> 0, 18 -> 1, 11 -> 1, 19 -> 1) 53 | ) 54 | ) 55 | ) 56 | 57 | reader.value should equal(testReport) 58 | } 59 | 60 | "return consistent values" in { 61 | //given two reports where the package names were only changed, should return the same results COV-207 62 | val reader = GoParser.parse(new File("."), new File("coverage-parser/src/test/resources/go/original_package.out")) 63 | val reader1 = 64 | GoParser.parse(new File("."), new File("coverage-parser/src/test/resources/go/changed_package_name.out")) 65 | 66 | reader.value.fileReports(0).coverage should equal(reader1.value.fileReports(0).coverage) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/DotcoverParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.util.TextUtils 7 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 8 | 9 | import scala.xml.{Elem, NodeSeq} 10 | 11 | case class StatementNode(fileIndex: Int, line: Int, covered: Boolean) 12 | 13 | object DotcoverParser extends CoverageParser with XmlReportParser { 14 | override val name: String = "DotCover" 15 | 16 | private val RootTag = "Root" 17 | private val CoverageAttribute = "CoveragePercent" 18 | private val CoveredAttribute = "Covered" 19 | 20 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = 21 | parseReport(reportFile, s"Could not find tag <$RootTag $CoverageAttribute=...>") { node => 22 | Right(parseReportNode(rootProject, node)) 23 | } 24 | 25 | override def validateSchema(xml: Elem): Boolean = (xml \\ RootTag \ s"@$CoverageAttribute").nonEmpty 26 | 27 | override def getRootNode(xml: Elem): NodeSeq = xml \\ RootTag 28 | 29 | private def parseReportNode(rootProject: File, rootNode: NodeSeq): CoverageReport = { 30 | val projectRootStr: String = TextUtils.sanitiseFilename(rootProject.getAbsolutePath) 31 | 32 | val fileIndices: Map[Int, String] = (rootNode \ "FileIndices" \ "File").map { x => 33 | (x \@ "Index").toInt -> (x \@ "Name") 34 | }.toMap 35 | 36 | val statementsPerFile: Map[Int, NodeSeq] = (rootNode \\ "Statement").groupBy(x => (x \@ "FileIndex").toInt) 37 | 38 | val fileReports = for { 39 | (fileIndex, statements) <- statementsPerFile 40 | filename = TextUtils.sanitiseFilename(fileIndices(fileIndex)).stripPrefix(projectRootStr).stripPrefix("/") 41 | lineCoverage = getLineCoverage(statements) 42 | totalLines = lineCoverage.keys.size 43 | coveredLines = lineCoverage.values.count(_ > 0) 44 | } yield CoverageFileReport(filename, lineCoverage) 45 | 46 | CoverageReport(fileReports.toSeq) 47 | } 48 | 49 | private def getLineCoverage(statementNodes: NodeSeq) = { 50 | val lines = for { 51 | node <- statementNodes 52 | // a statement can extend over several lines 53 | line <- (node \@ "Line").toInt to (node \@ "EndLine").toInt 54 | coveredValue = if ((node \@ CoveredAttribute).toBoolean) 1 else 0 55 | } yield (line, coveredValue) 56 | 57 | lines.toMap 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/transformation/GitFileNameUpdaterAndFilterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.transformation 2 | 3 | import com.codacy.api.{CoverageFileReport, CoverageReport} 4 | import com.codacy.transformation.FileNameMatcher.getFilenameFromPath 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class GitFileNameUpdaterAndFilterSpec extends AnyWordSpec with Matchers { 9 | 10 | private val acceptableFilenames = 11 | Seq("src/folder/file1.txt", "src/another-folder/file1.txt", "src/folder/file2.txt", "src/folder/file3.txt") 12 | private val acceptableFilenamesMap = acceptableFilenames.groupBy(getFilenameFromPath).view.toMap 13 | private val updaterAndFilter = new GitFileNameUpdaterAndFilter(acceptableFilenamesMap) 14 | 15 | "execute" should { 16 | "update and match filename" in { 17 | // ARRANGE 18 | val expectedFilename = "src/folder/file1.txt" 19 | 20 | val coverageReport = CoverageReport(Seq(CoverageFileReport("folder/file1.txt", Map.empty))) 21 | 22 | // ACT 23 | val result = updaterAndFilter.execute(coverageReport) 24 | 25 | // ASSERT 26 | result.fileReports.map(_.filename) shouldBe Seq(expectedFilename) 27 | } 28 | 29 | "update and match several filenames" in { 30 | // ARRANGE 31 | val expectedFilenames = 32 | Set("src/folder/file1.txt", "src/another-folder/file1.txt", "src/folder/file2.txt", "src/folder/file3.txt") 33 | 34 | val coverageReport = CoverageReport( 35 | Seq( 36 | CoverageFileReport("folder/file1.txt", Map.empty), 37 | CoverageFileReport("another-folder/file1.txt", Map.empty), 38 | CoverageFileReport("file2.txt", Map.empty), 39 | CoverageFileReport("src/folder/file3.txt", Map.empty) 40 | ) 41 | ) 42 | 43 | // ACT 44 | val result = updaterAndFilter.execute(coverageReport) 45 | 46 | // ASSERT 47 | result.fileReports.map(_.filename).toSet shouldBe expectedFilenames 48 | } 49 | 50 | "filters out reports not in acceptable filenames" in { 51 | // ARRANGE 52 | val coverageReport = CoverageReport( 53 | Seq( 54 | CoverageFileReport("folder/file1.txt", Map.empty), 55 | CoverageFileReport("folder/not-acceptable-file.txt", Map.empty) 56 | ) 57 | ) 58 | 59 | // ACT 60 | val result = updaterAndFilter.execute(coverageReport) 61 | 62 | // ASSERT 63 | result.fileReports.size shouldBe 1 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/model/configuration/Configuration.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.model.configuration 2 | 3 | import java.io.File 4 | import scala.util.matching.Regex 5 | import com.codacy.api.OrganizationProvider 6 | import com.codacy.api.client.RequestTimeout 7 | import com.codacy.parsers.CoverageParser 8 | import com.codacy.plugins.api.languages.{Language, Languages} 9 | 10 | sealed trait Configuration { 11 | def baseConfig: BaseConfig 12 | } 13 | 14 | case class ReportConfig( 15 | baseConfig: BaseConfig, 16 | languageOpt: Option[String], 17 | forceLanguage: Boolean, 18 | coverageReports: List[File], 19 | partial: Boolean, 20 | prefix: String, 21 | forceCoverageParser: Option[CoverageParser] 22 | ) extends Configuration { 23 | 24 | lazy val language: Option[Language] = languageOpt.flatMap(Languages.fromName) 25 | } 26 | 27 | case class FinalConfig(baseConfig: BaseConfig) extends Configuration 28 | 29 | sealed trait AuthenticationConfig 30 | 31 | case class ProjectTokenAuthenticationConfig(projectToken: String) extends AuthenticationConfig 32 | 33 | case class ApiTokenAuthenticationConfig( 34 | apiToken: String, 35 | organizationProvider: OrganizationProvider.Value, 36 | username: String, 37 | projectName: String 38 | ) extends AuthenticationConfig 39 | 40 | case class BaseConfig( 41 | authentication: AuthenticationConfig, 42 | codacyApiBaseUrl: String, 43 | commitUUID: Option[CommitUUID], 44 | debug: Boolean, 45 | timeout: RequestTimeout, 46 | sleepTime: Int, 47 | numRetries: Int, 48 | skipSslVerification: Boolean 49 | ) 50 | 51 | sealed trait CommitUUID extends Any { 52 | def value: String 53 | } 54 | 55 | object CommitUUID { 56 | /* The number of characters in a commit UUID. */ 57 | val length: Int = 40 58 | 59 | /* Regex to help detect a commit UUID. */ 60 | val regex: Regex = s"[0-9a-fA-F]{$length}".r 61 | 62 | private def isValid(commitUUID: String): Boolean = 63 | commitUUID.length == length && regex.findFirstIn(commitUUID).isDefined 64 | 65 | /** Either creates a [[CommitUUID]], if `commitUUID` is valid, or returns an error string, to display to the user. */ 66 | def fromString(commitUUID: String): Either[String, CommitUUID] = 67 | if (isValid(commitUUID)) { 68 | Right(CommitUUIDImpl(commitUUID)) 69 | } else { 70 | Left("Commit SHA-1 hash isn't valid. Make sure it consists of 40 hexadecimal characters.") 71 | } 72 | 73 | /** Commit UUID class that guarantees it contains a valid commit SHA, since it can only be instantiated via 74 | * [[fromString()]] */ 75 | private[CommitUUID] case class CommitUUIDImpl(value: String) extends AnyVal with CommitUUID 76 | } 77 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/OpenCoverParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.util.TextUtils 7 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 8 | 9 | import scala.xml.{Elem, NodeSeq} 10 | 11 | object OpenCoverParser extends CoverageParser with XmlReportParser { 12 | private val RootTag = "CoverageSession" 13 | private val IdAttribute = "uid" 14 | private val FileTag = "File" 15 | private val FileRefTag = "FileRef" 16 | private val LineAttribute = "sl" 17 | private val VisitCounterAttribute = "vc" 18 | private val FilesTag = "Files" 19 | private val FullPathAttribute = "fullPath" 20 | private val MethodTag = "Method" 21 | private val SequencePointTag = "SequencePoint" 22 | 23 | override val name: String = "OpenCover" 24 | 25 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = 26 | parseReport(reportFile, s"Could not find tag <$RootTag>") { node => 27 | Right(parseReportNode(node, TextUtils.sanitiseFilename(rootProject.getAbsolutePath))) 28 | } 29 | 30 | override def validateSchema(xml: Elem): Boolean = getRootNode(xml).nonEmpty 31 | 32 | override def getRootNode(xml: Elem): NodeSeq = xml \\ RootTag 33 | 34 | private def parseReportNode(rootNode: NodeSeq, projectRoot: String): CoverageReport = { 35 | val fileIndices: Map[Int, String] = (rootNode \\ FilesTag \ FileTag).map { n => 36 | (n \@ IdAttribute).toInt -> n \@ FullPathAttribute 37 | }.toMap 38 | 39 | val validMethods = (rootNode \\ MethodTag).filter(m => (m \ FileRefTag).nonEmpty) 40 | 41 | val fileReports = (for { 42 | (fileIndex, methods) <- validMethods.groupBy(m => (m \ FileRefTag \@ IdAttribute).toInt) 43 | filename <- fileIndices.get(fileIndex) 44 | 45 | sanitisedFileName = TextUtils.sanitiseFilename(filename).stripPrefix(projectRoot).stripPrefix("/") 46 | lineCoverage = getLineCoverage(methods, sanitisedFileName) 47 | totalLines = lineCoverage.size 48 | coveredLines = lineCoverage.count { case (_, visitCount) => visitCount > 0 } 49 | } yield CoverageFileReport(sanitisedFileName, lineCoverage)).toSeq 50 | 51 | CoverageReport(fileReports) 52 | } 53 | 54 | private def getLineCoverage(methodNodes: NodeSeq, filename: String) = { 55 | val lineCoverage = for { 56 | methodNode <- methodNodes 57 | sequencePoint <- methodNode \\ SequencePointTag 58 | } yield (sequencePoint \@ LineAttribute).toInt -> (sequencePoint \@ VisitCounterAttribute).toInt 59 | 60 | lineCoverage.toMap 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/providers/GitHubActionProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | import com.codacy.model.configuration.CommitUUID 7 | import com.codacy.rules.commituuid.CommitUUIDProvider 8 | import play.api.libs.json._ 9 | import wvlet.log.LazyLogger 10 | 11 | import scala.util.Try 12 | 13 | object GitHubActionProvider extends CommitUUIDProvider with LazyLogger { 14 | val name: String = "GitHub Actions" 15 | 16 | override def validateEnvironment(environment: Map[String, String]): Boolean = { 17 | environment.get("CI").contains("true") && environment.get("GITHUB_ACTIONS").contains("true") 18 | } 19 | 20 | override def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] = { 21 | // if the event is a pull_request or a workflow_run the GITHUB_SHA will 22 | // have a different commit UUID (the id of a merge commit) 23 | // https://help.github.com/en/actions/reference/events-that-trigger-workflows 24 | // for this reason, we need to fetch it from the event details that GitHub provides 25 | // equivalent to doing ${{github.event.pull_request.head.sha}} in a GitHub action workflow 26 | environment.get("GITHUB_EVENT_NAME") match { 27 | case Some(eventName @ ("pull_request" | "workflow_run")) => 28 | getEventCommitSha(environment, eventName) 29 | case _ => 30 | parseEnvironmentVariable(environment.get("GITHUB_SHA")) 31 | } 32 | } 33 | 34 | private def getEventCommitSha(envVars: Map[String, String], eventName: String): Either[String, CommitUUID] = { 35 | for { 36 | eventPath <- envVars.get("GITHUB_EVENT_PATH").toRight("Could not find event description file path") 37 | eventContent <- readFile(eventPath) 38 | sha <- extractHeadSHA(eventName = eventName, eventContent = eventContent) 39 | commitUUID <- CommitUUID.fromString(sha) 40 | } yield commitUUID 41 | } 42 | 43 | private def readFile(path: String): Either[String, String] = { 44 | val contentTry = Try { 45 | val fileBytes = Files.readAllBytes(new File(path).toPath) 46 | new String(fileBytes) 47 | } 48 | 49 | contentTry.toEither.left.map { ex => 50 | logger.error("Failed to read event file", ex) 51 | s"Failed to read event file with error: ${ex.getMessage}" 52 | } 53 | } 54 | 55 | private def extractHeadSHA(eventName: String, eventContent: String) = { 56 | Try { 57 | val eventJson = Json.parse(eventContent) 58 | eventName match { 59 | case "workflow_run" => 60 | (eventJson \ eventName \ "head_sha").as[String] 61 | case _ => 62 | (eventJson \ eventName \ "head" \ "sha").as[String] 63 | } 64 | }.toEither.left.map(t => s"Unable to fetch SHA from event file. Failed with error: ${t.getMessage}") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_opencover.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Report/CoverageReport.php.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/DotCoverParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.implementation.DotcoverParser 7 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 8 | import org.scalatest.wordspec.AnyWordSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class DotCoverParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 12 | private val nonExistentFile = "coverage-parser/src/test/resources/non-existent.xml" 13 | private val dotCoverReport = "coverage-parser/src/test/resources/test_dotcover.xml" 14 | private val differentFormatReport = "coverage-parser/src/test/resources/test_cobertura.xml" 15 | "parse" should { 16 | "return an invalid report" when { 17 | "report file does not exist" in { 18 | val reader = DotcoverParser.parse(new File("."), new File(nonExistentFile)) 19 | 20 | reader shouldBe Symbol("left") 21 | } 22 | 23 | "report file has a different format" in { 24 | val reader = DotcoverParser.parse(new File("."), new File(differentFormatReport)) 25 | 26 | reader shouldBe Symbol("left") 27 | } 28 | } 29 | 30 | "return a valid report" in { 31 | val reader = DotcoverParser.parse(new File("."), new File(dotCoverReport)) 32 | 33 | reader shouldBe Symbol("right") 34 | } 35 | 36 | "return the expected files" in { 37 | val reader = DotcoverParser.parse(new File("."), new File(dotCoverReport)) 38 | reader.value.fileReports.map(_.filename).sorted shouldBe Seq( 39 | "src/Coverage/FooBar.cs", 40 | "src/Tests/FooBarTests.cs", 41 | "src/Coverage/Program.cs", 42 | "src/Coverage/Bar.cs", 43 | "src/Coverage/Foo.cs" 44 | ).sorted 45 | } 46 | 47 | "return the expected coverage report" in { 48 | val reader = DotcoverParser.parse(new File("."), new File(dotCoverReport)) 49 | 50 | reader.value shouldBe CoverageReport( 51 | List( 52 | CoverageFileReport( 53 | "src/Coverage/FooBar.cs", 54 | Map(10 -> 1, 21 -> 1, 9 -> 1, 13 -> 0, 17 -> 1, 19 -> 0, 15 -> 0) 55 | ), 56 | CoverageFileReport( 57 | "src/Tests/FooBarTests.cs", 58 | Map( 59 | 14 -> 1, 60 | 20 -> 1, 61 | 28 -> 1, 62 | 22 -> 1, 63 | 27 -> 1, 64 | 12 -> 1, 65 | 31 -> 1, 66 | 11 -> 1, 67 | 23 -> 1, 68 | 30 -> 1, 69 | 19 -> 1, 70 | 15 -> 1 71 | ) 72 | ), 73 | CoverageFileReport("src/Coverage/Program.cs", Map(8 -> 0, 9 -> 0, 10 -> 0)), 74 | CoverageFileReport("src/Coverage/Bar.cs", Map(10 -> 0, 14 -> 1, 9 -> 1, 12 -> 0, 11 -> 0, 8 -> 1, 15 -> 1)), 75 | CoverageFileReport("src/Coverage/Foo.cs", Map(8 -> 1, 9 -> 1, 10 -> 1)) 76 | ) 77 | ) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/CoberturaParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.implementation.CoberturaParser 7 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 8 | import org.scalatest.matchers.should.Matchers 9 | import org.scalatest.wordspec.AnyWordSpec 10 | 11 | class CoberturaParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 12 | 13 | "CoberturaParser" should { 14 | 15 | "identify if report is invalid" in { 16 | val reader = CoberturaParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_jacoco.xml")) 17 | reader.isLeft shouldBe true 18 | } 19 | 20 | "identify if report is valid" in { 21 | val reader = 22 | CoberturaParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_cobertura.xml")) 23 | reader.isRight shouldBe true 24 | } 25 | 26 | "return a valid report" in { 27 | val reader = 28 | CoberturaParser.parse(new File("."), new File("coverage-parser/src/test/resources/test_cobertura.xml")) 29 | 30 | val testReport = CoverageReport( 31 | List( 32 | CoverageFileReport( 33 | "coverage-parser/src/test/resources/TestSourceFile.scala", 34 | Map(5 -> 1, 10 -> 1, 6 -> 2, 9 -> Int.MaxValue, 3 -> 0, 4 -> 1, 7 -> 1, 8 -> 3) 35 | ), 36 | CoverageFileReport("coverage-parser/src/test/resources/TestSourceFile2.scala", Map(1 -> 1, 2 -> 1, 3 -> 1)), 37 | ) 38 | ) 39 | 40 | reader.value should equal(testReport) 41 | } 42 | 43 | "not crash on thousands separators" in { 44 | val reader = 45 | CoberturaParser.parse(new File("."), new File("coverage-parser/src/test/resources/thousand_sep_cobertura.xml")) 46 | 47 | val testReport = CoverageReport( 48 | List( 49 | CoverageFileReport( 50 | "coverage-parser/src/test/resources/TestSourceFile.scala", 51 | Map(5 -> 1, 10 -> 1, 6 -> 2, 9 -> 1, 9 -> 0, 8 -> 1, 4 -> 1) 52 | ), 53 | CoverageFileReport("coverage-parser/src/test/resources/TestSourceFile2.scala", Map(1 -> 1, 2 -> 1, 3 -> 1)), 54 | ) 55 | ) 56 | 57 | reader.value should equal(testReport) 58 | } 59 | 60 | "return a valid report with windows file path separator" in { 61 | val reader = 62 | CoberturaParser.parse(new File("."), new File("coverage-parser/src/test/resources/windows_paths_cobertura.xml")) 63 | 64 | val testReport = CoverageReport( 65 | List( 66 | CoverageFileReport( 67 | "coverage-parser/src/test/resources/TestSourceFile.scala", 68 | Map(5 -> 1, 10 -> 1, 6 -> 2, 9 -> 1, 3 -> 0, 4 -> 1) 69 | ), 70 | CoverageFileReport("coverage-parser/src/test/resources/TestSourceFile2.scala", Map(1 -> 1, 2 -> 1, 3 -> 1)), 71 | ) 72 | ) 73 | 74 | reader.value should equal(testReport) 75 | } 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Config.php.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Parser/Parser.php.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 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/LCOVParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import com.codacy.parsers.CoverageParser 4 | import com.codacy.parsers.util.MathUtils._ 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import java.io.File 7 | 8 | import com.codacy.parsers.util.XMLoader 9 | 10 | import scala.io.Source 11 | import scala.util.{Failure, Success, Try} 12 | 13 | object LCOVParser extends CoverageParser { 14 | override val name: String = "LCOV" 15 | 16 | final val SF = "SF:" 17 | final val DA = "DA:" 18 | 19 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = { 20 | val report = Try(Source.fromFile(reportFile)) match { 21 | // most reports are XML, and we want to ensure the LCOV parser won't mishandle it and return an empty result 22 | case Success(lines) if Try(XMLoader.loadFile(reportFile)).isSuccess => 23 | Left(s"The file is not in the lcov format but is an xml.") 24 | case Success(lines) => 25 | Right(lines.getLines) 26 | case Failure(ex) => 27 | Left(s"Can't load report file. ${ex.getMessage}") 28 | } 29 | 30 | report.flatMap(parseLines(reportFile, _)) 31 | } 32 | 33 | private def parseLines(reportFile: File, lines: Iterator[String]): Either[String, CoverageReport] = { 34 | val coverageFileReports = 35 | lines.foldLeft[Either[String, Seq[CoverageFileReport]]](Right(Seq.empty[CoverageFileReport]))( 36 | (accum, next) => 37 | accum.flatMap { 38 | case reports if next startsWith SF => 39 | Right(CoverageFileReport(next.stripPrefix(SF), Map()) +: reports) 40 | case reports if next startsWith DA => 41 | reports.headOption match { 42 | case Some(value) => 43 | val coverage = next.stripPrefix(DA).split(",") 44 | if (coverage.length >= 2 && coverage.forall(_ forall Character.isDigit)) { 45 | val coverageValue = coverage.map(_.toIntOrMaxValue) 46 | Right( 47 | value.copy(coverage = value.coverage + (coverageValue(0) -> coverageValue(1))) +: reports.tail 48 | ) 49 | } else Left(s"Misformatting of file ${reportFile.toString}") 50 | case _ => Left(s"Fail to parse ${reportFile.toString}") 51 | } 52 | case reports => 53 | val res = Right(reports) 54 | res 55 | } 56 | ) 57 | coverageFileReports.map { fileReports => 58 | val totalFileReport = fileReports.map { report => 59 | CoverageFileReport(report.filename, report.coverage) 60 | } 61 | 62 | val (covered, total) = totalFileReport 63 | .map { f => 64 | (f.coverage.count { case (_, hit) => hit > 0 }, f.coverage.size) 65 | } 66 | .foldLeft(0 -> 0) { 67 | case ((accumCovered, accumTotal), (nextCovered, nextTotal)) => 68 | (accumCovered + nextCovered, accumTotal + nextTotal) 69 | } 70 | 71 | CoverageReport(totalFileReport) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Report/JsonProducer.php.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 | -------------------------------------------------------------------------------- /src/test/scala/com/codacy/rules/commituuid/providers/GitHubActionProviderSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid.providers 2 | 3 | import org.scalatest.{EitherValues} 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class GitHubActionProviderSpec extends AnyWordSpec with Matchers with EitherValues { 8 | val validPullRequestCommitUuid = "480b8293f4340216b8f630053a230e2867cd5c28" 9 | val invalidPullRequestCommitUuid = "65e4436280a41c721617cba15f33555d3122907e" 10 | 11 | val validWorkflowRunCommitUuid = "0f33f2554d99ce8c12ab126fc4e06bd2ad663e50" 12 | 13 | "getUUID" should { 14 | "fail" when { 15 | "no environment variables are defined" in { 16 | val provider = GitHubActionProvider 17 | val commitUuidEither = provider.getValidCommitUUID(Map.empty) 18 | commitUuidEither should be(Symbol("left")) 19 | } 20 | "not pull request and GITHUB_SHA is empty" in { 21 | val provider = GitHubActionProvider 22 | val commitUuidEither = provider.getValidCommitUUID(Map("GITHUB_EVENT_NAME" -> "push")) 23 | commitUuidEither should be(Symbol("left")) 24 | } 25 | "github action does not have correct commit sha" in { 26 | val provider = GitHubActionProvider 27 | val envVars = Map( 28 | "GITHUB_EVENT_NAME" -> "pull_request", 29 | "GITHUB_SHA" -> invalidPullRequestCommitUuid, 30 | "GITHUB_EVENT_PATH" -> "src/test/resources/invalid-github-action-pull-request-event.json" 31 | ) 32 | val commitUuidEither = provider.getValidCommitUUID(envVars) 33 | commitUuidEither should be(Symbol("left")) 34 | } 35 | } 36 | "succeed" when { 37 | "event is push and GITHUB_SHA has value" in { 38 | val provider = GitHubActionProvider 39 | val envVars = Map("GITHUB_EVENT_NAME" -> "push", "GITHUB_SHA" -> validPullRequestCommitUuid) 40 | val commitUuidEither = provider.getValidCommitUUID(envVars) 41 | commitUuidEither should be(Symbol("right")) 42 | commitUuidEither.value.value should be(validPullRequestCommitUuid) 43 | } 44 | "event is pull_request and json file includes needed information" in { 45 | val provider = GitHubActionProvider 46 | val envVars = Map( 47 | "GITHUB_EVENT_NAME" -> "pull_request", 48 | "GITHUB_SHA" -> invalidPullRequestCommitUuid, 49 | "GITHUB_EVENT_PATH" -> "src/test/resources/github-action-pull-request-event.json" 50 | ) 51 | val commitUuidEither = provider.getValidCommitUUID(envVars) 52 | commitUuidEither should be(Symbol("right")) 53 | commitUuidEither.value.value should be(validPullRequestCommitUuid) 54 | } 55 | "event is workflow_run and json file includes needed information" in { 56 | val provider = GitHubActionProvider 57 | val envVars = Map( 58 | "GITHUB_EVENT_NAME" -> "workflow_run", 59 | "GITHUB_SHA" -> invalidPullRequestCommitUuid, 60 | "GITHUB_EVENT_PATH" -> "src/test/resources/github-action-workflow-run-event.json" 61 | ) 62 | val commitUuidEither = provider.getValidCommitUUID(envVars) 63 | commitUuidEither should be(Symbol("right")) 64 | commitUuidEither.value.value should be(validWorkflowRunCommitUuid) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/PhpUnitXmlParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | 5 | import com.codacy.api.{CoverageFileReport, CoverageReport} 6 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 7 | import com.codacy.parsers.util.TextUtils 8 | 9 | import scala.xml.{Elem, NodeSeq} 10 | 11 | object PhpUnitXmlParser extends CoverageParser with XmlReportParser { 12 | override val name: String = "PHPUnit" 13 | 14 | private val PhpUnitTag = "phpunit" 15 | private val ProjectTag = "project" 16 | private val DirectoryTag = "directory" 17 | private val XmlParseErrorMessage = s"Could not find top level <$PhpUnitTag> tag"; 18 | 19 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = 20 | parseReport(reportFile, XmlParseErrorMessage) { 21 | parseReportNode(rootProject, _, reportFile.getParent) 22 | } 23 | 24 | override def validateSchema(xml: Elem): Boolean = getRootNode(xml).nonEmpty 25 | 26 | override def getRootNode(xml: Elem): NodeSeq = xml \\ PhpUnitTag 27 | 28 | private def parseReportNode( 29 | projectRoot: File, 30 | report: NodeSeq, 31 | reportRootPath: String 32 | ): Either[String, CoverageReport] = { 33 | val fileNodes = report \ ProjectTag \ DirectoryTag \ "file" 34 | val projectRootPath = TextUtils.sanitiseFilename(projectRoot.getAbsolutePath) 35 | val codeDirectory = report \ ProjectTag \ DirectoryTag \@ "name" 36 | val fileReports = makeFileReports(fileNodes, projectRootPath, codeDirectory, reportRootPath) 37 | fileReports.map(CoverageReport(_)) 38 | } 39 | 40 | private def makeFileReports( 41 | fileNodes: NodeSeq, 42 | projectRootPath: String, 43 | codeDirectory: String, 44 | reportRootPath: String 45 | ): Either[String, Seq[CoverageFileReport]] = { 46 | val builder = Seq.newBuilder[CoverageFileReport] 47 | var error = Option.empty[String] 48 | for (f <- fileNodes if error.isEmpty) { 49 | val reportFileName = f \@ "href" 50 | val fileName = getSourceFileName(projectRootPath, codeDirectory, reportFileName) 51 | getLineCoverage(reportRootPath, reportFileName) match { 52 | case Right(lineCoverage) => 53 | builder += CoverageFileReport(fileName, lineCoverage) 54 | case Left(message) => error = Some(message) 55 | } 56 | } 57 | error match { 58 | case Some(value) => 59 | Left(value) 60 | case None => 61 | Right(builder.result()) 62 | } 63 | } 64 | 65 | private def getLineCoverage(reportRootPath: String, filename: String) = { 66 | val coverageDetailFile = new File(reportRootPath, filename) 67 | val phpUnitNode = loadXmlReport(coverageDetailFile, XmlParseErrorMessage) 68 | 69 | val lineCoverage: Either[String, Map[Int, Int]] = phpUnitNode.map { node => 70 | (node \\ "coverage" \\ "line").map { line => 71 | (line \@ "nr").toInt -> (line \ "covered").length 72 | }.toMap 73 | } 74 | lineCoverage 75 | } 76 | 77 | private def getSourceFileName(pathToRemove: String, codeRootDirectory: String, reportRelativePath: String) = { 78 | new File(codeRootDirectory, reportRelativePath).getAbsolutePath 79 | .stripPrefix(pathToRemove) 80 | .stripPrefix("/") 81 | .stripSuffix(".xml") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/GoParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import com.codacy.parsers.CoverageParser 4 | 5 | import java.io.File 6 | 7 | import com.codacy.api.{CoverageFileReport, CoverageReport} 8 | 9 | import scala.io.Source 10 | import scala.util.{Failure, Success, Try} 11 | 12 | case class GoCoverageInfo(filename: String, lineFrom: Int, lineTo: Int, numberOfStatements: Int, countOfStatements: Int) 13 | 14 | object GoParser extends CoverageParser { 15 | 16 | override val name: String = "Go" 17 | 18 | final val MODE = """mode: ([set|count|atomic]*)""".r 19 | 20 | // filename.go:lineFrom.column,lineTo.column numberOfStatements countOfStatements 21 | final val regexpString = """([a-zA-Z\/\._\-\d]*):(\d+).*?,(\d+).* (\d+) (\d+)""".r 22 | 23 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = { 24 | val report = Try(Source.fromFile(reportFile)) match { 25 | case Success(lines) => 26 | Right(lines.getLines) 27 | case Failure(ex) => 28 | Left("Can't load report file.") 29 | } 30 | 31 | report.map(lines => parseLines(lines.toList)) 32 | } 33 | 34 | private def parseLines(lines: List[String]): CoverageReport = { 35 | val coverageInfo = parseAllCoverageInfo(lines) 36 | val coverageInfoGroupedByFilename: Map[String, Set[GoCoverageInfo]] = 37 | coverageInfo.toSet.groupBy((a: GoCoverageInfo) => a.filename) 38 | 39 | val coverageFileReports = 40 | coverageInfoGroupedByFilename.foldLeft[Seq[CoverageFileReport]](Seq.empty[CoverageFileReport])((accum, next) => { 41 | next match { 42 | case (filename, coverageInfosForFile) => 43 | // Only for Github - Process filename to remove "github.com/orgname/repositoryname/" 44 | val processedFilename = if (filename.startsWith("github.com/")) { 45 | filename.split("/", 4).lastOption.getOrElse(filename) // Get everything after repository name 46 | } else { 47 | filename 48 | } 49 | 50 | // Calculate hits for a file for given statement reports 51 | val coverage = coverageInfosForFile.foldLeft(Map[Int, Int]()) { 52 | case (hitMapAcc, coverageInfo) => 53 | // Calculate the range of lines the statement has 54 | val lines = Range.inclusive(coverageInfo.lineFrom, coverageInfo.lineTo) 55 | 56 | // For each line, add the number of hits 57 | hitMapAcc ++ lines.foldLeft(Map[Int, Int]()) { 58 | case (statementHitMapAcc, line) => 59 | statementHitMapAcc ++ 60 | // If the line is already present on the hit map, don't replace the value 61 | Map(line -> (hitMapAcc.getOrElse(line, 0) + coverageInfo.countOfStatements)) 62 | } 63 | } 64 | 65 | accum :+ CoverageFileReport(processedFilename, coverage) 66 | } 67 | }) 68 | 69 | CoverageReport(coverageFileReports) 70 | } 71 | 72 | private def parseAllCoverageInfo(lines: List[String]): List[GoCoverageInfo] = { 73 | lines.collect { 74 | case regexpString(filename, lineFrom, lineTo, numberOfStatements, countOfStatements, _*) => 75 | GoCoverageInfo(filename, lineFrom.toInt, lineTo.toInt, numberOfStatements.toInt, countOfStatements.toInt) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/create_issue_on_label.yml: -------------------------------------------------------------------------------- 1 | name: Create issue on Jira when labeled with JIRA_ISSUE_LABEL 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | jobs: 8 | jira: 9 | env: 10 | JIRA_ISSUE_LABEL: ${{ secrets.JIRA_ISSUE_LABEL }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Start workflow if GitHub issue is tagged with JIRA_ISSUE_LABEL 15 | if: github.event.label.name == env.JIRA_ISSUE_LABEL 16 | run: echo "Starting workflow" 17 | 18 | - name: Jira Login 19 | if: github.event.label.name == env.JIRA_ISSUE_LABEL 20 | id: login 21 | uses: atlassian/gajira-login@v2.0.0 22 | env: 23 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 24 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 25 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 26 | 27 | - name: Jira Create issue 28 | if: github.event.label.name == env.JIRA_ISSUE_LABEL 29 | id: create_jira_issue 30 | uses: atlassian/gajira-create@v2.0.1 31 | with: 32 | project: ${{ secrets.JIRA_PROJECT }} 33 | issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} 34 | summary: "[GH#${{ github.event.issue.number }}] ${{ github.event.issue.title }}" 35 | description: | 36 | ${{ github.event.issue.body }} 37 | ---- 38 | {panel} 39 | _[Github permalink |${{ github.event.issue.html_url }}]_ 40 | {panel} 41 | 42 | - name: Update Jira issue if JIRA_UPDATE_ISSUE_BODY is defined 43 | if: github.event.label.name == env.JIRA_ISSUE_LABEL && env.JIRA_UPDATE_ISSUE_BODY != '' 44 | env: 45 | JIRA_UPDATE_ISSUE_BODY: ${{ secrets.JIRA_UPDATE_ISSUE_BODY }} 46 | run: > 47 | curl 48 | -u ${{ secrets.JIRA_USER_EMAIL }}:${{ secrets.JIRA_API_TOKEN }} 49 | -X PUT 50 | -H 'Content-Type: application/json' 51 | -d '${{ env.JIRA_UPDATE_ISSUE_BODY }}' 52 | ${{ secrets.JIRA_BASE_URL }}/rest/api/2/issue/${{ steps.create_jira_issue.outputs.issue }} 53 | 54 | - name: Change Title 55 | if: github.event.label.name == env.JIRA_ISSUE_LABEL 56 | uses: actions/github-script@v2.0.0 57 | env: 58 | JIRA_ISSUE_NUMBER: ${{ steps.create_jira_issue.outputs.issue }} 59 | GITHUB_ORIGINAL_TITLE: ${{ github.event.issue.title }} 60 | with: 61 | github-token: ${{secrets.GITHUB_TOKEN}} 62 | script: | 63 | const newTitle = `[${process.env.JIRA_ISSUE_NUMBER}] ${process.env.GITHUB_ORIGINAL_TITLE}` 64 | github.issues.update({ 65 | issue_number: context.issue.number, 66 | owner: context.repo.owner, 67 | repo: context.repo.repo, 68 | title: newTitle 69 | }) 70 | 71 | - name: Add comment after sync 72 | if: github.event.label.name == env.JIRA_ISSUE_LABEL 73 | uses: actions/github-script@v2.0.0 74 | with: 75 | github-token: ${{secrets.GITHUB_TOKEN}} 76 | script: | 77 | github.issues.createComment({ 78 | issue_number: context.issue.number, 79 | owner: context.repo.owner, 80 | repo: context.repo.repo, 81 | body: 'Internal ticket created : [${{ steps.create_jira_issue.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create_jira_issue.outputs.issue }})' 82 | }) 83 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/PhpUnitXmlParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.parsers.implementation.PhpUnitXmlParser 6 | import org.scalatest.{BeforeAndAfterAll, EitherValues} 7 | import org.scalatest.wordspec.AnyWordSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class PhpUnitXmlParserTest extends AnyWordSpec with BeforeAndAfterAll with Matchers with EitherValues { 11 | private val rootPath = "/home/codacy-php/" 12 | private val validReport = "coverage-parser/src/test/resources/phpunitxml/index.xml" 13 | private val incorrectReport = "coverage-parser/src/test/resources/phpunitxml/incorrect_index.xml" 14 | private val coberturaReport = "coverage-parser/src/test/resources/test_cobertura.xml" 15 | private val nonExistentReport = "coverage-parser/src/test/resources/non_existent_file.xml" 16 | private val configPhpFile = "Config.php" 17 | 18 | "parse" should { 19 | "return an invalid report" when { 20 | "report file does not exist" in { 21 | val reader = PhpUnitXmlParser.parse(new File("."), new File(nonExistentReport)) 22 | 23 | reader shouldBe Symbol("left") 24 | } 25 | 26 | "report file has a different format" in { 27 | // use some coverage file that does not follow the PHPUnit xml format 28 | val reader = PhpUnitXmlParser.parse(new File("."), new File(coberturaReport)) 29 | 30 | reader shouldBe Symbol("left") 31 | } 32 | 33 | "report refers to non-existent file coverage report" in { 34 | // this index contains a reference to a file that includes references to non-existent files 35 | val reader = 36 | PhpUnitXmlParser.parse(new File("."), new File(incorrectReport)) 37 | 38 | reader.isLeft shouldBe true 39 | } 40 | } 41 | 42 | "verify if report is valid" in { 43 | val reader = PhpUnitXmlParser 44 | .parse(new File(rootPath), new File(validReport)) 45 | 46 | reader shouldBe Symbol("right") 47 | } 48 | 49 | "return a report with the expected number of files" in { 50 | val report = PhpUnitXmlParser 51 | .parse(new File(rootPath), new File(validReport)) 52 | .value 53 | 54 | report.fileReports.length shouldBe 10 55 | } 56 | 57 | "return a report with the expected file names" in { 58 | val report = PhpUnitXmlParser 59 | .parse(new File(rootPath), new File(validReport)) 60 | .value 61 | 62 | report.fileReports.map(_.filename).sorted shouldBe Seq( 63 | "src/Codacy/Coverage/Api/Api.php", 64 | "src/Codacy/Coverage/CodacyPhpCoverage.php", 65 | "src/Codacy/Coverage/Config.php", 66 | "src/Codacy/Coverage/Git/GitClient.php", 67 | "src/Codacy/Coverage/Parser/CloverParser.php", 68 | "src/Codacy/Coverage/Parser/Parser.php", 69 | "src/Codacy/Coverage/Parser/PhpUnitXmlParser.php", 70 | "src/Codacy/Coverage/Report/CoverageReport.php", 71 | "src/Codacy/Coverage/Report/FileReport.php", 72 | "src/Codacy/Coverage/Report/JsonProducer.php" 73 | ).sorted 74 | } 75 | 76 | "return a report with the expected line coverage" in { 77 | val report = PhpUnitXmlParser 78 | .parse(new File(rootPath), new File(validReport)) 79 | .value 80 | 81 | report.fileReports.find(_.filename.endsWith(configPhpFile)) match { 82 | case None => fail(configPhpFile + " file is not present in the list of file reports") 83 | case Some(fileReport) => 84 | fileReport.coverage shouldBe Map(24 -> 4, 25 -> 4, 26 -> 4, 27 -> 4, 28 -> 4, 29 -> 4) 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/create_issue.yml: -------------------------------------------------------------------------------- 1 | name: Create issue on Jira 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | jira: 9 | env: 10 | JIRA_CREATE_ISSUE_AUTO: ${{ secrets.JIRA_CREATE_ISSUE_AUTO }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Start workflow if JIRA_CREATE_ISSUE_AUTO is enabled 15 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' 16 | run: echo "Starting workflow" 17 | 18 | - name: Jira Login 19 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' 20 | id: login 21 | uses: atlassian/gajira-login@v2.0.0 22 | env: 23 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 24 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 25 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 26 | 27 | - name: Jira Create issue 28 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' 29 | id: create_jira_issue 30 | uses: atlassian/gajira-create@v2.0.1 31 | with: 32 | project: ${{ secrets.JIRA_PROJECT }} 33 | issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} 34 | summary: "[GH#${{ github.event.issue.number }}] ${{ github.event.issue.title }}" 35 | description: | 36 | ${{ github.event.issue.body }} 37 | ---- 38 | {panel} 39 | _[Github permalink |${{ github.event.issue.html_url }}]_ 40 | {panel} 41 | 42 | - name: Update Jira issue if JIRA_UPDATE_ISSUE_BODY is defined 43 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' && env.JIRA_UPDATE_ISSUE_BODY != '' 44 | env: 45 | JIRA_UPDATE_ISSUE_BODY: ${{ secrets.JIRA_UPDATE_ISSUE_BODY }} 46 | run: > 47 | curl 48 | -u ${{ secrets.JIRA_USER_EMAIL }}:${{ secrets.JIRA_API_TOKEN }} 49 | -X PUT 50 | -H 'Content-Type: application/json' 51 | -d '${{ env.JIRA_UPDATE_ISSUE_BODY }}' 52 | ${{ secrets.JIRA_BASE_URL }}/rest/api/2/issue/${{ steps.create_jira_issue.outputs.issue }} 53 | 54 | - name: Update GitHub issue 55 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' 56 | uses: actions/github-script@v2.0.0 57 | env: 58 | JIRA_ISSUE_NUMBER: ${{ steps.create_jira_issue.outputs.issue }} 59 | GITHUB_ORIGINAL_TITLE: ${{ github.event.issue.title }} 60 | JIRA_ISSUE_LABEL: ${{ secrets.JIRA_ISSUE_LABEL }} 61 | with: 62 | github-token: ${{secrets.GITHUB_TOKEN}} 63 | script: | 64 | const newTitle = `[${process.env.JIRA_ISSUE_NUMBER}] ${process.env.GITHUB_ORIGINAL_TITLE}` 65 | github.issues.update({ 66 | issue_number: context.issue.number, 67 | owner: context.repo.owner, 68 | repo: context.repo.repo, 69 | title: newTitle 70 | }) 71 | github.issues.addLabels({ 72 | issue_number: context.issue.number, 73 | owner: context.repo.owner, 74 | repo: context.repo.repo, 75 | labels: [process.env.JIRA_ISSUE_LABEL] 76 | }) 77 | 78 | 79 | - name: Add comment after sync 80 | if: env.JIRA_CREATE_ISSUE_AUTO == 'true' 81 | uses: actions/github-script@v2.0.0 82 | with: 83 | github-token: ${{secrets.GITHUB_TOKEN}} 84 | script: | 85 | github.issues.createComment({ 86 | issue_number: context.issue.number, 87 | owner: context.repo.owner, 88 | repo: context.repo.repo, 89 | body: 'Internal ticket created : [${{ steps.create_jira_issue.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create_jira_issue.outputs.issue }})' 90 | }) 91 | -------------------------------------------------------------------------------- /src/it/scala/com/codacy/CodacyCoverageReporterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.codacy 2 | 3 | import com.codacy.api.OrganizationProvider 4 | import com.codacy.configuration.parser.{BaseCommandConfig, Report} 5 | import com.codacy.di.Components 6 | import com.codacy.model.configuration.ReportConfig 7 | import com.codacy.rules.ConfigurationRules 8 | import org.scalatest.{EitherValues, Matchers, WordSpec} 9 | 10 | import java.io.File 11 | 12 | class CodacyCoverageReporterSpec extends WordSpec with Matchers with EitherValues { 13 | private val apiToken = sys.env.get("TEST_CODACY_API_TOKEN") 14 | private val projectToken = sys.env.get("TEST_CODACY_PROJECT_TOKEN") 15 | private val commitUuid = sys.env.get("TEST_COMMIT_UUID") 16 | private val projectName = sys.env.get("TEST_PROJECT_NAME") 17 | private val username = sys.env.get("TEST_USERNAME") 18 | private val apiBaseUrl = sys.env.get("TEST_BASE_API_URL") 19 | 20 | private def runCoverageReport( 21 | projectToken: Option[String], 22 | apiToken: Option[String], 23 | organizationProvider: Option[OrganizationProvider.Value], 24 | username: Option[String], 25 | projectName: Option[String], 26 | commitUuid: Option[String], 27 | sleepTime: Int, 28 | numRetries: Int 29 | ) = { 30 | val baseConfig = 31 | BaseCommandConfig( 32 | projectToken, 33 | apiToken, 34 | organizationProvider, 35 | username, 36 | projectName, 37 | apiBaseUrl, 38 | commitUuid, 39 | sleepTime, 40 | numRetries 41 | ) 42 | 43 | val commandConfig = Report( 44 | baseConfig = baseConfig, 45 | language = None, 46 | coverageReports = Some(List(new File("src/test/resources/dotcover-example.xml"))), 47 | prefix = None, 48 | forceCoverageParser = None 49 | ) 50 | 51 | val configRules = new ConfigurationRules(commandConfig, Map.empty) 52 | val components = new Components(configRules.validatedConfig.right.value) 53 | 54 | configRules.validatedConfig.right.value match { 55 | case config: ReportConfig => components.reportRules.codacyCoverage(config) 56 | case _ => fail("Config should be of type ReportConfig") 57 | } 58 | } 59 | 60 | "run" should { 61 | "be successful" when { 62 | "using a project token to send coverage" in { 63 | val result = runCoverageReport(projectToken, None, None, None, None, commitUuid, 10000, 3) 64 | 65 | result shouldBe 'right 66 | } 67 | 68 | "using an API token to send coverage" in { 69 | // empty projectToken so we skip project token 70 | // passing None will pick the token used for the codacy-coverage-reporter project 71 | val result = 72 | runCoverageReport( 73 | None, 74 | apiToken, 75 | Option(OrganizationProvider.gh), 76 | username, 77 | projectName, 78 | commitUuid, 79 | 10000, 80 | 3 81 | ) 82 | 83 | result shouldBe 'right 84 | } 85 | } 86 | 87 | "fail" when { 88 | "project token is invalid" in { 89 | val result = runCoverageReport(Some("invalid token"), None, None, None, None, commitUuid, 10000, 3) 90 | 91 | result shouldBe 'left 92 | } 93 | 94 | "API token is invalid" in { 95 | val result = runCoverageReport( 96 | None, 97 | Some("invalid token"), 98 | Option(OrganizationProvider.gh), 99 | username, 100 | projectName, 101 | commitUuid, 102 | 10000, 103 | 3 104 | ) 105 | 106 | result shouldBe 'left 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Report/FileReport.php.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 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/client/CodacyClient.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.client 2 | 3 | import play.api.libs.json._ 4 | import com.codacy.api.util.JsonOps 5 | import scalaj.http.{Http, HttpOptions} 6 | 7 | import java.net.URL 8 | import scala.util.{Failure, Success, Try} 9 | import scala.util.control.NonFatal 10 | 11 | class CodacyClient( 12 | apiUrl: Option[String] = None, 13 | apiToken: Option[String] = None, 14 | projectToken: Option[String] = None, 15 | allowUnsafeSSL: Boolean = false 16 | ) { 17 | 18 | private case class ErrorJson(error: String) 19 | private case class PaginatedResult[T](next: Option[String], values: Seq[T]) 20 | 21 | private implicit val errorJsonFormat: Reads[ErrorJson] = Json.reads[ErrorJson] 22 | 23 | private val tokens = Map.empty[String, String] ++ 24 | apiToken.map(t => "api-token" -> t) ++ 25 | projectToken.map(t => "project-token" -> t) ++ 26 | // This is deprecated and is kept for backward compatibility. It will removed in the context of CY-1272 27 | apiToken.map(t => "api_token" -> t) ++ 28 | projectToken.map(t => "project_token" -> t) 29 | 30 | private val remoteUrl = new URL(new URL(apiUrl.getOrElse("https://api.codacy.com")), "/2.0").toString() 31 | 32 | private def httpOptions = if (allowUnsafeSSL) Seq(HttpOptions.allowUnsafeSSL) else Seq.empty 33 | 34 | /* 35 | * Does an API post 36 | */ 37 | def post[T]( 38 | request: Request[T], 39 | value: String, 40 | timeoutOpt: Option[RequestTimeout] = None, 41 | sleepTime: Option[Int], 42 | numRetries: Option[Int] 43 | )(implicit reads: Reads[T]): RequestResponse[T] = { 44 | val url = s"$remoteUrl/${request.endpoint}" 45 | try { 46 | val headers = tokens ++ Map("Content-Type" -> "application/json") 47 | 48 | val httpRequest = timeoutOpt match { 49 | case Some(timeout) => 50 | Http(url) 51 | .timeout(connTimeoutMs = timeout.connTimeoutMs, readTimeoutMs = timeout.readTimeoutMs) 52 | .options(httpOptions) 53 | case None => Http(url).options(httpOptions) 54 | } 55 | 56 | val body = httpRequest 57 | .params(request.queryParameters) 58 | .headers(headers) 59 | .postData(value) 60 | .asString 61 | .body 62 | 63 | parseJsonAs[T](body) match { 64 | case failure: FailedResponse => 65 | retryPost(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1), failure.message) 66 | case success => success 67 | } 68 | } catch { 69 | case NonFatal(ex) => retryPost(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1), ex.getMessage) 70 | } 71 | } 72 | 73 | private def retryPost[T]( 74 | request: Request[T], 75 | value: String, 76 | timeoutOpt: Option[RequestTimeout], 77 | sleepTime: Option[Int], 78 | numRetries: Option[Int], 79 | failureMessage: String 80 | )(implicit reads: Reads[T]): RequestResponse[T] = { 81 | if (numRetries.exists(x => x > 0)) { 82 | sleepTime.map(x => Thread.sleep(x)) 83 | post(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1)) 84 | } else { 85 | RequestResponse.failure( 86 | s"Error doing a post to $remoteUrl/${request.endpoint}: exhausted retries due to $failureMessage" 87 | ) 88 | } 89 | } 90 | 91 | private def parseJsonAs[T](input: String)(implicit reads: Reads[T]): RequestResponse[T] = { 92 | parseJson(input) match { 93 | case failure: FailedResponse => failure 94 | case SuccessfulResponse(json) => 95 | json 96 | .validate[T] 97 | .fold( 98 | errors => FailedResponse(JsonOps.handleConversionFailure(errors)), 99 | converted => SuccessfulResponse(converted) 100 | ) 101 | } 102 | } 103 | 104 | private def parseJson(input: String): RequestResponse[JsValue] = { 105 | Try(Json.parse(input)) match { 106 | case Success(json) => 107 | json 108 | .validate[ErrorJson] 109 | .fold(_ => SuccessfulResponse(json), apiError => FailedResponse(s"API Error: ${apiError.error}")) 110 | case Failure(exception) => 111 | FailedResponse(s"Failed to parse API response as JSON: $input\nUnderlying exception - ${exception.getMessage}") 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codacy Coverage Reporter 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/c56384a7b0044caea298480b9fde2522)](https://www.codacy.com/gh/codacy/codacy-coverage-reporter/dashboard?utm_source=github.com&utm_medium=referral&utm_content=codacy/codacy-coverage-reporter&utm_campaign=Badge_Grade) 4 | [![Build Status](https://circleci.com/gh/codacy/codacy-coverage-reporter.png?style=shield&circle-token=:circle-token)](https://circleci.com/gh/codacy/codacy-coverage-reporter) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.codacy/codacy-coverage-reporter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.codacy/codacy-coverage-reporter) 6 | 7 | Multi-language coverage reporter for Codacy https://www.codacy.com 8 | 9 | ## Setup 10 | 11 | Follow the instructions on how to [add coverage to your repository](https://docs.codacy.com/coverage-reporter/adding-coverage-to-your-repository/). 12 | 13 | If necessary, see [alternative ways of running Codacy Coverage Reporter](https://docs.codacy.com/coverage-reporter/alternative-ways-of-running-coverage-reporter/) for other ways of running Codacy Coverage Reporter, such as by installing the binary manually or using a CircleCI Orb or the [Codacy Coverage Reporter GitHub Action](https://github.com/codacy/codacy-coverage-reporter-action). 14 | 15 | For a complete list of commands and options, run the Codacy Coverage Reporter with the flag `--help`. For example: 16 | 17 | ``` 18 | $ bash <(curl -Ls https://coverage.codacy.com/get.sh) report --help 19 | ______ __ 20 | / ____/___ ____/ /___ ________ __ 21 | / / / __ \/ __ / __ `/ ___/ / / / 22 | / /___/ /_/ / /_/ / /_/ / /__/ /_/ / 23 | \____/\____/\__,_/\__,_/\___/\__, / 24 | /____/ 25 | 26 | Codacy Coverage Reporter 27 | 28 | --> Using codacy reporter codacy-coverage-reporter-linux from cache 29 | Command: report 30 | Usage: codacy-coverage-reporter report 31 | --project-token | -t 32 | --api-token | -a 33 | --organization-provider (manual, gh, bb, ghe, bbe, gl, gle) 34 | --username | -u 35 | --project-name | -p 36 | --codacy-api-base-url 37 | --commit-uuid 38 | --http-timeout 39 | --skip | -s 40 | --sleep-time 41 | --num-retries 42 | --language | -l 43 | --coverage-reports | -r 44 | --partial 45 | --prefix 46 | --skip-ssl-verification [default: false] - Skip the SSL certificate verification when communicating with the Codacy API 47 | --force-coverage-parser 48 | Available parsers are: opencover,clover,lcov,phpunit,jacoco,dotcover,cobertura,go 49 | 50 | 51 | --> Succeeded! 52 | ``` 53 | 54 | Codacy Coverage Reporter can be run with the `CODACY_REPORTER_OPTIONS` environment variable. This is useful for passing options to the reporter without having to modify the script. 55 | 56 | ``` 57 | CODACY_REPORTER_OPTIONS="--skip-ssl-verification true" 58 | ``` 59 | 60 | ## What is Codacy? 61 | 62 | [Codacy](https://www.codacy.com/) is an Automated Code Review Tool that monitors your technical debt, helps you improve your code quality, teaches best practices to your developers, and helps you save time in Code Reviews. 63 | 64 | ### Among Codacy’s features: 65 | 66 | - Identify new Static Analysis issues 67 | - Commit and Pull Request Analysis with GitHub, GitLab, and Bitbucket 68 | - Auto-comments on Commits and Pull Requests 69 | - Integrations with Slack and Jira 70 | - Track issues in Code Style, Security, Error Proneness, Performance, Unused Code and other categories 71 | 72 | Codacy also helps keep track of Code Coverage, Code Duplication, and Code Complexity. 73 | 74 | Codacy supports PHP, Python, Ruby, Java, JavaScript, and Scala, among others. 75 | 76 | ### Free for Open Source 77 | 78 | Codacy is free for Open Source projects. 79 | -------------------------------------------------------------------------------- /.github/workflows/comment_issue.yml: -------------------------------------------------------------------------------- 1 | name: Comment issue on Jira 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | jira: 9 | env: 10 | JIRA_CREATE_COMMENT_AUTO: ${{ secrets.JIRA_CREATE_COMMENT_AUTO }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Start workflow if JIRA_CREATE_COMMENT_AUTO is enabled 15 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' 16 | run: echo "Starting workflow" 17 | 18 | - name: Check GitHub Issue type 19 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' 20 | id: github_issue_type 21 | uses: actions/github-script@v2.0.0 22 | with: 23 | result-encoding: string 24 | script: | 25 | // An Issue can be a pull request, you can identify pull requests by the pull_request key 26 | const pullRequest = ${{ toJson(github.event.issue.pull_request) }} 27 | if(pullRequest) { 28 | return "pull-request" 29 | } else { 30 | return "issue" 31 | } 32 | 33 | - name: Check if GitHub Issue has JIRA_ISSUE_LABEL 34 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' 35 | id: github_issue_has_jira_issue_label 36 | uses: actions/github-script@v2.0.0 37 | env: 38 | JIRA_ISSUE_LABEL: ${{ secrets.JIRA_ISSUE_LABEL }} 39 | with: 40 | result-encoding: string 41 | script: | 42 | const labels = ${{ toJson(github.event.issue.labels) }} 43 | if(labels.find(label => label.name == process.env.JIRA_ISSUE_LABEL)) { 44 | return "true" 45 | } else { 46 | return "false" 47 | } 48 | 49 | - name: Continue workflow only for Issues (not Pull Requests) tagged with JIRA_ISSUE_LABEL 50 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' && env.GITHUB_ISSUE_TYPE == 'issue' && env.GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL == 'true' 51 | env: 52 | GITHUB_ISSUE_TYPE: ${{ steps.github_issue_type.outputs.result }} 53 | GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL: ${{ steps.github_issue_has_jira_issue_label.outputs.result }} 54 | run: echo "GitHub Issue is tracked on Jira, eligilbe to be commented" 55 | 56 | - name: Jira Login 57 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' && env.GITHUB_ISSUE_TYPE == 'issue' && env.GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL == 'true' 58 | id: login 59 | uses: atlassian/gajira-login@v2.0.0 60 | env: 61 | GITHUB_ISSUE_TYPE: ${{ steps.github_issue_type.outputs.result }} 62 | GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL: ${{ steps.github_issue_has_jira_issue_label.outputs.result }} 63 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 64 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 65 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 66 | 67 | - name: Extract Jira number 68 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' && env.GITHUB_ISSUE_TYPE == 'issue' && env.GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL == 'true' 69 | id: extract_jira_number 70 | uses: actions/github-script@v2.0.0 71 | env: 72 | GITHUB_ISSUE_TYPE: ${{ steps.github_issue_type.outputs.result }} 73 | GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL: ${{ steps.github_issue_has_jira_issue_label.outputs.result }} 74 | JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }} 75 | GITHUB_TITLE: ${{ github.event.issue.title }} 76 | with: 77 | script: | 78 | const jiraTaskRegex = new RegExp(`\\\[(${process.env.JIRA_PROJECT}-[0-9]+?)\\\]`) 79 | return process.env.GITHUB_TITLE.match(jiraTaskRegex)[1] 80 | result-encoding: string 81 | 82 | - name: Jira Add comment on issue 83 | if: env.JIRA_CREATE_COMMENT_AUTO == 'true' && env.GITHUB_ISSUE_TYPE == 'issue' && env.GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL == 'true' 84 | id: add_comment_jira_issue 85 | uses: atlassian/gajira-comment@v2.0.2 86 | env: 87 | GITHUB_ISSUE_TYPE: ${{ steps.github_issue_type.outputs.result }} 88 | GITHUB_ISSUE_HAS_JIRA_ISSUE_LABEL: ${{ steps.github_issue_has_jira_issue_label.outputs.result }} 89 | with: 90 | issue: ${{ steps.extract_jira_number.outputs.result }} 91 | comment: | 92 | GitHub Comment : ${{ github.event.comment.user.login }} 93 | {quote}${{ github.event.comment.body }}{quote} 94 | ---- 95 | {panel} 96 | _[Github permalink |${{ github.event.comment.html_url }}]_ 97 | {panel} 98 | -------------------------------------------------------------------------------- /coverage-parser/src/main/scala/com/codacy/parsers/implementation/CloverParser.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers.implementation 2 | 3 | import java.io.File 4 | import java.nio.file.Paths 5 | 6 | import com.codacy.api.{CoverageFileReport, CoverageReport} 7 | import com.codacy.parsers.util.TextUtils 8 | import com.codacy.parsers.{CoverageParser, XmlReportParser} 9 | 10 | import scala.xml.{Elem, Node, NodeSeq} 11 | 12 | object CloverParser extends CoverageParser with XmlReportParser { 13 | private val CoverageTag = "coverage" 14 | private val ProjectTag = "project" 15 | private val MetricsTag = "metrics" 16 | 17 | override val name: String = "Clover" 18 | 19 | override def parse(rootProject: File, reportFile: File): Either[String, CoverageReport] = { 20 | parseReport(reportFile, s"Could not find tag hierarchy <$CoverageTag> <$ProjectTag> <$MetricsTag> tags") { node => 21 | parseReportNode(rootProject, node) 22 | } 23 | } 24 | 25 | override def validateSchema(xml: Elem): Boolean = (xml \\ CoverageTag \ ProjectTag \ MetricsTag).nonEmpty 26 | 27 | override def getRootNode(xml: Elem): NodeSeq = xml \\ CoverageTag 28 | 29 | private def parseReportNode(rootProject: File, report: NodeSeq): Either[String, CoverageReport] = { 30 | val rootPath = TextUtils.sanitiseFilename(rootProject.getAbsolutePath) 31 | 32 | val coverageFiles = (report \\ "file").foldLeft[Either[String, Seq[CoverageFileReport]]](Right(List())) { 33 | case (Right(accumulatedFileReports), fileTag) => 34 | val fileReport = getCoverageFileReport(rootPath, fileTag) 35 | fileReport.map(_ +: accumulatedFileReports) 36 | 37 | case (Left(errorMessage), _) => Left(errorMessage) 38 | } 39 | 40 | for { 41 | coverageFiles <- coverageFiles 42 | } yield CoverageReport(coverageFiles) 43 | } 44 | 45 | private def getCoverageFileReport(rootPath: String, fileNode: Node): Either[String, CoverageFileReport] = { 46 | val filePath = getUnixPathAttribute(fileNode, "path") 47 | val filename = getUnixPathAttribute(fileNode, "name") 48 | 49 | val relativeFilePath = filePath 50 | .orElse(filename) 51 | .fold[Either[String, String]] { 52 | Left("Could not read file path due to missing 'path' and 'name' attributes in the report file tag.") 53 | } { 54 | case path if Paths.get(path).isAbsolute => 55 | Right(path.stripPrefix(rootPath).stripPrefix("/")) 56 | 57 | case path => 58 | Right(path) 59 | } 60 | 61 | for { 62 | relativeFilePath <- relativeFilePath 63 | linesCoverage <- getLinesCoverage(fileNode).left 64 | .map(errorMessage => s"Could not retrieve lines coverage for file '$relativeFilePath': $errorMessage") 65 | } yield CoverageFileReport(relativeFilePath, linesCoverage) 66 | } 67 | 68 | private def getLinesCoverage(fileNode: Node): Either[String, Map[Int, Int]] = { 69 | val fileLineTags = (fileNode \ "line") 70 | 71 | fileLineTags.foldLeft[Either[String, Map[Int, Int]]](Right(Map.empty[Int, Int])) { 72 | case (left: Left[_, _], _) => left 73 | 74 | case (Right(lines), line) 75 | if ((line \@ "type") == "stmt" || (line \@ "type") == "cond") && (line \@ "count").nonEmpty => 76 | val lineCoverage = for { 77 | lineNumber <- getFirstNonEmptyValueAsInt(Seq(line), "num") 78 | countOfExecutions <- getFirstNonEmptyValueAsInt(Seq(line), "count") 79 | } yield (lineNumber, countOfExecutions) 80 | lineCoverage.map(lines + _) 81 | 82 | case (accumulated, _) => accumulated 83 | } 84 | } 85 | 86 | /* Retrieves the attribute with name @attributeName from @node, 87 | * converts the contents to string and converts path to unix style 88 | */ 89 | private def getUnixPathAttribute(node: Node, attributeName: String): Option[String] = { 90 | node.attribute(attributeName).flatMap(_.headOption.map(_.text)).map(TextUtils.sanitiseFilename) 91 | } 92 | 93 | /* Retrieves the first non-empty value in attribute with name @attributeName from @nodes 94 | * and converts the value to a number 95 | */ 96 | private def getFirstNonEmptyValueAsInt(nodes: Seq[Node], attributeName: String): Either[String, Int] = { 97 | val attributeValues = nodes 98 | .flatMap(_.attribute(attributeName).getOrElse(Seq.empty[Node])) 99 | 100 | val firstNonEmptyValue = attributeValues 101 | .map(_.text.trim) 102 | .find(_.nonEmpty) 103 | 104 | val result = firstNonEmptyValue.fold[Either[String, String]]( 105 | Left(s"Could not find attribute with name '$attributeName'") 106 | )(Right(_)) 107 | 108 | result.flatMap { attributeValue => 109 | parseInt(attributeValue).left.flatMap(_ => Left(s"Value of attribute with name '$attributeName' is not a number")) 110 | } 111 | } 112 | 113 | /* Parses string value into a number */ 114 | private def parseInt(value: String): Either[String, Int] = { 115 | try { 116 | Right(value.toInt) 117 | } catch { 118 | case _: NumberFormatException => 119 | Left(s"Value '$value' is not a number") 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/phpunitxml/Parser/PhpUnitXmlParser.php.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 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/scala/com/codacy/rules/commituuid/CommitUUIDProvider.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.rules.commituuid 2 | 3 | import java.io.File 4 | 5 | import com.codacy.rules.commituuid.providers._ 6 | import com.codacy.model.configuration.CommitUUID 7 | import wvlet.log.LogSupport 8 | 9 | import com.codacy.api.helpers.vcs.{CommitInfo, GitClient} 10 | import scala.util.{Failure, Success} 11 | 12 | /** 13 | * Trait for a provider that can get a [[CommitUUID]] from environment variables. 14 | */ 15 | trait CommitUUIDProvider { 16 | 17 | /** Name of the provider */ 18 | val name: String 19 | 20 | /** Default error message */ 21 | protected val commitNotFoundMessage = s"Can't find $name commit SHA-1 hash in the environment." 22 | 23 | protected val commitNotValidMessage = 24 | "Commit SHA-1 hash isn't valid. Make sure it consists of 40 hexadecimal characters." 25 | 26 | /** Parses `environmentVariable` as a [[CommitUUID]], validates it and returns it, as a [[Right]]. 27 | * 28 | * If the parsing or validation fail, return an error message, as a [[Left]]. 29 | * 30 | * @param environmentVariable An option for the value of an environment variable. 31 | * @return Commit uuid or an error message 32 | */ 33 | protected def parseEnvironmentVariable(environmentVariable: Option[String]): Either[String, CommitUUID] = 34 | environmentVariable match { 35 | case Some(commitUUID) => CommitUUID.fromString(commitUUID) 36 | case None => Left(commitNotFoundMessage) 37 | } 38 | 39 | /** Validates if the environment has the expected variables for this provider. 40 | * 41 | * @param environment environment variables. 42 | * @return `true` if all variables are defined, `false` otherwise. 43 | */ 44 | def validateEnvironment(environment: Map[String, String]): Boolean 45 | 46 | /** Gets a **valid** commit UUID, as [[Right]], from the environment if one exists under the 47 | * expected name and [[CommitUUID.isValid is valid]]. 48 | * Otherwise, get an error message, as [[Left]], to present to the user. 49 | * 50 | * @param environment environment variables. 51 | * @return Either a **valid** commit UUID or an error message. 52 | */ 53 | def getValidCommitUUID(environment: Map[String, String]): Either[String, CommitUUID] 54 | } 55 | 56 | /** 57 | * Provides a commit UUID 58 | * 59 | * This object provides a commit uuid from various providers 60 | */ 61 | object CommitUUIDProvider extends LogSupport { 62 | 63 | private val providers = List( 64 | AppveyorProvider, 65 | ArgoCDProvider, 66 | AWSCodeBuildProvider, 67 | AzurePipelinesProvider, 68 | BitbucketCloudProvider, 69 | BitriseCIProvider, 70 | BuildkiteCIProvider, 71 | CircleCIProvider, 72 | CodefreshCIProvider, 73 | CodeshipCIProvider, 74 | DockerProvider, 75 | DroneCIProvider, 76 | GitHubActionProvider, 77 | GitlabProvider, 78 | GreenhouseCIProvider, 79 | HerokuCIProvider, 80 | JenkinsProvider, 81 | MagnumCIProvider, 82 | SemaphoreCIProvider, 83 | ShippableCIProvider, 84 | SolanoCIProvider, 85 | TeamCityProvider, 86 | TravisCIProvider, 87 | WerckerCIProvider 88 | ) 89 | 90 | /** Try to get a commit UUID from all available providers. 91 | * 92 | * @param environment environment variables. 93 | * @return Either a commit uuid or an error message. 94 | */ 95 | def getFromAll(environment: Map[String, String]): Either[String, CommitUUID] = { 96 | getFromEnvironment(environment) match { 97 | case Left(msg) => 98 | logger.info(msg) 99 | logger.info("Trying to get commit SHA-1 hash from local Git directory") 100 | getLatestFromGit() 101 | case uuid => uuid 102 | } 103 | } 104 | 105 | /** Gets the latest commit uuid from the local git directory. 106 | * 107 | * @return Either a commit uuid or an error message. 108 | */ 109 | def getLatestFromGit(): Either[String, CommitUUID] = { 110 | val currentPath = new File(System.getProperty("user.dir")) 111 | new GitClient(currentPath).latestCommitInfo match { 112 | case Failure(e) => 113 | Left("Commit SHA-1 hash not provided and could not retrieve it from local Git directory") 114 | case Success(CommitInfo(uuid, authorName, authorEmail, date)) => 115 | val info = 116 | s"""Commit SHA-1 hash not provided, using latest commit of local Git directory: 117 | |$uuid $authorName <$authorEmail> $date""".stripMargin 118 | 119 | logger.info(info) 120 | CommitUUID.fromString(uuid) 121 | } 122 | } 123 | 124 | /** Get a commit UUID from the first provider that can find a valid commit UUID in `environment`. 125 | * 126 | * @param environment environment variables. 127 | * 128 | * @see [[providers]] to check the order for which providers are tested. 129 | */ 130 | def getFromEnvironment(environment: Map[String, String]): Either[String, CommitUUID] = { 131 | 132 | val validUUID = providers.collectFirst { 133 | case provider if provider.validateEnvironment(environment) && provider.getValidCommitUUID(environment).isRight => 134 | val uuid = provider.getValidCommitUUID(environment) 135 | uuid.foreach(u => logger.info(s"CI/CD provider ${provider.name} found Commit UUID ${u.value}")) 136 | uuid 137 | } 138 | 139 | validUUID 140 | .toRight("Can't find or validate commit SHA-1 hash from any supported CI/CD provider.") 141 | .flatMap(identity) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /coverage-parser/src/test/scala/com/codacy/parsers/CloverParserTest.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.parsers 2 | 3 | import java.io.File 4 | 5 | import com.codacy.parsers.implementation.CloverParser 6 | import org.scalatest.EitherValues 7 | import org.scalatest.matchers.should.Matchers 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class CloverParserTest extends AnyWordSpec with Matchers with EitherValues { 11 | 12 | "parse" should { 13 | 14 | "fail to parse an invalid report" when { 15 | 16 | "the report file does not exist" in { 17 | // Arrange 18 | val nonExistentReportPath = "coverage-parser/src/test/resources/non-existent.xml" 19 | 20 | // Act 21 | val parseResult = CloverParser.parse(new File("."), new File(nonExistentReportPath)) 22 | 23 | // Assert 24 | val error = parseResult.left.value 25 | error should startWith("Unparseable report. ") 26 | error should endWith("coverage-parser/src/test/resources/non-existent.xml (No such file or directory)") 27 | } 28 | 29 | "the report is not in the Clover format" in { 30 | // Arrange 31 | val reportNotInCloverFormat = "coverage-parser/src/test/resources/test_cobertura.xml" 32 | 33 | // Act 34 | val parseResult = CloverParser.parse(new File("."), new File(reportNotInCloverFormat)) 35 | 36 | // Assert 37 | parseResult shouldBe Left("Invalid report. Could not find tag hierarchy tags.") 38 | } 39 | } 40 | 41 | val cloverReportPath = "coverage-parser/src/test/resources/test_clover.xml" 42 | val cloverWithoutPackagesFilePath = "coverage-parser/src/test/resources/test_clover_without_packages.xml" 43 | 44 | "succeed to parse a valid report" when { 45 | 46 | "the report has packages" in { 47 | // Act 48 | val parseResult = CloverParser.parse(new File("."), new File(cloverReportPath)) 49 | 50 | // Assert 51 | parseResult shouldBe Symbol("right") 52 | } 53 | 54 | "the report does not have packages" in { 55 | // Act 56 | val parseResult = CloverParser 57 | .parse(new File("/home/codacy-php/"), new File(cloverWithoutPackagesFilePath)) 58 | 59 | // Assert 60 | parseResult shouldBe Symbol("right") 61 | } 62 | 63 | } 64 | 65 | "parse correct file paths" when { 66 | 67 | "reports contain both name and path attributes in file tags" in { 68 | // Arrange 69 | val cloverWithPaths = new File("coverage-parser/src/test/resources/test_clover_with_paths.xml") 70 | 71 | // Act 72 | val parsedReportFilePaths = 73 | CloverParser 74 | .parse(new File("/Users/username/workspace/repository"), cloverWithPaths) 75 | .value 76 | .fileReports 77 | .map(_.filename) 78 | 79 | // Assert 80 | parsedReportFilePaths should contain("src/app/file.js") 81 | } 82 | 83 | } 84 | 85 | "return the same report with or without packages" in { 86 | // Act 87 | val parseResultWithoutPackages = CloverParser 88 | .parse(new File("/home/codacy-php/"), new File(cloverWithoutPackagesFilePath)) 89 | val parseResultWithPackages = 90 | CloverParser.parse(new File("/home/codacy-php/"), new File(cloverReportPath)) 91 | 92 | // Assert 93 | parseResultWithoutPackages shouldBe parseResultWithPackages 94 | } 95 | 96 | "return a report with the expected number of files" in { 97 | // Arrange 98 | val expectedNumberOfFiles = 5 99 | 100 | // Act 101 | val fileReports = 102 | CloverParser.parse(new File("/home/codacy-php/"), new File(cloverReportPath)).value.fileReports 103 | 104 | // Assert 105 | fileReports should have length expectedNumberOfFiles 106 | } 107 | 108 | "return a report with the expected relative file paths" in { 109 | // Arrange 110 | val expectedFilePaths = Seq( 111 | "src/Codacy/Coverage/Parser/Parser.php", 112 | "src/Codacy/Coverage/Report/CoverageReport.php", 113 | "vendor/sebastian/global-state/src/Blacklist.php", 114 | "vendor/sebastian/global-state/src/Restorer.php", 115 | "vendor/sebastian/global-state/src/Snapshot.php" 116 | ) 117 | 118 | // Act 119 | val parsedReportFilePaths = 120 | CloverParser 121 | .parse(new File("/home/codacy-php/"), new File(cloverReportPath)) 122 | .value 123 | .fileReports 124 | .map(_.filename) 125 | 126 | // Assert 127 | parsedReportFilePaths should contain theSameElementsAs expectedFilePaths 128 | } 129 | 130 | "return a report with the expected file line coverage" in { 131 | // Arrange 132 | val filePath = "src/Codacy/Coverage/Report/CoverageReport.php" 133 | 134 | // Act 135 | val fileLineCoverage = 136 | CloverParser 137 | .parse(new File("/home/codacy-php/"), new File(cloverReportPath)) 138 | .value 139 | .fileReports 140 | .find(_.filename == filePath) 141 | .getOrElse(fail(s"Could not find report for file:$filePath")) 142 | .coverage 143 | 144 | // Assert 145 | fileLineCoverage shouldBe Map( 146 | 11 -> 1, 147 | 12 -> 1, 148 | 13 -> 1, 149 | 16 -> 1, 150 | 19 -> 0, 151 | 30 -> 0, 152 | 31 -> 0, 153 | 32 -> 0, 154 | 33 -> 0, 155 | 36 -> 0, 156 | 39 -> 0, 157 | 42 -> 0 158 | ) 159 | } 160 | 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /api-scala/src/main/scala/com/codacy/api/service/CoverageServices.scala: -------------------------------------------------------------------------------- 1 | package com.codacy.api.service 2 | 3 | import com.codacy.api.client.{CodacyClient, Request, RequestResponse, RequestSuccess, RequestTimeout} 4 | import com.codacy.api.{CoverageReport, OrganizationProvider} 5 | import play.api.libs.json.Json 6 | 7 | class CoverageServices(client: CodacyClient) { 8 | 9 | /** 10 | * Send coverage report to Codacy endpoint. 11 | * This endpoint requires a project token to authenticate the request and identify the project. 12 | * Therefore, the client must be initialized with a valid project token. 13 | * @param commitUuid commit unique identifier 14 | * @param language programing language 15 | * @param coverageReport coverage report being sent to Codacy 16 | * @param partial flag that signals if the report operation will be broken in multiple operations 17 | * @param timeoutOpt socket connection and read timeouts in milliseconds 18 | * @return Request response 19 | */ 20 | def sendReport( 21 | commitUuid: String, 22 | language: String, 23 | coverageReport: CoverageReport, 24 | partial: Boolean = false, 25 | timeoutOpt: Option[RequestTimeout] = None, 26 | sleepTime: Option[Int] = None, 27 | numRetries: Option[Int] = None, 28 | ): RequestResponse[RequestSuccess] = { 29 | val endpoint = s"coverage/$commitUuid/${encodePathSegment(language.toLowerCase)}" 30 | 31 | postRequest(endpoint, coverageReport, partial, timeoutOpt, sleepTime, numRetries) 32 | } 33 | 34 | /** 35 | * Send final notification signaling the end of the report operation. 36 | * This endpoint requires an account token to authenticate the request and identify the project. 37 | * Therefore, the client must be initialized with a valid account token. 38 | * @param commitUuid commit unique identifier 39 | * @param timeoutOpt socket connection and read timeouts in milliseconds 40 | * @return Request Response 41 | */ 42 | def sendFinalNotification( 43 | commitUuid: String, 44 | timeoutOpt: Option[RequestTimeout] = None, 45 | sleepTime: Option[Int] = None, 46 | numRetries: Option[Int] = None, 47 | ): RequestResponse[RequestSuccess] = { 48 | val endpoint = s"commit/$commitUuid/coverageFinal" 49 | 50 | postEmptyRequest(endpoint, timeoutOpt, sleepTime, numRetries) 51 | } 52 | 53 | /** 54 | * Send coverage report with a project name to Codacy endpoint. 55 | * This endpoint requires an account token to authenticate the request. 56 | * Therefore, the client must be initialized with a valid account token. 57 | * @param username reporter's username 58 | * @param projectName name of the project the report pertains 59 | * @param commitUuid commit unique identifier 60 | * @param language programing language 61 | * @param coverageReport coverage report being reported 62 | * @param partial flag that signals if the report operation will be broken in multiple operations 63 | * @param timeoutOpt socket connection and read timeouts in milliseconds 64 | * @return Request Response 65 | */ 66 | def sendReportWithProjectName( 67 | provider: OrganizationProvider.Value, 68 | username: String, 69 | projectName: String, 70 | commitUuid: String, 71 | language: String, 72 | coverageReport: CoverageReport, 73 | partial: Boolean = false, 74 | timeoutOpt: Option[RequestTimeout] = None, 75 | sleepTime: Option[Int] = None, 76 | numRetries: Option[Int] = None, 77 | ): RequestResponse[RequestSuccess] = { 78 | val endpoint = 79 | s"${provider.toString}/$username/$projectName/commit/$commitUuid/coverage/${encodePathSegment(language.toLowerCase)}" 80 | postRequest(endpoint, coverageReport, partial, timeoutOpt, sleepTime, numRetries) 81 | } 82 | 83 | /** 84 | * Send final notification with a project name, signaling the end of the report operation. 85 | * This endpoint requires an account token to authenticate the request. 86 | * Therefore, the client must be initialized with a valid account token. 87 | * @param username reporter's username 88 | * @param projectName name of the project the report pertains 89 | * @param commitUuid commit unique identifier 90 | * @param timeoutOpt socket connection and read timeouts in milliseconds 91 | * @return Request Response 92 | */ 93 | def sendFinalWithProjectName( 94 | provider: OrganizationProvider.Value, 95 | username: String, 96 | projectName: String, 97 | commitUuid: String, 98 | timeoutOpt: Option[RequestTimeout] = None, 99 | sleepTime: Option[Int] = None, 100 | numRetries: Option[Int] = None 101 | ): RequestResponse[RequestSuccess] = { 102 | val endpoint = s"${provider.toString}/$username/$projectName/commit/$commitUuid/coverageFinal" 103 | 104 | postEmptyRequest(endpoint, timeoutOpt, sleepTime, numRetries) 105 | } 106 | 107 | private def postRequest( 108 | endpoint: String, 109 | coverageReport: CoverageReport, 110 | partial: Boolean, 111 | timeoutOpt: Option[RequestTimeout], 112 | sleepTime: Option[Int], 113 | numRetries: Option[Int] 114 | ) = { 115 | val queryParams = getQueryParameters(partial) 116 | 117 | val jsonString = serializeCoverageReport(coverageReport) 118 | 119 | client.post(Request(endpoint, classOf[RequestSuccess], queryParams), jsonString, timeoutOpt, sleepTime, numRetries) 120 | } 121 | 122 | private def postEmptyRequest( 123 | endpoint: String, 124 | timeoutOpt: Option[RequestTimeout], 125 | sleepTime: Option[Int], 126 | numRetries: Option[Int] 127 | ) = 128 | client.post(Request(endpoint, classOf[RequestSuccess]), "{}", timeoutOpt, sleepTime, numRetries) 129 | 130 | private def getQueryParameters(partial: Boolean) = { 131 | Map("partial" -> partial.toString) 132 | } 133 | private def serializeCoverageReport(coverageReport: CoverageReport) = 134 | Json.stringify(Json.toJson(coverageReport)) 135 | 136 | /** 137 | * Any encoding that we do here, needs to have the same output 138 | * of play.utils.UriEncoding.encodePathSegment for our languages. 139 | * https://github.com/playframework/playframework/blob/316fbd61c9fc6a6081a3aeef7e773c8bbccd0b6b/core/play/src/main/scala/play/utils/UriEncoding.scala#L50 140 | */ 141 | private def encodePathSegment(segment: String): String = segment.replaceAll(" ", "%20") 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/test/resources/non-git-report.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 | -------------------------------------------------------------------------------- /src/test/resources/dotcover-example.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 | -------------------------------------------------------------------------------- /coverage-parser/src/test/resources/test_dotcover.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 | --------------------------------------------------------------------------------