Implement cross-architectural and native testing on Gitlab
This change implements, on gitlab.com/racket/racket CI pipelines, cross architectural and native testing using virtualized hardware using user mode qemu. It also fixes use of caches/artifacts to deal with passing of the qemu build and llvm build through stages. Testing added for Racket: Native: armv7l (running on rpi3), x86_64 Emulated: arm64, armel, armhf, i386, mips, mips64el, mipsel, ppc64el, s390x Testing added for Racket CS: Native: x86_64 Emulation: i386
This commit is contained in:
commit
35d269c29e
210
.gitlab-ci.yml
210
.gitlab-ci.yml
|
@ -2,6 +2,7 @@ stages:
|
|||
- prepare
|
||||
- test
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Using debian:stable-slim to perform an llvm build with Z3 support
|
||||
# The reason this is necessary is that we need Z3 support in scan-build
|
||||
# to be able to crosscheck errors with z3 which decreases the false positives
|
||||
|
@ -9,12 +10,13 @@ stages:
|
|||
#
|
||||
# Also, this should only be performed once. Once in cache llvm won't be build anymore
|
||||
# until we force it.
|
||||
prepare-cache:
|
||||
prepare-cache:llvm:
|
||||
image: debian:stable-slim
|
||||
stage: prepare
|
||||
tags:
|
||||
- linux
|
||||
- x86_64
|
||||
- shared-cache
|
||||
variables:
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
script:
|
||||
|
@ -25,16 +27,22 @@ prepare-cache:
|
|||
- mkdir $INSTALL_DIR
|
||||
- mv z3-4.8.4.d6df51951f4c-x64-debian-8.11/bin z3-4.8.4.d6df51951f4c-x64-debian-8.11/include $INSTALL_DIR
|
||||
- export PATH=$INSTALL_DIR/bin:$PATH
|
||||
- git clone https://github.com/llvm/llvm-project.git
|
||||
- git clone -b release/8.x https://github.com/llvm/llvm-project.git
|
||||
- cd llvm-project
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR -DCLANG_ANALYZER_ENABLE_Z3_SOLVER=ON -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_ENABLE_PROJECTS=clang -DZ3_INCLUDE_DIR=$INSTALL_DIR/include/ ../llvm/
|
||||
- cmake -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR -DCLANG_ANALYZER_ENABLE_Z3_SOLVER=ON -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_ENABLE_PROJECTS=clang -DZ3_INCLUDE_DIR=$INSTALL_DIR/include/ -DCMAKE_BUILD_TYPE=MinSizeRel ../llvm/
|
||||
- make -j5
|
||||
- make -j5 install
|
||||
cache:
|
||||
key: llvm-8x-HEAD
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
artifacts:
|
||||
name: "llvm-8x-HEAD"
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
expire_in: 1 week
|
||||
|
||||
# Build racket with scan-build
|
||||
scan-build:racket:
|
||||
|
@ -42,6 +50,7 @@ scan-build:racket:
|
|||
tags:
|
||||
- linux
|
||||
- x86_64
|
||||
- shared-cache
|
||||
variables:
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
before_script:
|
||||
|
@ -51,13 +60,11 @@ scan-build:racket:
|
|||
- export PATH=$INSTALL_DIR/bin:$PATH
|
||||
- export LD_LIBRARY_PATH=$INSTALL_DIR/bin:$LD_LIBRARY_PATH
|
||||
- scan-build -o scan-report_cc -analyzer-config 'crosscheck-with-z3=true' make PKGS="" CPUS=5 CONFIGURE_ARGS_qq='CFLAGS="-O0 -g -DMZ_DECLARE_NORETURN" --disable-strip'
|
||||
dependencies:
|
||||
- prepare-cache:llvm
|
||||
artifacts:
|
||||
paths:
|
||||
- scan-report_cc/
|
||||
cache:
|
||||
policy: pull
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
|
||||
# Build racketcs with scan-build
|
||||
scan-build:racketcs:
|
||||
|
@ -65,6 +72,7 @@ scan-build:racketcs:
|
|||
tags:
|
||||
- linux
|
||||
- x86_64
|
||||
- shared-cache
|
||||
variables:
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
before_script:
|
||||
|
@ -74,14 +82,13 @@ scan-build:racketcs:
|
|||
- export PATH=$INSTALL_DIR/bin:$PATH
|
||||
- export LD_LIBRARY_PATH=$INSTALL_DIR/bin:$LD_LIBRARY_PATH
|
||||
- scan-build -o scan-report-cs_cc -analyzer-config 'crosscheck-with-z3=true' make PKGS="" CPUS=5 CONFIGURE_ARGS_qq='CFLAGS="-O0 -g -DMZ_DECLARE_NORETURN" --disable-strip' cs
|
||||
dependencies:
|
||||
- prepare-cache:llvm
|
||||
artifacts:
|
||||
paths:
|
||||
- scan-report-cs_cc/
|
||||
cache:
|
||||
policy: pull
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
##
|
||||
## The following jobs build/test racket and racketcs with ubsan enabled
|
||||
##
|
||||
|
@ -94,15 +101,6 @@ scan-build:racketcs:
|
|||
- update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 800 --slave /usr/bin/g++ g++ /usr/bin/g++-8
|
||||
- export PATH=$PWD/racket/bin:$PATH
|
||||
|
||||
# This is just used to provide some environment information for debugging purposes
|
||||
envinfo:
|
||||
extends: .prepare
|
||||
script:
|
||||
- cat /proc/cpuinfo
|
||||
- lsb_release -a
|
||||
- gcc -v
|
||||
- export
|
||||
|
||||
test:ubsan:
|
||||
extends: .prepare
|
||||
script:
|
||||
|
@ -161,3 +159,175 @@ test:ubsan:cs:
|
|||
paths:
|
||||
- cs-logs/
|
||||
- runtime-errors.log
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Runs a cross compilation and testing using qemu chrooted into the correct architecture
|
||||
# Each of the test:<arch>: will extend .preparearch which run the proper script
|
||||
# Currently testing:
|
||||
# * arm64
|
||||
# * armel
|
||||
# * armhf
|
||||
# * i386
|
||||
# * mips
|
||||
# * mipsel
|
||||
# * mips64el
|
||||
# * s390x
|
||||
# * ppc64el
|
||||
|
||||
prepare-cache:qemu:
|
||||
image: ubuntu:18.04
|
||||
stage: prepare
|
||||
tags:
|
||||
- linux
|
||||
- x86_64
|
||||
- privileged
|
||||
variables:
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
script:
|
||||
- if [ -d $INSTALL_DIR ]; then exit 0; fi
|
||||
- apt-get update && apt-get install -y gcc make bison flex git wget xz-utils python pkg-config libglib2.0-dev libpixman-1-dev
|
||||
- wget https://download.qemu.org/qemu-3.1.0.tar.xz
|
||||
- tar -xvJf qemu-3.1.0.tar.xz
|
||||
- mkdir qemu-build
|
||||
- cd qemu-build
|
||||
- ../qemu-3.1.0/configure --static --disable-kvm --disable-xen --disable-spice --target-list='i386-linux-user aarch64-linux-user arm-linux-user mips-linux-user mipsel-linux-user mips64el-linux-user s390x-linux-user ppc64le-linux-user riscv64-linux-user' --prefix=$INSTALL_DIR
|
||||
- make -j5
|
||||
- make -j5 install
|
||||
cache:
|
||||
key: qemu-3.1.0
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
artifacts:
|
||||
name: "qemu-3.1.0"
|
||||
paths:
|
||||
- $INSTALL_DIR
|
||||
expire_in: 1 week
|
||||
|
||||
.preparearch:
|
||||
image: ubuntu:18.04
|
||||
stage: test
|
||||
tags:
|
||||
- x86_64
|
||||
- privileged
|
||||
- linux
|
||||
before_script:
|
||||
- ls -la
|
||||
- find $INSTALL_DIR -type f
|
||||
- export PATH=$INSTALL_DIR/bin:$PATH
|
||||
- apt-get update
|
||||
script:
|
||||
- .gitlab/build-test.sh --jobs ${JOBS} --with-arch ${ARCH} --with-debian stretch --with-debian-mirror http://ftp.de.debian.org/debian/ --with-project-path ${CI_PROJECT_DIR} --with-chroot-path /tmp/racket-${ARCH}-${CI_COMMIT_SHORT_SHA}-chroot --with-qemu-path $INSTALL_DIR
|
||||
dependencies:
|
||||
- prepare-cache:qemu
|
||||
|
||||
test:native:x86_64:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "x86_64"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:native:armv7l:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "armv7l"
|
||||
JOBS: 5
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
tags:
|
||||
- armv7l
|
||||
- linux
|
||||
|
||||
test:emulation:arm64:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "arm64"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:armel:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "armel"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:armhf:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "armhf"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:i386:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "i386"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:mips:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "mips"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:mipsel:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "mipsel"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:mips64el:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "mips64el"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:s390x:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "s390x"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:emulation:ppc64el:
|
||||
extends: .preparearch
|
||||
variables:
|
||||
ARCH: "ppc64el"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
|
||||
.preparearch:cs:
|
||||
image: ubuntu:18.04
|
||||
stage: test
|
||||
tags:
|
||||
- x86_64
|
||||
- privileged
|
||||
- linux
|
||||
before_script:
|
||||
- export PATH=$INSTALL_DIR:$PATH
|
||||
- apt-get update
|
||||
script:
|
||||
- .gitlab/build-test.sh --jobs ${JOBS} --with-arch ${ARCH} --with-debian stretch --with-debian-mirror http://ftp.de.debian.org/debian/ --with-project-path ${CI_PROJECT_DIR} --with-chroot-path /tmp/racket-${ARCH}-${CI_COMMIT_SHORT_SHA}-chroot --enable-cs --with-qemu-path $INSTALL_DIR
|
||||
dependencies:
|
||||
- prepare-cache:qemu
|
||||
|
||||
test:native:x86_64:cs:
|
||||
extends: .preparearch:cs
|
||||
allow_failure: true
|
||||
variables:
|
||||
ARCH: "x86_64"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
test:native:i386:cs:
|
||||
extends: .preparearch:cs
|
||||
variables:
|
||||
ARCH: "i386"
|
||||
JOBS: 6
|
||||
INSTALL_DIR: $CI_PROJECT_DIR/install
|
||||
|
||||
|
|
238
.gitlab/build-test.sh
Executable file
238
.gitlab/build-test.sh
Executable file
|
@ -0,0 +1,238 @@
|
|||
#! /bin/bash
|
||||
# This script shows no error on shellcheck:
|
||||
# https://github.com/koalaman/shellcheck
|
||||
set -e
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Script called by jobs in .gitlab-ci.yml to build and test racket,
|
||||
# possibly in a cross environment.
|
||||
|
||||
last_command=
|
||||
current_command=
|
||||
# keep track of the last executed command
|
||||
trap 'last_command=$current_command; current_command=$BASH_COMMAND' DEBUG
|
||||
# echo an error message before exiting
|
||||
trap 'echo "\"${last_command}\" command failed with exit code $?."' EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
|
||||
function usage () {
|
||||
MSG=$1
|
||||
|
||||
echo "${MSG}"
|
||||
echo
|
||||
echo "Usage: ./build-test.sh [--jobs <count>]"
|
||||
echo " [--single-thread]"
|
||||
echo " [--with-arch <arch>]"
|
||||
echo " [--with-debian <debian>]"
|
||||
echo " [--with-debian-mirror <debian-mirror>]"
|
||||
echo " [--with-configure-args <configure-args>]"
|
||||
echo " [--enable-cs]"
|
||||
echo " [--with-project-path <project-path>]"
|
||||
echo " [--with-chroot-path <chroot-path>]"
|
||||
echo " [--with-qemu-path <qemu-path>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
DEBIAN=
|
||||
DEBIAN_MIRROR=
|
||||
JOBS=
|
||||
RACKET_CONFIGURE_ARGS=
|
||||
ARCH="$(uname -m)"
|
||||
BUILD_DIR=${CI_PROJECT_DIR}
|
||||
MAKE_TARGET="in-place"
|
||||
CHROOT_DIR="/tmp/racket-chroot"
|
||||
QEMU_PATH=
|
||||
|
||||
# Parse options
|
||||
until
|
||||
opt=$1
|
||||
case ${opt} in
|
||||
--jobs)
|
||||
shift
|
||||
JOBS=$1
|
||||
;;
|
||||
--single-thread)
|
||||
JOBS=1
|
||||
;;
|
||||
--with-arch)
|
||||
shift
|
||||
ARCH=$1
|
||||
;;
|
||||
--with-debian)
|
||||
shift
|
||||
DEBIAN=$1
|
||||
;;
|
||||
--with-debian-mirror)
|
||||
shift
|
||||
DEBIAN_MIRROR=$1
|
||||
;;
|
||||
--with-configure-args)
|
||||
shift
|
||||
RACKET_CONFIGURE_ARGS=$1
|
||||
;;
|
||||
--enable-cs)
|
||||
MAKE_TARGET="cs"
|
||||
;;
|
||||
--with-project-path)
|
||||
shift
|
||||
BUILD_DIR=$1
|
||||
;;
|
||||
--with-chroot-path)
|
||||
shift
|
||||
CHROOT_DIR=$1
|
||||
;;
|
||||
--with-qemu-path)
|
||||
shift
|
||||
QEMU_PATH=$1
|
||||
;;
|
||||
?*)
|
||||
usage "Unknown argument $1"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
[ "x${opt}" = "x" ]
|
||||
do
|
||||
shift
|
||||
done
|
||||
|
||||
set -eu
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Set QEMU ARCH which depends on ARCH
|
||||
|
||||
if [ ! -e "/.chroot_is_done" ]
|
||||
then
|
||||
QEMU_ARCH=
|
||||
|
||||
case ${ARCH} in
|
||||
"amd64")
|
||||
QEMU_ARCH="x86_64"
|
||||
;;
|
||||
"arm64")
|
||||
QEMU_ARCH="aarch64"
|
||||
;;
|
||||
"armel"|"armhf"|"armv7l")
|
||||
QEMU_ARCH="arm"
|
||||
;;
|
||||
"i386"|"mips"|"mipsel"|"mips64el"|"s390x"|"x86_64")
|
||||
QEMU_ARCH=${ARCH}
|
||||
;;
|
||||
"ppc64el")
|
||||
QEMU_ARCH="ppc64le"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown architecture ${ARCH}"
|
||||
echo "Available archs: amd64, arm64, armel, armhf, armv7l, i386, mips, mipsel, mips64el, s390x, ppc64el"
|
||||
echo "These are the official names for the debian ports available listed at:"
|
||||
echo "https://www.debian.org/ports/"
|
||||
echo "NOTE: we also accept x86_64 as an alias for amd64"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Packages to install on the HOST
|
||||
HOST_DEPENDENCIES="debootstrap binfmt-support sbuild rsync"
|
||||
|
||||
# Packages to install on the GUEST
|
||||
GUEST_DEPENDENCIES="devscripts build-essential git m4 sudo python libfontconfig1-dev make gcc libpango1.0-dev libcairo2-dev openssl emacs25-nox libturbojpeg0-dev uuid-dev"
|
||||
|
||||
function setup_chroot {
|
||||
# Host dependencies
|
||||
echo "Installing host dependencies"
|
||||
apt-get install -y ${HOST_DEPENDENCIES}
|
||||
|
||||
# Create chrooted environment
|
||||
echo "Creating chroot environment"
|
||||
mkdir "${CHROOT_DIR}"
|
||||
debootstrap --foreign --no-check-gpg --include=fakeroot,build-essential \
|
||||
--arch="${ARCH}" "${DEBIAN}" "${CHROOT_DIR}" "${DEBIAN_MIRROR}"
|
||||
cp ${QEMU_PATH}/bin/qemu-${QEMU_ARCH} "${CHROOT_DIR}"/usr/bin/qemu-${QEMU_ARCH}-static
|
||||
chroot "${CHROOT_DIR}" ./debootstrap/debootstrap --second-stage
|
||||
sbuild-createchroot --arch="${ARCH}" --foreign --setup-only \
|
||||
"${DEBIAN}" "${CHROOT_DIR}" "${DEBIAN_MIRROR}"
|
||||
|
||||
# Install dependencies inside chroot
|
||||
echo "Installing guest dependencies"
|
||||
chroot "${CHROOT_DIR}" apt-get update
|
||||
chroot "${CHROOT_DIR}" apt-get --allow-unauthenticated install \
|
||||
-y ${GUEST_DEPENDENCIES}
|
||||
|
||||
# Create build dir and copy travis build files to our chroot environment
|
||||
echo "Copying into chroot: ${BUILD_DIR}/ -> ${CHROOT_DIR}/${BUILD_DIR}/"
|
||||
mkdir -p "${CHROOT_DIR}"/"${BUILD_DIR}"
|
||||
rsync -av "${BUILD_DIR}"/ "${CHROOT_DIR}"/"${BUILD_DIR}"/
|
||||
|
||||
# Indicate chroot environment has been set up
|
||||
touch "${CHROOT_DIR}"/.chroot_is_done
|
||||
|
||||
# Call ourselves again which will cause tests to run
|
||||
echo "Recursively calling script"
|
||||
if [ ${MAKE_TARGET} = "cs" ]; then
|
||||
chroot "${CHROOT_DIR}" bash -c "cd ${BUILD_DIR} && ./.gitlab/build-test.sh --jobs ${JOBS} --with-arch ${ARCH} --with-project-path ${BUILD_DIR} --enable-cs"
|
||||
else
|
||||
chroot "${CHROOT_DIR}" bash -c "cd ${BUILD_DIR} && ./.gitlab/build-test.sh --jobs ${JOBS} --with-arch ${ARCH} --with-project-path ${BUILD_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Information about environment
|
||||
echo "Environment information"
|
||||
echo "======================="
|
||||
echo " Machine : $(uname -m)"
|
||||
echo " Jobs : ${JOBS}"
|
||||
echo " Target Arch : ${ARCH}"
|
||||
echo " chroot Path : ${CHROOT_DIR}"
|
||||
echo " Build Path : ${BUILD_DIR}"
|
||||
echo " Make Target : ${MAKE_TARGET}"
|
||||
echo " Debian : ${DEBIAN}"
|
||||
echo " Debian Mirror : ${DEBIAN_MIRROR}"
|
||||
echo "Racket Configure Args : ${RACKET_CONFIGURE_ARGS}"
|
||||
if [ ! -e "/.chroot_is_done" ]; then
|
||||
echo " QEMU Arch : ${QEMU_ARCH}"
|
||||
fi
|
||||
|
||||
if [ ! -e "/.chroot_is_done" ]; then
|
||||
if [ "${ARCH}" != "$(uname -m)" ]; then
|
||||
# test run, need to set up chrooted environment first
|
||||
echo "Setting up chrooted ${ARCH} environment"
|
||||
setup_chroot
|
||||
else # We are compiling and running tests natively
|
||||
apt-get install -y ${GUEST_DEPENDENCIES}
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Compiling"
|
||||
echo "Environment: $(uname -a)"
|
||||
|
||||
annotate-output make CPUS=${JOBS} \
|
||||
PKGS="racket-test db-test unstable-flonum-lib net-test" \
|
||||
CONFIGURE_ARGS_qq="${RACKET_CONFIGURE_ARGS}" \
|
||||
${MAKE_TARGET}
|
||||
|
||||
echo "Running tests"
|
||||
echo "Environment: $(uname -a)"
|
||||
|
||||
export PATH=${BUILD_DIR}/racket/bin:$PATH
|
||||
command -v racket
|
||||
racket -v
|
||||
annotate-output raco test -l tests/racket/test
|
||||
annotate-output racket -l tests/racket/contract/all
|
||||
annotate-output raco test -l tests/json/json
|
||||
annotate-output raco test -l tests/file/main
|
||||
annotate-output raco test -l tests/net/head
|
||||
annotate-output raco test -l tests/net/uri-codec
|
||||
annotate-output raco test -l tests/net/url
|
||||
annotate-output raco test -l tests/net/url-port
|
||||
annotate-output raco test -l tests/net/encoders
|
||||
annotate-output raco test -l tests/openssl/basic
|
||||
annotate-output raco test -l tests/openssl/https
|
||||
annotate-output raco test -l tests/match/main
|
||||
annotate-output raco test -l tests/zo-path
|
||||
annotate-output raco test -l tests/xml/test
|
||||
annotate-output raco test -l tests/db/all-tests
|
||||
annotate-output raco test -c tests/stxparse
|
||||
|
||||
echo "DONE!"
|
Loading…
Reference in New Issue
Block a user