name: test suite

on:
  pull_request:           # all pull requests
  push:
    branches: [ master ]  # all commits on master
  schedule:
    - cron: '0 2 * * 0'   # every Sunday at 2:00am UTC  (Saturday 7:00pm MST)

jobs:
  main:
    runs-on: ubuntu-22.04
    timeout-minutes: 90
    strategy:
      fail-fast: false  # Actions seems kind of flaky lately
      matrix:
        builder:
          - ch-image
        pack_fmt: [squash-mount, tar-unpack, squash-unpack]
        keep_sudo:   # if false, remove self from sudoers post-install/setup
          - false
        cache:
          - enabled
        include:
          - builder: none
            pack_fmt: squash-mount
            keep_sudo: false
            cache: disabled
          - builder: docker
            pack_fmt: tar-unpack
            keep_sudo: true
            cache: enabled
          - builder: ch-image
            pack_fmt: squash-mount
            keep_sudo: false
            cache: disabled

    env:
      CH_IMAGE_CACHE: ${{ matrix.cache }}
      CH_TEST_BUILDER: ${{ matrix.builder }}
      CH_TEST_TARDIR: /mnt/tarballs
      CH_TEST_IMGDIR: /mnt/images
      CH_TEST_PACK_FMT: ${{ matrix.pack_fmt }}
      CH_TEST_PERMDIRS: /mnt/perms_test /run/perms_test
      ch_prefix: /var/tmp

    steps:
      - uses: actions/checkout@v3

      # This allows SSH access to the GitHub Actions VM to debug things that
      # only happen on CI. Comment out unless needed. WARNING: tmate.io has
      # access to unencrypted SSH traffic.
      # See: https://github.com/marketplace/actions/debugging-with-tmate
      #- name: set up tmate session
      #  uses: mxschmitt/action-tmate@v3
      #  with:
      #    detached: true

      - name: early setup & validation
        run: |
          [[ -n $CH_TEST_BUILDER ]]
          sudo timedatectl set-timezone America/Denver
          sudo chmod 1777 /mnt /usr/local/src
          echo "ch_makej=-j$(getconf _NPROCESSORS_ONLN)" >> $GITHUB_ENV
          # Remove sbin directories from $PATH (see issue #43). Assume none of
          # these are the first entry in $PATH.
          echo "PATH=$PATH"
          path_new=$PATH
          for i in /sbin /usr/sbin /usr/local/sbin; do
              path_new=${path_new/:$i/}
          done
          echo "path_new=$path_new"
          echo "PATH=$path_new" >> $GITHUB_ENV
          # Set sudo umask to something quite restrictive. The default is
          # 0022, but we had a "make install" bug (issue #947) that was
          # tickled by 0027, which is a better setting. For reasons I don’t
          # understand, this only affects sudo, but since that’s what we want,
          # I left it. Note there are a few dependency installs below that
          # have similar permissions bugs; these relax the umask on a
          # case-by-case basis.
          sudo sed -i -E 's/^UMASK\s+022/UMASK 0077/' /etc/login.defs
          fgrep UMASK /etc/login.defs

      - name: print starting environment
        run: |
          echo builder: ${{ matrix.builder }}
          echo pack_fmt: ${{ matrix.pack_fmt }}
          echo keep_sudo: ${{ matrix.keep_sudo }}
          echo cache: ${{ matrix.cache }}
          uname -a
          lsb_release -a
          id
          pwd
          getconf _NPROCESSORS_ONLN
          free -m
          df -h
          locale -a
          timedatectl
          env | egrep '^(PATH|USER)='
          env | egrep '^(ch|CH)_'
          [[ $PATH != */usr/local/sbin* ]]  # verify sbin removal; see above
          printf 'umask for %s: ' $USER && umask
          printf 'umask under sudo: ' && sudo sh -c umask
          [[ $(umask) = 0022 ]]
          [[ $(sudo sh -c umask) = 0077 ]]

      - name: lines of code
        if: ${{ matrix.builder == 'none' }}
        run: |
          sudo apt-get install cloc
          misc/loc

      - name: install/configure dependencies, all
        run: |
          # configure doesn’t tell us about these.
          sudo apt-get install attr pigz pv
          # configure does tell us about these.
          sudo apt-get install bats squashfs-tools
          # Track newest Sphinx in case it breaks things.
          # FIXME: pip yells about docutils 0.19 so ask for latest 0.18
          sudo su -c 'umask 0022 && pip3 install docutils==0.18.1 sphinx sphinx-rtd-theme sphinx-reredirects'
          # Use newest shellcheck.
          cd /usr/local/src
          wget -nv https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz
          tar xf shellcheck-stable.linux.x86_64.tar.xz
          sudo mv shellcheck-stable/shellcheck /usr/local/bin
          which shellcheck
          shellcheck --version

      - name: install/configure dependencies, ch-image
        if: ${{ matrix.builder == 'ch-image' }}
        run: |
          # Use most current packages b/c new versions sometimes break things.
          sudo sh -c 'umask 0022 && pip3 install lark requests wheel'
          # Configure Git and friends for build cache.
          if [[ $CH_IMAGE_CACHE = enabled ]]; then
              # Git
              command -v git
              git --version
              git config --global user.name 'Judit Polgár'
              git config --global user.email judit@example.com
              git config --global core.excludesfile ~/.gitignore
              echo __ch-test_ignore__ >> ~/.gitignore
              # Graphviz
              sudo apt-get install -y graphviz
              dot -V
              # git2dot
              sudo sh -c 'umask 0022 && pip install python-dateutil'
              git clone https://github.com/jlinoff/git2dot.git ~/git2dot
              cd ~/git2dot
              git checkout $(git tag | sort -Vr | head -1)
              sudo cp -a git2dot.py /usr/local/bin
              command -v git2dot.py
              git2dot.py --version
          fi

      - name: install libsquashfuse
        if: ${{ matrix.pack_fmt == 'squash-mount' }}
        run: |
          set -x
          sudo apt-get install -y libfuse3-dev
          cd /usr/local/src
          git clone https://github.com/vasi/squashfuse.git
          cd squashfuse
          # Check out the latest release.
          git checkout $(git tag -l '[0-9].*' | sort -V | tail -1)
          ./autogen.sh
          ./configure --prefix=/usr/local
          make
          sudo sh -c 'umask 0022 && make install'
          sudo ldconfig
          # Unset setuid on all fusermount3(1) on the system.
          sudo chmod -v u-s /usr/bin/fusermount*
          ls -lh $(command -v fusermount3)
          [[ ! -u $(command -v fusermount3) ]]


      - name: install musl and friends
        if: ${{ matrix.pack_fmt == 'tar-unpack' }}
        run: |
          sudo apt-get install -y musl-tools
          # At present we use argp to parse command line arguments. This is
          # not portable; it’s a glibc extension (see #1260). Thus, install a
          # standalone argp.
          cd /usr/local/src
          git clone --depth=1 https://github.com/ericonr/argp-standalone.git
          cd argp-standalone
          autoreconf --force --install
          CC=musl-gcc ./configure
          make $ch_makej
          sudo install -D -m644 argp.h /usr/include/x86_64-linux-musl/argp.h
          sudo install -D -m755 libargp.a /usr/lib/x86_64-linux-musl/libargp.a
          # musl-gcc does not install Linux headers. This is a bad kludge.
          # See: https://www.openwall.com/lists/musl/2017/11/23/1
          sudo ln -s ../linux ../asm-generic ../x86_64-linux-gnu \
                     /usr/include/x86_64-linux-musl/
          sudo ln -s x86_64-linux-gnu/asm /usr/include/x86_64-linux-musl/

      - name: disable bundled lark, ch-image
        if: ${{    matrix.builder == 'ch-image'
                && matrix.pack_fmt == 'squash-mount'
                && matrix.cache == 'disabled' }}
        run: |
          set -x
          # no install, disable
          ./autogen.sh --no-lark
          ./configure --disable-bundled-lark --prefix=$ch_prefix/from-git
          fgrep '"lark" module ... external' config.log
          ! test -f ./lib/lark/lark.py
          make $ch_makej
          sudo make $ch_makej install
          [[ $(bin/ch-image -v --dependencies) = 'lark path: /usr/local/lib/python3.10/dist-packages/lark/__init__.py' ]]
          [[ $($ch_prefix/from-git/bin/ch-image -v --dependencies) = 'lark path: /usr/local/lib/python3.10/dist-packages/lark/__init__.py' ]]
          make clean
          # install, disable
          ./autogen.sh
          ./configure --disable-bundled-lark --prefix=$ch_prefix/from-git
          fgrep '"lark" module ... bundled' config.log
          test -f ./lib/lark/lark.py
          make $ch_makej
          sudo make $ch_makej install
          [[ $(bin/ch-image -v --dependencies) = 'lark path: /home/runner/work/charliecloud/charliecloud/lib/lark/__init__.py' ]]
          [[ $($ch_prefix/from-git/bin/ch-image -v --dependencies) = 'lark path: /usr/local/lib/python3.10/dist-packages/lark/__init__.py' ]]
          make clean
          # no install, enable: build fails, untested
          # install, enable: normal configuration, remainder of CI

      - name: build/install from Git
        run: |
          ./autogen.sh
          # Remove Autotools to make sure everything works without them.
          sudo apt-get remove autoconf automake
          # Configure and verify output.
          #
          # We want to support any standard libc, so build against musl for
          # some cases:
          #
          #   squash-mount: glibc; libsquashfuse and libfuse3 are built with it
          #   squash-unpack: glibc; to test ch-fromhost which assumes glibc
          #   tar-unpack: musl
          #
          # See PR #1258, which is a FTBFS for musl. Other build steps in this
          # workflow also use glibc.
          if [[ $CH_TEST_PACK_FMT == tar-unpack ]]; then
              export CC=musl-gcc
              # GNU ldd(1) doesn’t work on musl binaries.
              LDD='/lib/ld-musl-x86_64.so.1 --list'
          else
              LDD=ldd
          fi
          # Go wild with install directories to make sure it works (see #683).
          # We do have a normal install below.
          ./configure --prefix=$ch_prefix/from-git \
                      --datarootdir=$ch_prefix/from-git_dataroot \
                      --libexecdir=$ch_prefix/from-git_libexec \
                      --libdir=$ch_prefix/from-git_lib \
                      --mandir=$ch_prefix/from-git_man \
                      --docdir=$ch_prefix/from-git_doc \
                      --htmldir=$ch_prefix/from-git_html
          set -x
          fgrep 'documentation: yes' config.log
          [[ $CH_TEST_BUILDER = docker ]] && command -v docker
          if [[ $CH_TEST_BUILDER = ch-image ]]; then
              fgrep 'with ch-image(1): yes' config.log
              fgrep '"lark" module ... bundled' config.log
              test -f ./lib/lark/lark.py
          fi
          fgrep 'recommended tests, tar-unpack mode: yes' config.log
          fgrep 'recommended tests, squash-unpack mode: yes' config.log
          if [[ $CH_TEST_PACK_FMT = squash-mount ]]; then
              fgrep 'recommended tests, squash-mount mode: yes' config.log
              fgrep 'internal SquashFS mounting ... yes' config.log
          else
              fgrep 'recommended tests, squash-mount mode: no' config.log
              fgrep 'internal SquashFS mounting ... no' config.log
          fi
          set +x
          # Build and install.
          make $ch_makej
          sudo make $ch_makej install
          $LDD bin/ch-run
          bin/ch-run --version
          $LDD $ch_prefix/from-git/bin/ch-run
          $ch_prefix/from-git/bin/ch-run --version
          # Ensure bundled Lark in tarball.
          make $ch_makej dist
          tar tf charliecloud-*.tar.gz | fgrep /lib/lark/lark.py

      - name: late setup & validation, ch-image
        if: ${{ matrix.builder == 'ch-image' }}
        run: |
          set -x
          [[ $(bin/ch-image gestalt python-path) = /usr/bin/python3 ]]
          [[ $(bin/ch-image -v --dependencies) = "lark path: /home/runner/work/charliecloud/charliecloud/lib/lark/__init__.py" ]]
          [[ $($ch_prefix/from-git/bin/ch-image gestalt python-path) = /usr/bin/python3 ]]
          [[ $($ch_prefix/from-git/bin/ch-image -v --dependencies) = "lark path: /var/tmp/from-git_lib/charliecloud/lark/__init__.py" ]]

      - name: late setup & validation, all
        run: |
          bin/ch-test --is-pedantic all
          bin/ch-test --is-sudo all

      - name: make filesystem permissions fixtures
        run: |
          bin/ch-test mk-perm-dirs

      - name: start local registry, ch-image
        if: ${{ matrix.builder == 'ch-image' }}
        # See HOWTO in Google Docs for details.
        run: |
          set -x
          mkdir ~/registry-etc
          cp test/registry-config.yml ~/registry-etc/config.yml
          cd ~/registry-etc
          openssl req -batch -subj '/C=US/ST=NM/L=LA/O=LANL/CN=localhost' \
                      -newkey rsa:4096 -nodes -sha256 -x509 -days 36500 \
                      -keyout localhost.key -out localhost.crt
          #openssl x509 -text -noout -in localhost.crt
          sudo apt-get install apache2-utils
          htpasswd -Bbn charlie test > ./htpasswd
          diff -u <(docker run --rm registry:2 \
                           cat /etc/docker/registry/config.yml) \
                  config.yml || true
          docker run -d --rm -p 127.0.0.1:5000:5000 \
                  -v ~/registry-etc:/etc/docker/registry registry:2
          docker ps -a

      - name: build/install from tarball
        run: |
          # Create and unpack tarball. The wildcard saves us having to put the
          # version in a variable. This assumes there isn’t already a tarball
          # or unpacked directory in $ch_prefix, which is true on the clean
          # VMs GitHub gives us. Note that cd fails if it gets more than one
          # argument, which helps, but this is probably kind of brittle.
          make $ch_makej dist
          mv charliecloud-*.tar.gz $ch_prefix
          cd $ch_prefix
          tar xf charliecloud-*.tar.gz
          rm charliecloud-*.tar.gz  # else cd fails with “too many arguments”
          cd charliecloud-*
          pwd
          # Configure and verify output.
          ./configure --prefix=$ch_prefix/from-tarball
          set -x
          fgrep 'documentation: yes' config.log
          [[ $CH_TEST_BUILDER = buildah* ]] && command -v buildah
          [[ $CH_TEST_BUILDER = docker ]] && command -v docker
          [[ $CH_TEST_BUILDER = ch-image ]] && fgrep 'with ch-image(1): yes' config.log
          fgrep 'recommended tests, tar-unpack mode: yes' config.log
          fgrep 'recommended tests, squash-unpack mode: yes' config.log
          if [[ ${{ matrix.pack_fmt }} = squash-mount ]]; then
              fgrep 'recommended tests, squash-mount mode: yes' config.log
              fgrep 'internal SquashFS mounting ... yes' config.log
          else
              fgrep 'recommended tests, squash-mount mode: no' config.log
              fgrep 'internal SquashFS mounting ... no' config.log
          fi
          set +x
          # Build and install.
          make $ch_makej
          sudo make $ch_makej install
          bin/ch-run --version
          $ch_prefix/from-tarball/bin/ch-run --version

      - name: configure sudo to user root, group non-root
        if: ${{ matrix.keep_sudo }}
        run: |
          sudo sed -Ei 's/=\(ALL\)/=(ALL:ALL)/g' /etc/sudoers.d/runner
          sudo cat /etc/sudoers.d/runner

      - name: remove sudo
        if: ${{ ! matrix.keep_sudo }}
        run: |
          sudo rm /etc/sudoers.d/runner
          ! sudo echo hello

      - name: run test suite (Git WD, standard)
        run: |
          bin/ch-test all
          # Validate that “rootemu” test phase didn’t run (skipped by default
          # on standard scope).
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = no ]]

      - name: run root emulation phase
        if: ${{    matrix.builder == 'ch-image'
                && matrix.pack_fmt == 'squash-mount'
                && matrix.cache == 'enabled' }}
        run: |
          # We only really need to run the root emulation tests once in CI.
          # ch-image with an enabled cache is required for these unit tests.
          # Since all CI tests that use ch-image with an enabled cache take
          # roughly the same amount of time, we arbitrarily chose squash-mount
          # as the pack format.
          bin/ch-test rootemu
          # Validate that “rootemu” test phase ran.
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = yes ]]

      - name: run test suite (installed from Git WD, standard)
        if: ${{    matrix.builder == 'ch-image'
                && matrix.pack_fmt == 'squash-mount'
                && matrix.cache == 'enabled' }}
        run: |
          $ch_prefix/from-git/bin/ch-test all
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = no ]]

      - name: run test suite (installed from tarball, standard)
        if: ${{    matrix.builder == 'ch-image'
                && matrix.pack_fmt == 'squash-mount'
                && matrix.cache == 'enabled' }}
        run: |
          $ch_prefix/from-tarball/bin/ch-test all
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = no ]]

      - name: uninstall from tarball
        if: ${{ matrix.keep_sudo }}
        run: |
          cd $ch_prefix/charliecloud-*
          sudo make uninstall
          diff -u - <(find $ch_prefix/from-tarball | sort) <<'EOF'
          /var/tmp/from-tarball
          /var/tmp/from-tarball/bin
          /var/tmp/from-tarball/lib
          /var/tmp/from-tarball/libexec
          /var/tmp/from-tarball/share
          /var/tmp/from-tarball/share/doc
          /var/tmp/from-tarball/share/man
          /var/tmp/from-tarball/share/man/man1
          /var/tmp/from-tarball/share/man/man7
          EOF

      - name: rebuild with most things --disable’d
        if: ${{ matrix.builder == 'docker' }}
        run: |
          make distclean
          ./configure --prefix=/doesnotexist \
                      --disable-html --disable-man --disable-ch-image
          set -x
          fgrep 'HTML documentation ... no' config.log
          fgrep 'man pages ... no' config.log
          fgrep 'ch-image(1) ... no' config.log
          fgrep 'with ch-image(1): no' config.log
          # This CI test requires Docker, but the “configure” script doesn’t
          # check for Docker, so we do it ourselves with “command -v”.
          command -v docker
          fgrep 'more complete tests: no' config.log
          set +x
          # Build.
          make $ch_makej
          bin/ch-run --version

      - name: run test suite (Git WD, standard)
        if: ${{ matrix.builder == 'docker' }}
        run: |
          bin/ch-test all
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = no ]]

      - name: remove non-essential dependencies
        if: ${{ matrix.builder == 'docker' }}
        run: |
          set -x
          # This breaks lots of dependencies unrelated to our build but YOLO.
          sudo dpkg --remove --force-depends \
                    pigz \
                    pv \
                    python3-requests \
                    squashfs-tools
          sudo pip3 uninstall -y sphinx sphinx-rtd-theme
          if [[ ${{ matrix.pack_fmt }} = squash-mount ]]; then
              ( cd /usr/local/src/squashfuse && sudo make uninstall )
          fi
          ! python3 -c 'import requests'
          ! python3 -c 'import lark'

      - name: rebuild
        if: ${{ matrix.builder == 'docker' }}
        run: |
          make distclean
          ./configure --prefix=/doesnotexist
          set -x
          fgrep 'documentation: no' config.log
          command -v docker
          fgrep 'more complete tests: no' config.log
          fgrep 'recommended tests, squash-mount mode: no' config.log
          set +x
          # Build and install.
          make $ch_makej
          bin/ch-run --version

      - name: run test suite (Git WD, standard)
        if: ${{ matrix.builder == 'docker' }}
        run: |
          bin/ch-test all
          [[ $(cat /tmp/ch-test.tmp.$USER/rootemu) = no ]]

      - name: print ending environment
        if: ${{ always() }}
        run: |
          free -m
          df -h
          du -sch $CH_TEST_TARDIR/* || true
          du -sch $CH_TEST_IMGDIR/* || true
