diff --git a/.github/workflows/CI-distro.yml b/.github/workflows/CI-distro.yml new file mode 100644 index 000000000..309b4aede --- /dev/null +++ b/.github/workflows/CI-distro.yml @@ -0,0 +1,105 @@ +name: Test GAP package distro + +on: + push: + branches: + - 'master' + - 'release-*' + tags: '*' + pull_request: + 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, TOML; + output = sprint(print, "gap-packages=", map(name -> name[9:end], sort!(collect(filter(startswith("GAP_pkg_"), keys(TOML.parsefile(find_artifacts_toml(Base.active_project())))))))); + 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)}} + exclude: + - gap-package: 'alnuth' # `AL_EXECUTABLE, the executable for PARI/GP, has to be set` + - gap-package: 'atlasrep' # random segfaults during testing + - gap-package: 'autodoc' # tries and fails to build its own documentation, inside the (partially write protected) artifacts dir + - gap-package: 'example' # no jll + - gap-package: 'examplesforhomalg' # `Error, found no GAP executable in PATH` + - gap-package: 'guarana' # `AL_EXECUTABLE, the executable for PARI/GP, has to be set` + - gap-package: 'hap' # `polymake command not found. Please set POLYMAKE_COMMAND by hand` + - gap-package: 'hapcryst' # `polymake command not found. Please set POLYMAKE_COMMAND by hand` + - gap-package: 'help' # test failure in HeLP-4.0/tst/yes_4ti2.tst:39 + - gap-package: 'io' # segfaults, most likely due to child process handling + - gap-package: 'itc' # dependency `xgap` has no jll + - gap-package: 'localizeringforhomalg' # `Error, found no GAP executable in PATH` + - gap-package: 'normalizinterface' # Tests fail: NormalizInterface currently bundles Normaliz 3.9.3 and its test suite is tuned to that, but our JLL builds it against normaliz_jll which has 3.10.2 + - gap-package: 'packagemanager' # tests need curlInterface to passe, reactivate once we have a working JLL for that + - gap-package: 'polenta' # `AL_EXECUTABLE, the executable for PARI/GP, has to be set` + - gap-package: 'polycyclic' # test suite currently broken and also disabled by GAP package distribution + - gap-package: 'polymaking' # `polymake command not found. Please set POLYMAKE_COMMAND by hand` + - gap-package: 'profiling' # segfaults during testing + - gap-package: 'radiroot' # `AL_EXECUTABLE, the executable for PARI/GP, has to be set` + - gap-package: 'ringsforhomalg' # `Error, found no GAP executable in PATH` + - gap-package: 'semigroups' # no jll + - gap-package: 'xgap' # no jll + + steps: + - uses: actions/checkout@v4 + - name: "Install extra dependencies" + run: sudo apt-get install --no-install-recommends texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended dot2tex + - 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: + include-matrix: false # don't create a cache per GAP package + cache-scratchspaces: false + - name: "Build package" + uses: julia-actions/julia-buildpkg@v1 + - name: "Run GAP package tests" + run: | + julia --color=yes --project=. -e 'using GAP, Test; @test GAP.Packages.test("${{ matrix.gap-package }}")' diff --git a/CHANGES.md b/CHANGES.md index 1e004ba48..343854ad3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - Update to GAP 4.14.0 - Add `GAP.Packages.build_recursive(name)` +- Add `GAP.Packages.test(name)` - Change `GAP.Packages.build(name)` to no longer try to build the package if it is already installed - Instead of downloading a single huge "artifact" containing all deposited GAP diff --git a/docs/src/packages.md b/docs/src/packages.md index b62acf69b..a28dce618 100644 --- a/docs/src/packages.md +++ b/docs/src/packages.md @@ -15,6 +15,7 @@ GAP.Packages.update GAP.Packages.remove GAP.Packages.build GAP.Packages.build_recursive +GAP.Packages.test GAP.Packages.locate_package ``` diff --git a/src/packages.jl b/src/packages.jl index ab10a2c91..7b803ceb3 100644 --- a/src/packages.jl +++ b/src/packages.jl @@ -3,7 +3,7 @@ module Packages import Downloads import Pidfile -import ...GAP: Globals, GapObj, replace_global!, RNamObj, sysinfo, Wrappers +import ...GAP: disable_error_handler, Globals, GapObj, replace_global!, RNamObj, sysinfo, Wrappers const DEFAULT_PKGDIR = Ref{String}() const DOWNLOAD_HELPER = Ref{Downloads.Downloader}() @@ -453,6 +453,65 @@ function build_recursive(name::String; quiet::Bool = false, return true end +""" + test(name::String) + +Run the tests of GAP package `name` and return a boolean indicating whether +they succeeded (`true`) or not. + +It is intended to be used with the `@test` macro from the `Test` package. + +The function uses [the GAP function `TestPackage`](GAP_ref(ref:TestPackage)). +""" +function test(name::String) + global disable_error_handler + + function with_gap_var(f, name::Symbol, val) + gname = GapObj(name) + old_value = Globals.ValueGlobal(gname) + replace_global!(name, val) + try + f() + finally + replace_global!(name, old_value) + end + end + + error_occurred = false + called_QuitGap = false + function fake_QuitGap(code) + called_QuitGap && return # only do something on the first call + called_QuitGap = true + if code != 0 + error_occurred = true + end + return + end + + disable_error_handler[] = true + result = false + 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, fake_QuitGap) do + with_gap_var(:FORCE_QUIT_GAP, fake_QuitGap) do + result = Globals.TestPackage(GapObj(name)) + end + end + end + end + end + finally + disable_error_handler[] = false + end + + # 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`. + # Note: `result` may be `fail`, so it is not always booleany. + return !error_occurred && (called_QuitGap || result == true) +end + """ locate_package(name::String)