Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GAP distro tests #1067

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
75 changes: 75 additions & 0 deletions .github/workflows/CI-distro.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Test GAP package distro

on:
pull_request:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pull_request:

IMO this shouldn't run on every commit in every PR, as it produces >150 jobs. But I have kept it here for now such that in this PR, we can already see it running.

schedule:
# Every Monday at 3:08 AM UTC
- cron: '8 3 * * 1'
workflow_dispatch:

concurrency:
# group by workflow and ref; the last slightly strange component ensures that for pull
# requests, we limit to 1 concurrent job, but for the default repository branch we don't
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_name != github.event.repository.default_branch || github.run_number }}
# Cancel intermediate builds, but only if it is a pull request build.
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}

jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
gap-packages: ${{ steps.set-matrix.outputs.gap-packages }}
steps:
- uses: actions/checkout@v4
- name: "Set up Julia"
uses: julia-actions/setup-julia@v2
with:
version: '1'
- name: "Cache artifacts"
uses: julia-actions/cache@v2
- name: "Build package"
uses: julia-actions/julia-buildpkg@v1
- name: Get list of GAP packages
id: set-matrix
run: julia --project=. -e '
using Artifacts;
output = sprint(print, "gap-packages=", filter(startswith(r"d|f"), readdir(artifact"gap_packages")));
println(output);
open(ENV["GITHUB_OUTPUT"], "a") do io;
println(io, output);
end;'

test:
name: ${{ matrix.gap-package }}
needs: generate-matrix
runs-on: ${{ matrix.os }}
timeout-minutes: 20
continue-on-error: ${{ matrix.julia-version == 'nightly' }}
strategy:
fail-fast: false
matrix:
julia-version:
- '1'
julia-arch:
- x64
os:
- ubuntu-latest
gap-package: ${{fromJSON(needs.generate-matrix.outputs.gap-packages)}}

steps:
- uses: actions/checkout@v4
- name: "Set up Julia"
uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.julia-version }}
arch: ${{ matrix.julia-arch }}
- name: "Cache artifacts"
uses: julia-actions/cache@v2
with:
cache-scratchspaces: false
- name: "Build package"
uses: julia-actions/julia-buildpkg@v1
- name: "Build GAP package"
run: julia --color=yes --project=. -e 'using GAP; GAP.Packages.build_recursive("${{ matrix.gap-package }}") || exit(1)'
- name: "Run GAP package tests"
run: julia --color=yes --project=. -e 'using GAP, Test; GAP.Packages.test("${{ matrix.gap-package }}")'
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Scratch = "6c6a2e73-6563-6170-7368-637461726353"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
AbstractAlgebra = "0.41.11, 0.42.1, 0.43"
Expand All @@ -40,4 +41,5 @@ Pkg = "1.6"
REPL = "1.6"
Random = "1.6"
Scratch = "1.1"
Test = "1.6"
julia = "1.6"
3 changes: 3 additions & 0 deletions docs/src/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ GAP.Packages.load
GAP.Packages.install
GAP.Packages.update
GAP.Packages.remove
GAP.Packages.build
GAP.Packages.build_recursive
GAP.Packages.test
GAP.Packages.locate_package
```

152 changes: 151 additions & 1 deletion src/packages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module Packages

import Downloads
import Pidfile
import ...GAP: Globals, GapObj, replace_global!, RNamObj, sysinfo, Wrappers
import Test: @test
import ...GAP: disable_error_handler, Globals, GapObj, replace_global!, RNamObj, sysinfo, Wrappers

const DEFAULT_PKGDIR = Ref{String}()
const DOWNLOAD_HELPER = Ref{Downloads.Downloader}()
Expand Down Expand Up @@ -359,6 +360,155 @@ function remove(spec::String; interactive::Bool = true, quiet::Bool = false,
end
end

"""
build(name::String; quiet::Bool = false,
debug::Bool = false,
pkgdir::AbstractString = GAP.Packages.DEFAULT_PKGDIR[])

Build the GAP package with name `name` that is installed in the
`pkgdir` directory.

If no package with name `name` is installed in `pkgdir` but there is a version of
`name` bundled with the GAP package distro, this version is copied to `pkgdir` and built.

Return `true` if the build was successful, and `false` otherwise.

The function uses [the function `CompilePackage` from GAP's package
`PackageManager`](GAP_ref(PackageManager:CompilePackage)).
The info messages shown by this function can be suppressed by passing
`true` as the value of `quiet`.
"""
function build(name::String; quiet::Bool = false,
debug::Bool = false,
pkgdir::AbstractString = DEFAULT_PKGDIR[])
# point PackageManager to the given pkg dir
Globals.PKGMAN_CustomPackageDir = GapObj(pkgdir)
mkpath(pkgdir)

gname = GapObj(name)
allinfo = collect(Globals.PackageInfo(gname))
userinfo = filter(info -> startswith(String(info.InstallationPath), pkgdir), allinfo)
isempty(allinfo) && error("package not found")
length(userinfo) > 1 && error("multiple installations of package found in pkgdir")
if isempty(userinfo)
length(allinfo) > 1 && error("multiple installations of package found")
info = only(allinfo)
oldpath = String(info.InstallationPath)
newpath = joinpath(pkgdir, name * "-" * String(info.Version))
cp(oldpath, newpath)
end
if quiet || debug
oldlevel = Wrappers.InfoLevel(Globals.InfoPackageManager)
Wrappers.SetInfoLevel(Globals.InfoPackageManager, quiet ? 0 : 3)
end
Globals.PKGMAN_RefreshPackageInfo()
res = Globals.CompilePackage(gname)
if quiet || debug
Wrappers.SetInfoLevel(Globals.InfoPackageManager, oldlevel)
end
return res
end

"""
build_recursive(name::String; quiet::Bool = false,
debug::Bool = false,
pkgdir::AbstractString = GAP.Packages.DEFAULT_PKGDIR[])

Build the GAP package with name `name` that is installed in the
`pkgdir` directory, as well as all of its (transitive) dependencies.

This is achieved by calling [`build`](@ref) for the package `name` and then
all of its `NeededOtherPackages`, recursively.
All keyword arguments are passed on to [`build`](@ref).
"""
function build_recursive(name::String; quiet::Bool = false,
debug::Bool = false,
pkgdir::AbstractString = DEFAULT_PKGDIR[])
# point PackageManager to the given pkg dir
Globals.PKGMAN_CustomPackageDir = GapObj(pkgdir)
mkpath(pkgdir)

todo = Set{String}((name,))
done = Set{String}()
while !isempty(todo)
dep = pop!(todo)
dep in done && continue

res = build(dep; quiet, debug, pkgdir)
res || return false
gdep = GapObj(dep)
allinfo = collect(Globals.PackageInfo(gdep))
userinfo = filter(info -> startswith(String(info.InstallationPath), pkgdir), allinfo)
info = only(userinfo)
push!(done, dep)
for (needed, _) in info.Dependencies.NeededOtherPackages
push!(todo, String(needed))
end
end
return true
end

"""
test(name::String)

Test the GAP package with name `name`.

The function uses [the function `TestPackage`](GAP_ref(ref:TestPackage)).

Use-sites of this function should verify that the `Test` package is loaded
before calling this function, as this function might be moved
to a package extension that depends on the `Test` package in the future.
"""
function test(name::String)
global disable_error_handler

function with_gap_var(f, name::String, val)
gname = GapObj(name)
old_value = Globals.ValueGlobal(gname)
Globals.MakeReadWriteGlobal(gname)
Globals.UnbindGlobal(gname)
Globals.BindGlobal(gname, val)
try
f()
finally
Globals.MakeReadWriteGlobal(gname);
Globals.UnbindGlobal(gname);
Globals.BindGlobal(gname, old_value);
end
end

error_occurred = false
ended_using_QuitGap = false
function fake_QuitGap(code)
if code != 0
error_occurred = true
end
ended_using_QuitGap = true
end

disable_error_handler[] = true
try
with_gap_var("ERROR_OUTPUT", Globals._JULIAINTERFACE_ORIGINAL_ERROR_OUTPUT) do
with_gap_var("QuitGap", fake_QuitGap) do
with_gap_var("QUIT_GAP", fake_QuitGap) do
with_gap_var("ForceQuitGap", identity) do
with_gap_var("FORCE_QUIT_GAP", identity) do
result = Globals.TestPackage(GapObj(name))
@test ended_using_QuitGap || result == true
# Due to the hack above, we run into an error in TestPackage that is usually unreachable.
# In the case of a `QuitGap` call, we thus don't check for `result == true`.
end
end
end
end
end
finally
disable_error_handler[] = false
end

@test !error_occurred
end

"""
locate_package(name::String)

Expand Down
Loading