diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2cb0ebaa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + pull_request: + branches: [main] + schedule: + - cron: '0 0 * * 0' + +jobs: + test: + name: Test (Octave ${{ matrix.octave }}) + runs-on: ubuntu-latest + container: gnuoctave/octave:${{ matrix.octave }} + strategy: + matrix: + octave: ["8.4.0", "9.4.0", "10.3.0", "11.3.0"] + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # actions/checkout@v6.0.3 + with: + submodules: recursive + + - name: Build and test + run: make check-ci diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ec0698e..f6053249 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,6 +112,19 @@ Your contribution must be an independent work or derived from code that may be r [Fork](https://github.com/gnu-octave/pkg-control/fork) the pkg-control repository to your own account and clone the resulting repository. Refer to the [README](README.md) file for information about how to build the package archive which can be installed in GNU Octave. +### Running tests locally + +The easiest way to run the test suite locally is with Docker. Make sure the +submodules are initialized first, then run: + +```bash +git submodule update --init +make check-local +``` + +This pulls the latest `gnuoctave/octave` image and runs the full test suite +inside it, matching the CI environment. Docker must be installed and running. + ### Pull request When your changes are finished, commit and push the change to your forked repository on Github (make sure your fork is up to date) and create a pull request. diff --git a/Makefile b/Makefile index f2fb08ab..b82e8029 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ DOCS_PDF := $(DOCS_DIR)/$(PACKAGE).pdf DOCS_QCH := $(DOCS_DIR)/$(PACKAGE).qch DOCS_LOGO := $(DOCS_DIR)/$(PACKAGE).svg -.PHONY: help dist docs-html docs release install all check run clean +.PHONY: help dist docs-html docs release install all check check-ci check-local run clean help: @echo " " @@ -208,6 +208,15 @@ check: install $(OCTAVE) --path "inst/" --path "src/" \ --eval 'pkg test control' +check-ci: + test -f $(SRC)/Makefile.conf || (cd $(SRC) && ./bootstrap && ./configure) + $(MAKE) -C $(SRC) all + $(OCTAVE) --no-gui --version + $(OCTAVE) --no-gui --path "inst/" --path "src/" devel/run_tests.m + +check-local: + docker compose --file devel/compose.yaml --project-directory . run --rm octave + clean: $(RM) -r $(TARGET_DIR) $(MAKE) -C $(SRC) clean diff --git a/devel/compose.yaml b/devel/compose.yaml new file mode 100644 index 00000000..97e46ce0 --- /dev/null +++ b/devel/compose.yaml @@ -0,0 +1,7 @@ +services: + octave: + image: gnuoctave/octave:latest + working_dir: /workspace + volumes: + - .:/workspace + command: make check-ci diff --git a/devel/run_tests.m b/devel/run_tests.m new file mode 100644 index 00000000..79fae7c9 --- /dev/null +++ b/devel/run_tests.m @@ -0,0 +1,57 @@ +## Run embedded %!test blocks from source without installing the package. +## +## The canonical way (according to the docs I've found) to to actually test is +## to call 'pkg test control' but that is coupled to docs today because it +## requires you to install first, but to install you have to build a release +## tarball with dist which depends on docs. This is a "hack" to get around that +## and just run the tests directly. This should probably change in the future +## b/c searching and grepping through a bunch of src files to test is very +## awkward. +## +## Called by 'make check-ci'. Exit code is nonzero if any tests fail. + +## Register autoloads from PKG_ADD directives (normally done by pkg install) + +src_path = fullfile (fileparts (mfilename ("fullpath")), "..", "src"); + +## Find all .cc files in src/ b/c those are compiled to .oct files. We literally +## just want this out of the cpp files so that we know what .oct file we should +## target +## // PKG_ADD: autoload ("__sl_sb03md__", "__control_slicot_functions__.oct"); +cc_files = glob (fullfile (src_path, "*.cc")); + +## Read every file and grep of PKG_ADD, call autoload() on it with the absolute +## path to the .oct file. Autoload registers a deffered load which tells octave +## "when someone calls funcname, load filepath to find it, the .oct file isn't +## actually loaded until that first call" +for i = 1:numel (cc_files) + txt = fileread (cc_files{i}); + ## Use a regex to capture the 2 arguments (function name and .oct filename) + toks = regexp (txt, 'PKG_ADD: autoload \("(\w+)", "(\w+\.oct)"\)', "tokens"); + for j = 1:numel (toks) + autoload (toks{j}{1}, fullfile (src_path, toks{j}{2})); + endfor +endfor + +## Find all the class dirs to test ("inst", "inst/@tf", etc...) +dirs = [{"inst"}, cellstr(glob ("inst/@*"))']; + +n_pass = 0; +n_total = 0; + +## For every dir we collected, go through every mfile and call test +for i = 1:numel (dirs) + mfiles = dir (fullfile (dirs{i}, "*.m")); + for j = 1:numel (mfiles) + f = fullfile (mfiles(j).folder, mfiles(j).name); + ## collect output args from test call + ## https://docs.octave.org/v11.1.0/Test-Functions.html + [p, total, xfail, xbug, skip, rtskip] = test (f, "quiet", stdout); + n_pass += p; + n_total += total - xfail - xbug - skip - rtskip; + endfor +endfor + +n_fail = n_total - n_pass; +fprintf ("\nTotal: %d passed, %d failed\n", n_pass, n_fail); +exit (n_fail > 0);