diff --git a/Project.toml b/Project.toml index 9b00fbe..d38d86f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Coverage" uuid = "a2441757-f6aa-5fb2-8edb-039e3f45d037" authors = ["Iain Dunning ", "contributors"] -version = "1.3.0" +version = "1.3.1" [deps] CoverageTools = "c36e975a-824b-4404-a568-ef97ca766997" diff --git a/src/Coverage.jl b/src/Coverage.jl index 66647fb..7f0b683 100644 --- a/src/Coverage.jl +++ b/src/Coverage.jl @@ -1,40 +1,36 @@ -####################################################################### -# Coverage.jl -# Input: Code coverage and memory allocations -# Output: Useful things -# https://github.com/JuliaCI/Coverage.jl -####################################################################### module Coverage - using CoverageTools - using LibGit2 - export FileCoverage - export LCOV - export analyze_malloc - export amend_coverage_from_src! - export clean_file - export clean_folder - export get_summary - export merge_coverage_counts - export process_cov - export process_file - export process_folder +using CoverageTools +using LibGit2 - const CovCount = CoverageTools.CovCount - const FileCoverage = CoverageTools.FileCoverage - const amend_coverage_from_src! = CoverageTools.amend_coverage_from_src! - const clean_file = CoverageTools.clean_file - const clean_folder = CoverageTools.clean_folder - const get_summary = CoverageTools.get_summary - const iscovfile = CoverageTools.iscovfile - const merge_coverage_counts = CoverageTools.merge_coverage_counts - const process_cov = CoverageTools.process_cov - const process_file = CoverageTools.process_file - const process_folder = CoverageTools.process_folder +export FileCoverage +export LCOV +export analyze_malloc +export amend_coverage_from_src! +export clean_file +export clean_folder +export get_summary +export merge_coverage_counts +export process_cov +export process_file +export process_folder - include("coveralls.jl") - include("codecovio.jl") - include("lcov.jl") - include("memalloc.jl") - include("parser.jl") -end +const CovCount = CoverageTools.CovCount +const FileCoverage = CoverageTools.FileCoverage +const amend_coverage_from_src! = CoverageTools.amend_coverage_from_src! +const clean_file = CoverageTools.clean_file +const clean_folder = CoverageTools.clean_folder +const get_summary = CoverageTools.get_summary +const iscovfile = CoverageTools.iscovfile +const merge_coverage_counts = CoverageTools.merge_coverage_counts +const process_cov = CoverageTools.process_cov +const process_file = CoverageTools.process_file +const process_folder = CoverageTools.process_folder + +include("coveralls.jl") +include("codecovio.jl") +include("lcov.jl") +include("memalloc.jl") +include("parser.jl") + +end # module diff --git a/src/codecovio.jl b/src/codecovio.jl index a8a8d50..d798b68 100644 --- a/src/codecovio.jl +++ b/src/codecovio.jl @@ -7,285 +7,286 @@ This module provides functionality to push coverage information to the CodeCov.i web service. It exports the `submit` and `submit_local` methods. """ module Codecov - using HTTP - using Coverage - using CoverageTools - using JSON - using LibGit2 - - export submit, submit_local, submit_generic - - #= - JSON structure for Codecov.io - https://codecov.io/api#post-report - - { - "coverage": { - "path/to/file.py": [null, 1, 0, null, true, 0, 0, 1, 1], - "path/to/other.py": [null, 0, 1, 1, "1/3", null] - }, - "messages": { - "path/to/other.py": { - "1": "custom message for line 1" - } - } - } - =# - # Turn vector of filename : coverage pairs into a dictionary - function to_json(fcs::Vector{FileCoverage}) - cov = Dict() - for fc in fcs - cov[fc.filename] = vcat(nothing, fc.coverage) - end - return Dict("coverage" => cov) +using HTTP +using Coverage +using CoverageTools +using JSON +using LibGit2 + +export submit, submit_local, submit_generic + +#= +JSON structure for Codecov.io +https://codecov.io/api#post-report + +{ + "coverage": { + "path/to/file.py": [null, 1, 0, null, true, 0, 0, 1, 1], + "path/to/other.py": [null, 0, 1, 1, "1/3", null] + }, + "messages": { + "path/to/other.py": { + "1": "custom message for line 1" + } + } +} +=# + +# Turn vector of filename : coverage pairs into a dictionary +function to_json(fcs::Vector{FileCoverage}) + cov = Dict() + for fc in fcs + cov[fc.filename] = vcat(nothing, fc.coverage) end + return Dict("coverage" => cov) +end - """ - kwargs provides default values to insert into args_array, only if they are - not already specified in args_array. - """ - function set_defaults(args_array::Dict; kwargs...) - args_array = copy(args_array) - for kwarg in kwargs - if !haskey(args_array, kwarg[1]) - push!(args_array, kwarg) - end +""" +kwargs provides default values to insert into args_array, only if they are +not already specified in args_array. +""" +function set_defaults(args_array::Dict; kwargs...) + args_array = copy(args_array) + for kwarg in kwargs + if !haskey(args_array, kwarg[1]) + push!(args_array, kwarg) end - return args_array - end - - """ - submit(fcs::Vector{FileCoverage}) - - Takes a vector of file coverage results (produced by `process_folder`), - and submits them to Codecov.io. Assumes that this code is being run - on TravisCI or AppVeyor. If running locally, use `submit_local`. - """ - function submit(fcs::Vector{FileCoverage}; kwargs...) - submit_generic(fcs, add_ci_to_kwargs(; kwargs...)) end + return args_array +end +""" + submit(fcs::Vector{FileCoverage}) - add_ci_to_kwargs(; kwargs...) = add_ci_to_kwargs(Dict{Symbol,Any}(kwargs)) - function add_ci_to_kwargs(kwargs::Dict) - if lowercase(get(ENV, "APPVEYOR", "false")) == "true" - appveyor_pr = get(ENV, "APPVEYOR_PULL_REQUEST_NUMBER", "") - appveyor_job = join( - [ - ENV["APPVEYOR_ACCOUNT_NAME"], - ENV["APPVEYOR_PROJECT_SLUG"], - ENV["APPVEYOR_BUILD_VERSION"], - ], - "%2F", - ) - kwargs = set_defaults(kwargs, - service = "appveyor", - branch = ENV["APPVEYOR_REPO_BRANCH"], - commit = ENV["APPVEYOR_REPO_COMMIT"], - pull_request = appveyor_pr, - job = appveyor_job, - slug = ENV["APPVEYOR_REPO_NAME"], - build = ENV["APPVEYOR_JOB_ID"], - ) - elseif lowercase(get(ENV, "TRAVIS", "false")) == "true" - kwargs = set_defaults(kwargs, - service = "travis-org", - branch = ENV["TRAVIS_BRANCH"], - commit = ENV["TRAVIS_COMMIT"], - pull_request = ENV["TRAVIS_PULL_REQUEST"], - job = ENV["TRAVIS_JOB_ID"], - slug = ENV["TRAVIS_REPO_SLUG"], - build = ENV["TRAVIS_JOB_NUMBER"], - ) - elseif lowercase(get(ENV, "CIRCLECI", "false")) == "true" - circle_slug = join( - [ - ENV["CIRCLE_PROJECT_USERNAME"], - ENV["CIRCLE_PROJECT_REPONAME"], - ], - "%2F", - ) - kwargs = set_defaults(kwargs, - service = "circleci", - branch = ENV["CIRCLE_BRANCH"], - commit = ENV["CIRCLE_SHA1"], - pull_request = get(ENV, "CIRCLE_PR_NUMBER", "false"), # like Travis - build_url = ENV["CIRCLE_BUILD_URL"], - slug = circle_slug, - build = ENV["CIRCLE_BUILD_NUM"], - ) - elseif lowercase(get(ENV, "JENKINS", "false")) == "true" - kwargs = set_defaults(kwargs, - service = "jenkins", - branch = ENV["GIT_BRANCH"], - commit = ENV["GIT_COMMIT"], - job = ENV["JOB_NAME"], - build = ENV["BUILD_ID"], - build_url = ENV["BUILD_URL"], - jenkins_url = ENV["JENKINS_URL"], - ) - elseif haskey(ENV, "BUILD_BUILDURI") # Azure Pipelines - ref = get(ENV, "SYSTEM_PULLREQUEST_TARGETBRANCH", ENV["BUILD_SOURCEBRANCHNAME"]) - branch = startswith(ref, "refs/heads/") ? ref[12:end] : ref - kwargs = set_defaults(kwargs, - service = "azure_pipelines", - branch = branch, - commit = ENV["BUILD_SOURCEVERSION"], - pull_request = get(ENV, "SYSTEM_PULLREQUEST_PULLREQUESTNUMBER", ""), - job = ENV["BUILD_DEFINITIONNAME"], - slug = ENV["BUILD_REPOSITORY_NAME"], - build = ENV["BUILD_BUILDID"], - ) - elseif haskey(ENV, "GITHUB_ACTION") # GitHub Actions - event_path = open(JSON.Parser.parse, ENV["GITHUB_EVENT_PATH"]) - ref = ENV["GITHUB_REF"] - if startswith(ref, "refs/heads/") - branch = ref[12:end] - ga_pr = "false" - elseif startswith(ref, "refs/tags/") - branch = ref[11:end] - ga_pr = "false" - elseif startswith(ref, "refs/pull/") - branch = ENV["GITHUB_HEAD_REF"] - ga_pr_info = get(event_path, "pull_request", Dict()) - ga_pr = get(ga_pr_info, "number", "false") - end - ga_build_url = "https://github.com/$(ENV["GITHUB_REPOSITORY"])/actions/runs/$(ENV["GITHUB_RUN_ID"])" - kwargs = set_defaults(kwargs, - service = "github-actions", - branch = branch, - commit = ENV["GITHUB_SHA"], - pull_request = ga_pr, - slug = ENV["GITHUB_REPOSITORY"], - build = ENV["GITHUB_RUN_ID"], - build_url = ga_build_url, - ) - else - error("No compatible CI platform detected") +Takes a vector of file coverage results (produced by `process_folder`), +and submits them to Codecov.io. Assumes that this code is being run +on TravisCI or AppVeyor. If running locally, use `submit_local`. +""" +function submit(fcs::Vector{FileCoverage}; kwargs...) + submit_generic(fcs, add_ci_to_kwargs(; kwargs...)) +end + + +add_ci_to_kwargs(; kwargs...) = add_ci_to_kwargs(Dict{Symbol,Any}(kwargs)) +function add_ci_to_kwargs(kwargs::Dict) + if lowercase(get(ENV, "APPVEYOR", "false")) == "true" + appveyor_pr = get(ENV, "APPVEYOR_PULL_REQUEST_NUMBER", "") + appveyor_job = join( + [ + ENV["APPVEYOR_ACCOUNT_NAME"], + ENV["APPVEYOR_PROJECT_SLUG"], + ENV["APPVEYOR_BUILD_VERSION"], + ], + "%2F", + ) + kwargs = set_defaults(kwargs, + service = "appveyor", + branch = ENV["APPVEYOR_REPO_BRANCH"], + commit = ENV["APPVEYOR_REPO_COMMIT"], + pull_request = appveyor_pr, + job = appveyor_job, + slug = ENV["APPVEYOR_REPO_NAME"], + build = ENV["APPVEYOR_JOB_ID"], + ) + elseif lowercase(get(ENV, "TRAVIS", "false")) == "true" + kwargs = set_defaults(kwargs, + service = "travis-org", + branch = ENV["TRAVIS_BRANCH"], + commit = ENV["TRAVIS_COMMIT"], + pull_request = ENV["TRAVIS_PULL_REQUEST"], + job = ENV["TRAVIS_JOB_ID"], + slug = ENV["TRAVIS_REPO_SLUG"], + build = ENV["TRAVIS_JOB_NUMBER"], + ) + elseif lowercase(get(ENV, "CIRCLECI", "false")) == "true" + circle_slug = join( + [ + ENV["CIRCLE_PROJECT_USERNAME"], + ENV["CIRCLE_PROJECT_REPONAME"], + ], + "%2F", + ) + kwargs = set_defaults(kwargs, + service = "circleci", + branch = ENV["CIRCLE_BRANCH"], + commit = ENV["CIRCLE_SHA1"], + pull_request = get(ENV, "CIRCLE_PR_NUMBER", "false"), # like Travis + build_url = ENV["CIRCLE_BUILD_URL"], + slug = circle_slug, + build = ENV["CIRCLE_BUILD_NUM"], + ) + elseif lowercase(get(ENV, "JENKINS", "false")) == "true" + kwargs = set_defaults(kwargs, + service = "jenkins", + branch = ENV["GIT_BRANCH"], + commit = ENV["GIT_COMMIT"], + job = ENV["JOB_NAME"], + build = ENV["BUILD_ID"], + build_url = ENV["BUILD_URL"], + jenkins_url = ENV["JENKINS_URL"], + ) + elseif haskey(ENV, "BUILD_BUILDURI") # Azure Pipelines + ref = get(ENV, "SYSTEM_PULLREQUEST_TARGETBRANCH", ENV["BUILD_SOURCEBRANCHNAME"]) + branch = startswith(ref, "refs/heads/") ? ref[12:end] : ref + kwargs = set_defaults(kwargs, + service = "azure_pipelines", + branch = branch, + commit = ENV["BUILD_SOURCEVERSION"], + pull_request = get(ENV, "SYSTEM_PULLREQUEST_PULLREQUESTNUMBER", ""), + job = ENV["BUILD_DEFINITIONNAME"], + slug = ENV["BUILD_REPOSITORY_NAME"], + build = ENV["BUILD_BUILDID"], + ) + elseif haskey(ENV, "GITHUB_ACTION") # GitHub Actions + event_path = open(JSON.Parser.parse, ENV["GITHUB_EVENT_PATH"]) + ref = ENV["GITHUB_REF"] + if startswith(ref, "refs/heads/") + branch = ref[12:end] + ga_pr = "false" + elseif startswith(ref, "refs/tags/") + branch = ref[11:end] + ga_pr = "false" + elseif startswith(ref, "refs/pull/") + branch = ENV["GITHUB_HEAD_REF"] + ga_pr_info = get(event_path, "pull_request", Dict()) + ga_pr = get(ga_pr_info, "number", "false") end - - return kwargs + ga_build_url = "https://github.com/$(ENV["GITHUB_REPOSITORY"])/actions/runs/$(ENV["GITHUB_RUN_ID"])" + kwargs = set_defaults(kwargs, + service = "github-actions", + branch = branch, + commit = ENV["GITHUB_SHA"], + pull_request = ga_pr, + slug = ENV["GITHUB_REPOSITORY"], + build = ENV["GITHUB_RUN_ID"], + build_url = ga_build_url, + ) + else + error("No compatible CI platform detected") end - """ - submit_local(fcs::Vector{FileCoverage}, dir::AbstractString=pwd()) + return kwargs +end - Take a `Vector` of file coverage results (produced by `process_folder`), - and submit them to Codecov.io. Assumes the submission is being made from - a local git installation, rooted at `dir`. A repository token should be specified by a - `token` keyword argument or the `CODECOV_TOKEN` environment variable. - """ - function submit_local(fcs::Vector{FileCoverage}, dir::AbstractString=pwd(); kwargs...) - submit_generic(fcs, add_local_to_kwargs(dir; kwargs...)) - end +""" + submit_local(fcs::Vector{FileCoverage}, dir::AbstractString=pwd()) - add_local_to_kwargs(dir; kwargs...) = add_local_to_kwargs(dir, Dict{Symbol,Any}(kwargs)) - function add_local_to_kwargs(dir, kwargs::Dict) - LibGit2.with(LibGit2.GitRepoExt(dir)) do repo - LibGit2.with(LibGit2.head(repo)) do headref - branch_name = LibGit2.shortname(headref) # this function returns a String - commit_oid = LibGit2.GitHash(LibGit2.peel(headref)) - kwargs = set_defaults(kwargs, - commit = string(commit_oid), - branch = branch_name - ) - end +Take a `Vector` of file coverage results (produced by `process_folder`), +and submit them to Codecov.io. Assumes the submission is being made from +a local git installation, rooted at `dir`. A repository token should be specified by a +`token` keyword argument or the `CODECOV_TOKEN` environment variable. +""" +function submit_local(fcs::Vector{FileCoverage}, dir::AbstractString=pwd(); kwargs...) + submit_generic(fcs, add_local_to_kwargs(dir; kwargs...)) +end + +add_local_to_kwargs(dir; kwargs...) = add_local_to_kwargs(dir, Dict{Symbol,Any}(kwargs)) +function add_local_to_kwargs(dir, kwargs::Dict) + LibGit2.with(LibGit2.GitRepoExt(dir)) do repo + LibGit2.with(LibGit2.head(repo)) do headref + branch_name = LibGit2.shortname(headref) # this function returns a String + commit_oid = LibGit2.GitHash(LibGit2.peel(headref)) + kwargs = set_defaults(kwargs, + commit = string(commit_oid), + branch = branch_name + ) end - - return kwargs end + return kwargs +end - """ - submit_generic(fcs::Vector{FileCoverage}) - - Takes a vector of file coverage results (produced by `process_folder`), - and submits them to a Codecov.io instance. Keyword arguments are converted - into a generic Codecov.io API uri. It is essential that the keywords and - values match the Codecov upload/v2 API specification. - The `codecov_url` keyword argument or the CODECOV_URL environment variable - can be used to specify the base path of the uri. - The `codecov_url_path` keyword argument or the CODECOV_URL_PATH environment variable - can be used to specify the final path of the uri. - The `dry_run` keyword can be used to prevent the http request from - being generated. - """ - submit_generic(fcs::Vector{FileCoverage}; kwargs...) = - submit_generic(fcs, Dict{Symbol,Any}(kwargs)) - function submit_generic(fcs::Vector{FileCoverage}, kwargs::Dict) - @assert length(kwargs) > 0 - dry_run = get(kwargs, :dry_run, false) - - uri_str = construct_uri_string(kwargs) - - @info "Submitting data to Codecov..." - @debug "Codecov.io API URL:\n" * mask_token(uri_str) - - is_black_hole_server = parse(Bool, strip(get(ENV, "JULIA_COVERAGE_IS_BLACK_HOLE_SERVER", "false")))::Bool - if !dry_run - # Tell Codecov we have an upload for them - response = HTTP.post(uri_str; headers=Dict("Accept" => "text/plain")) - # Get the temporary URL to use for uploading to S3 - repr = String(response) - s3url = get(split(String(response.body), '\n'), 2, "") - repr = chomp(replace(repr, s3url => "")) - @debug "Result of submission:" * repr - !is_black_hole_server && upload_to_s3(; s3url=s3url, fcs=fcs) - end - end - function upload_to_s3(; s3url, fcs) - startswith(s3url, "https://") || error("Invalid codecov response: $s3url") - # Upload to S3 - request = HTTP.put(s3url; body=json(to_json(fcs)), - header=Dict("Content-Type" => "application/json", - "x-amz-storage-class" => "REDUCED_REDUNDANCY")) - @debug "Result of submission:" * mask_token(String(request)) +""" + submit_generic(fcs::Vector{FileCoverage}) + +Takes a vector of file coverage results (produced by `process_folder`), +and submits them to a Codecov.io instance. Keyword arguments are converted +into a generic Codecov.io API uri. It is essential that the keywords and +values match the Codecov upload/v2 API specification. +The `codecov_url` keyword argument or the CODECOV_URL environment variable +can be used to specify the base path of the uri. +The `codecov_url_path` keyword argument or the CODECOV_URL_PATH environment variable +can be used to specify the final path of the uri. +The `dry_run` keyword can be used to prevent the http request from +being generated. +""" +submit_generic(fcs::Vector{FileCoverage}; kwargs...) = + submit_generic(fcs, Dict{Symbol,Any}(kwargs)) +function submit_generic(fcs::Vector{FileCoverage}, kwargs::Dict) + @assert length(kwargs) > 0 + dry_run = get(kwargs, :dry_run, false) + + uri_str = construct_uri_string(kwargs) + + @info "Submitting data to Codecov..." + @debug "Codecov.io API URL:\n" * mask_token(uri_str) + + is_black_hole_server = parse(Bool, strip(get(ENV, "JULIA_COVERAGE_IS_BLACK_HOLE_SERVER", "false")))::Bool + if !dry_run + # Tell Codecov we have an upload for them + response = HTTP.post(uri_str; headers=Dict("Accept" => "text/plain")) + # Get the temporary URL to use for uploading to S3 + repr = String(response) + s3url = get(split(String(response.body), '\n'), 2, "") + repr = chomp(replace(repr, s3url => "")) + @debug "Result of submission:" * repr + !is_black_hole_server && upload_to_s3(; s3url=s3url, fcs=fcs) end +end - function construct_uri_string(kwargs::Dict) - url = get(ENV, "CODECOV_URL", "") - isempty(url) || (kwargs = set_defaults(kwargs, codecov_url = url)) +function upload_to_s3(; s3url, fcs) + startswith(s3url, "https://") || error("Invalid codecov response: $s3url") + # Upload to S3 + request = HTTP.put(s3url; body=json(to_json(fcs)), + header=Dict("Content-Type" => "application/json", + "x-amz-storage-class" => "REDUCED_REDUNDANCY")) + @debug "Result of submission:" * mask_token(String(request)) +end - path = get(ENV, "CODECOV_URL_PATH", "") - isempty(path) || (kwargs = set_defaults(kwargs, codecov_url_path = path)) +function construct_uri_string(kwargs::Dict) + url = get(ENV, "CODECOV_URL", "") + isempty(url) || (kwargs = set_defaults(kwargs, codecov_url = url)) - token = get(ENV, "CODECOV_TOKEN", "") - isempty(token) || (kwargs = set_defaults(kwargs, token = token)) + path = get(ENV, "CODECOV_URL_PATH", "") + isempty(path) || (kwargs = set_defaults(kwargs, codecov_url_path = path)) - flags = get(ENV, "CODECOV_FLAGS", "") - isempty(flags) || (kwargs = set_defaults(kwargs; flags = flags)) + token = get(ENV, "CODECOV_TOKEN", "") + isempty(token) || (kwargs = set_defaults(kwargs, token = token)) - name = get(ENV, "CODECOV_NAME", "") - isempty(name) || (kwargs = set_defaults(kwargs; name = name)) + flags = get(ENV, "CODECOV_FLAGS", "") + isempty(flags) || (kwargs = set_defaults(kwargs; flags = flags)) - codecov_url = get(kwargs, :codecov_url, "https://codecov.io") - if isempty(codecov_url) || codecov_url[end] == '/' - error("the codecov_url should not end with a /, given url $(repr(codecov_url))") - end - - codecov_url_path = get(kwargs, :codecov_url_path, "/upload/v4") - if isempty(codecov_url_path) || codecov_url_path[1] != '/' || codecov_url_path[end] == '/' - error("the codecov_url_path should begin with, but not end with, a /, given url $(repr(codecov_url_path))") - end + name = get(ENV, "CODECOV_NAME", "") + isempty(name) || (kwargs = set_defaults(kwargs; name = name)) - uri_str = "$(codecov_url)$(codecov_url_path)?" - for (k, v) in kwargs - # add all except a few special key/value pairs to the URL - # (:verbose is there for backwards compatibility with versions - # of this code that treated it in a special way) - if k != :codecov_url && k != :dry_run && k != :verbose - uri_str = "$(uri_str)$(k)=$(v)&" - end - end + codecov_url = get(kwargs, :codecov_url, "https://codecov.io") + if isempty(codecov_url) || codecov_url[end] == '/' + error("the codecov_url should not end with a /, given url $(repr(codecov_url))") + end - return uri_str + codecov_url_path = get(kwargs, :codecov_url_path, "/upload/v4") + if isempty(codecov_url_path) || codecov_url_path[1] != '/' || codecov_url_path[end] == '/' + error("the codecov_url_path should begin with, but not end with, a /, given url $(repr(codecov_url_path))") end - function mask_token(uri_string) - return replace(uri_string, r"token=[^&]*" => "token=") + uri_str = "$(codecov_url)$(codecov_url_path)?" + for (k, v) in kwargs + # add all except a few special key/value pairs to the URL + # (:verbose is there for backwards compatibility with versions + # of this code that treated it in a special way) + if k != :codecov_url && k != :dry_run && k != :verbose + uri_str = "$(uri_str)$(k)=$(v)&" + end end -end # module Codecov + return uri_str +end + +function mask_token(uri_string) + return replace(uri_string, r"token=[^&]*" => "token=") +end + +end # module diff --git a/src/coveralls.jl b/src/coveralls.jl index b00805a..905bbe0 100644 --- a/src/coveralls.jl +++ b/src/coveralls.jl @@ -7,231 +7,232 @@ This module provides functionality to push coverage information to the Coveralls web service. It exports the `submit` and `submit_local` methods. """ module Coveralls - using Coverage - using CoverageTools - using HTTP - using JSON - using LibGit2 - using MbedTLS - - export submit, submit_local - - #= - JSON structure for Coveralls - Accessed 2015/07/24: - https://coveralls.zendesk.com/hc/en-us/articles/201774865-API-Introduction + +using Coverage +using CoverageTools +using HTTP +using JSON +using LibGit2 +using MbedTLS + +export submit, submit_local + +#= +JSON structure for Coveralls +Accessed 2015/07/24: +https://coveralls.zendesk.com/hc/en-us/articles/201774865-API-Introduction +{ + "service_job_id": "1234567890", + "service_name": "travis-ci", + "source_files": [ + { + "name": "example.rb", + "source": "def four\n 4\nend", + "coverage": [null, 1, null] + }, { - "service_job_id": "1234567890", - "service_name": "travis-ci", - "source_files": [ - { - "name": "example.rb", - "source": "def four\n 4\nend", - "coverage": [null, 1, null] - }, - { - "name": "lib/two.rb", - "source": "def seven\n eight\n nine\nend", - "coverage": [null, 1, 0, null] - } - ] + "name": "lib/two.rb", + "source": "def seven\n eight\n nine\nend", + "coverage": [null, 1, 0, null] } - =# - - # to_json - # Convert a FileCoverage instance to its Coveralls JSON representation - function to_json(fc::FileCoverage) - name = Sys.iswindows() ? replace(fc.filename, '\\' => '/') : fc.filename - return Dict("name" => name, - "source_digest" => bytes2hex(digest(MD_MD5, fc.source, "secret")), - "coverage" => fc.coverage) - end + ] +} +=# + +# to_json +# Convert a FileCoverage instance to its Coveralls JSON representation +function to_json(fc::FileCoverage) + name = Sys.iswindows() ? replace(fc.filename, '\\' => '/') : fc.filename + return Dict("name" => name, + "source_digest" => bytes2hex(digest(MD_MD5, fc.source, "secret")), + "coverage" => fc.coverage) +end + +# Format the body argument to HTTP.post +function makebody(data::Dict) + iodata = IOBuffer(JSON.json(data)) + json_file = HTTP.Multipart("json_file", iodata, "application/json") + return Dict("json_file" => json_file) +end - # Format the body argument to HTTP.post - function makebody(data::Dict) - iodata = IOBuffer(JSON.json(data)) - json_file = HTTP.Multipart("json_file", iodata, "application/json") - return Dict("json_file" => json_file) - end - - """ - submit(fcs::Vector{FileCoverage}; kwargs...) - - Take a vector of file coverage results (produced by `process_folder`), - and submits them to Coveralls. Assumes that this code is being run - on TravisCI, AppVeyor or Jenkins. If running locally, use `submit_local`. - """ - function submit(fcs::Vector{FileCoverage}; kwargs...) - data = prepare_request(fcs, false) - post_request(data) - end +""" + submit(fcs::Vector{FileCoverage}; kwargs...) - function prepare_request(fcs::Vector{FileCoverage}, local_env::Bool, git_info=query_git_info) - data = Dict{String,Any}("source_files" => map(to_json, fcs)) - - if local_env - # Attempt to parse git info via git_info, unless the user explicitly disables it by setting git_info to nothing - data["service_name"] = "local" - data["git"] = parse_git_info(git_info) - elseif lowercase(get(ENV, "APPVEYOR", "false")) == "true" - data["service_job_number"] = ENV["APPVEYOR_BUILD_NUMBER"] - data["service_job_id"] = ENV["APPVEYOR_BUILD_ID"] - data["service_name"] = "appveyor" - appveyor_pr = get(ENV, "APPVEYOR_PULL_REQUEST_NUMBER", "") - isempty(appveyor_pr) || (data["service_pull_request"] = appveyor_pr) - elseif lowercase(get(ENV, "TRAVIS", "false")) == "true" - data["service_number"] = ENV["TRAVIS_BUILD_NUMBER"] - data["service_job_id"] = ENV["TRAVIS_JOB_ID"] - data["service_name"] = "travis-ci" - travis_pr = get(ENV, "TRAVIS_PULL_REQUEST", "") - isempty(travis_pr) || (data["service_pull_request"] = travis_pr) - elseif lowercase(get(ENV, "JENKINS", "false")) == "true" - data["service_job_id"] = ENV["BUILD_ID"] - data["service_name"] = "jenkins-ci" - data["git"] = parse_git_info(git_info) - - # get the name of the branch if not a pull request - if get(ENV, "CI_PULL_REQUEST", "false") == "false" - data["git"]["branch"] = split(ENV["GIT_BRANCH"], "/")[2] - end - elseif haskey(ENV, "GITHUB_ACTION") - data["service_job_id"] = ENV["GITHUB_RUN_ID"] - data["service_name"] = "github" - data["git"] = parse_git_info(git_info) - - event_path = open(JSON.Parser.parse, ENV["GITHUB_EVENT_PATH"]) - github_pr_info = get(event_path, "pull_request", Dict()) - github_pr = get(github_pr_info, "number", "") - github_pr::Union{AbstractString, Integer} - ((github_pr isa Integer) || (!isempty(github_pr))) && (data["service_pull_request"] = strip(string(github_pr))) - else - data["git"] = parse_git_info(git_info) +Take a vector of file coverage results (produced by `process_folder`), +and submits them to Coveralls. Assumes that this code is being run +on TravisCI, AppVeyor or Jenkins. If running locally, use `submit_local`. +""" +function submit(fcs::Vector{FileCoverage}; kwargs...) + data = prepare_request(fcs, false) + post_request(data) +end + +function prepare_request(fcs::Vector{FileCoverage}, local_env::Bool, git_info=query_git_info) + data = Dict{String,Any}("source_files" => map(to_json, fcs)) + + if local_env + # Attempt to parse git info via git_info, unless the user explicitly disables it by setting git_info to nothing + data["service_name"] = "local" + data["git"] = parse_git_info(git_info) + elseif lowercase(get(ENV, "APPVEYOR", "false")) == "true" + data["service_job_number"] = ENV["APPVEYOR_BUILD_NUMBER"] + data["service_job_id"] = ENV["APPVEYOR_BUILD_ID"] + data["service_name"] = "appveyor" + appveyor_pr = get(ENV, "APPVEYOR_PULL_REQUEST_NUMBER", "") + isempty(appveyor_pr) || (data["service_pull_request"] = appveyor_pr) + elseif lowercase(get(ENV, "TRAVIS", "false")) == "true" + data["service_number"] = ENV["TRAVIS_BUILD_NUMBER"] + data["service_job_id"] = ENV["TRAVIS_JOB_ID"] + data["service_name"] = "travis-ci" + travis_pr = get(ENV, "TRAVIS_PULL_REQUEST", "") + isempty(travis_pr) || (data["service_pull_request"] = travis_pr) + elseif lowercase(get(ENV, "JENKINS", "false")) == "true" + data["service_job_id"] = ENV["BUILD_ID"] + data["service_name"] = "jenkins-ci" + data["git"] = parse_git_info(git_info) + + # get the name of the branch if not a pull request + if get(ENV, "CI_PULL_REQUEST", "false") == "false" + data["git"]["branch"] = split(ENV["GIT_BRANCH"], "/")[2] end + elseif haskey(ENV, "GITHUB_ACTION") + data["service_job_id"] = ENV["GITHUB_RUN_ID"] + data["service_name"] = "github" + data["git"] = parse_git_info(git_info) + + event_path = open(JSON.Parser.parse, ENV["GITHUB_EVENT_PATH"]) + github_pr_info = get(event_path, "pull_request", Dict()) + github_pr = get(github_pr_info, "number", "") + github_pr::Union{AbstractString, Integer} + ((github_pr isa Integer) || (!isempty(github_pr))) && (data["service_pull_request"] = strip(string(github_pr))) + else + data["git"] = parse_git_info(git_info) + end - service_name = get(ENV, "COVERALLS_SERVICE_NAME", "") - isempty(service_name) || (data["service_name"] = service_name) - - service_number = get(ENV, "COVERALLS_SERVICE_NUMBER", "") - isempty(service_number) || (data["service_number"] = service_number) + service_name = get(ENV, "COVERALLS_SERVICE_NAME", "") + isempty(service_name) || (data["service_name"] = service_name) - service_job_number = get(ENV, "COVERALLS_SERVICE_JOB_NUMBER", "") - isempty(service_job_number) || (data["service_job_number"] = service_job_number) + service_number = get(ENV, "COVERALLS_SERVICE_NUMBER", "") + isempty(service_number) || (data["service_number"] = service_number) - jobid = get(ENV, "COVERALLS_SERVICE_JOB_ID", "") - isempty(jobid) || (data["service_job_id"] = jobid) + service_job_number = get(ENV, "COVERALLS_SERVICE_JOB_NUMBER", "") + isempty(service_job_number) || (data["service_job_number"] = service_job_number) - flag_name = get(ENV, "COVERALLS_FLAG_NAME", "") - isempty(flag_name) || (data["flag_name"] = flag_name) + jobid = get(ENV, "COVERALLS_SERVICE_JOB_ID", "") + isempty(jobid) || (data["service_job_id"] = jobid) - ci_pr = get(ENV, "COVERALLS_PULL_REQUEST", "") - isempty(ci_pr) || (data["service_pull_request"] = ci_pr) + flag_name = get(ENV, "COVERALLS_FLAG_NAME", "") + isempty(flag_name) || (data["flag_name"] = flag_name) - if !haskey(data, "service_name") - error("No compatible CI platform detected") - end + ci_pr = get(ENV, "COVERALLS_PULL_REQUEST", "") + isempty(ci_pr) || (data["service_pull_request"] = ci_pr) - data = add_repo_token(data, local_env) - if get(ENV, "COVERALLS_PARALLEL", "false") == "true" - data["parallel"] = "true" - end - return data + if !haskey(data, "service_name") + error("No compatible CI platform detected") end - function parse_git_info(git_info::Function) - result = nothing - try - result = git_info() - catch ex - @warn "Parse of git information failed" exception=e, catch_backtrace() - end - return result + data = add_repo_token(data, local_env) + if get(ENV, "COVERALLS_PARALLEL", "false") == "true" + data["parallel"] = "true" end - - parse_git_info(git_info::Dict) = git_info - - - # query_git_info - # Pulls information about the repository that isn't available if we - # are running somewhere other than TravisCI - function query_git_info(dir=pwd()) - repo = LibGit2.GitRepoExt(dir) - head = LibGit2.head(repo) - head_cmt = LibGit2.peel(head) - head_oid = LibGit2.GitHash(head_cmt) - commit_sha = string(head_oid) - author_name = string(LibGit2.author(head_cmt).name) - author_email = string(LibGit2.author(head_cmt).email) - committer_name = string(LibGit2.committer(head_cmt).name) - committer_email = string(LibGit2.committer(head_cmt).email) - message = LibGit2.message(head_cmt) - remote_name = "origin" - branch = LibGit2.shortname(head) - - # determine remote url, but only if repo is not in detached state - remote = "" - if branch != "HEAD" - LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, remote_name)) do rmt - remote = LibGit2.url(rmt) - end + return data +end + +function parse_git_info(git_info::Function) + result = nothing + try + result = git_info() + catch ex + @warn "Parse of git information failed" exception=e, catch_backtrace() + end + return result +end + +parse_git_info(git_info::Dict) = git_info + + +# query_git_info +# Pulls information about the repository that isn't available if we +# are running somewhere other than TravisCI +function query_git_info(dir=pwd()) + repo = LibGit2.GitRepoExt(dir) + head = LibGit2.head(repo) + head_cmt = LibGit2.peel(head) + head_oid = LibGit2.GitHash(head_cmt) + commit_sha = string(head_oid) + author_name = string(LibGit2.author(head_cmt).name) + author_email = string(LibGit2.author(head_cmt).email) + committer_name = string(LibGit2.committer(head_cmt).name) + committer_email = string(LibGit2.committer(head_cmt).email) + message = LibGit2.message(head_cmt) + remote_name = "origin" + branch = LibGit2.shortname(head) + + # determine remote url, but only if repo is not in detached state + remote = "" + if branch != "HEAD" + LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, remote_name)) do rmt + remote = LibGit2.url(rmt) end - LibGit2.close(repo) - - return Dict( - "branch" => branch, - "remotes" => [ - Dict( - "name" => remote_name, - "url" => remote - ) - ], - "head" => Dict( - "id" => commit_sha, - "author_name" => author_name, - "author_email" => author_email, - "committer_name" => committer_name, - "committer_email" => committer_email, - "message" => message + end + LibGit2.close(repo) + + return Dict( + "branch" => branch, + "remotes" => [ + Dict( + "name" => remote_name, + "url" => remote ) + ], + "head" => Dict( + "id" => commit_sha, + "author_name" => author_name, + "author_email" => author_email, + "committer_name" => committer_name, + "committer_email" => committer_email, + "message" => message ) - end - - """ - submit_local(fcs::Vector{FileCoverage}, git_info=query_git_info; kwargs...) + ) +end - Take a `Vector` of file coverage results (produced by `process_folder`), - and submits them to Coveralls. For submissions not from CI. - - git_info can be either a `Dict` or a function that returns a `Dict`. - """ - function submit_local(fcs::Vector{FileCoverage}, git_info=query_git_info; kwargs...) - data = prepare_request(fcs, true, git_info) - post_request(data) - end +""" + submit_local(fcs::Vector{FileCoverage}, git_info=query_git_info; kwargs...) - # posts the actual request given the data - function post_request(data) - @info "Submitting data to Coveralls..." - coveralls_url = get(ENV, "COVERALLS_URL", "https://coveralls.io/api/v1/jobs") - req = HTTP.post(coveralls_url, HTTP.Form(makebody(data))) - @debug "Result of submission:\n" * String(req) - nothing - end +Take a `Vector` of file coverage results (produced by `process_folder`), +and submits them to Coveralls. For submissions not from CI. - # adds the repo token to the data - function add_repo_token(data, local_submission) - repo_token = - get(ENV, "COVERALLS_TOKEN") do - get(ENV, "REPO_TOKEN") do #backward compatibility - # error unless we are on Travis - if local_submission || (data["service_name"] != "travis-ci") - error("Coveralls submission requires a COVERALLS_TOKEN environment variable") - end +git_info can be either a `Dict` or a function that returns a `Dict`. +""" +function submit_local(fcs::Vector{FileCoverage}, git_info=query_git_info; kwargs...) + data = prepare_request(fcs, true, git_info) + post_request(data) +end + +# posts the actual request given the data +function post_request(data) + @info "Submitting data to Coveralls..." + coveralls_url = get(ENV, "COVERALLS_URL", "https://coveralls.io/api/v1/jobs") + req = HTTP.post(coveralls_url, HTTP.Form(makebody(data))) + @debug "Result of submission:\n" * String(req) + nothing +end + +# adds the repo token to the data +function add_repo_token(data, local_submission) + repo_token = + get(ENV, "COVERALLS_TOKEN") do + get(ENV, "REPO_TOKEN") do #backward compatibility + # error unless we are on Travis + if local_submission || (data["service_name"] != "travis-ci") + error("Coveralls submission requires a COVERALLS_TOKEN environment variable") end end - if repo_token !== nothing - data["repo_token"] = repo_token - end - return data + end + if repo_token !== nothing + data["repo_token"] = repo_token end + return data +end -end # module Coveralls +end # module