From 6835a99a721da8f50e58a180574c25f86364e1a7 Mon Sep 17 00:00:00 2001 From: Brett Viren Date: Thu, 2 Mar 2023 08:49:52 -0500 Subject: [PATCH 01/32] Import waf-tools to waft/ --- waft/README.org | 224 +++++++++++++++++++++++++++ waft/cuda.py | 44 ++++++ waft/eigen.py | 33 ++++ waft/generic.py | 156 +++++++++++++++++++ waft/kokkos.py | 63 ++++++++ waft/libtorch.py | 19 +++ waft/make-release.sh | 268 +++++++++++++++++++++++++++++++++ waft/protobuf.py | 56 +++++++ waft/refresh-wcb | 115 ++++++++++++++ waft/rootsys.py | 135 +++++++++++++++++ waft/rpathify.py | 25 +++ waft/setup-for-nix.sh | 23 +++ waft/smplpkgs.py | 257 +++++++++++++++++++++++++++++++ waft/sourceme-ups.sh | 28 ++++ waft/test-release.sh | 115 ++++++++++++++ waft/wcb.py | 177 ++++++++++++++++++++++ waft/wct-configure-for-nix.sh | 93 ++++++++++++ waft/wct-configure-for-ups.sh | 69 +++++++++ waft/wct-configure-for-view.sh | 67 +++++++++ 19 files changed, 1967 insertions(+) create mode 100644 waft/README.org create mode 100644 waft/cuda.py create mode 100644 waft/eigen.py create mode 100644 waft/generic.py create mode 100644 waft/kokkos.py create mode 100644 waft/libtorch.py create mode 100755 waft/make-release.sh create mode 100644 waft/protobuf.py create mode 100755 waft/refresh-wcb create mode 100644 waft/rootsys.py create mode 100644 waft/rpathify.py create mode 100644 waft/setup-for-nix.sh create mode 100644 waft/smplpkgs.py create mode 100644 waft/sourceme-ups.sh create mode 100755 waft/test-release.sh create mode 100644 waft/wcb.py create mode 100755 waft/wct-configure-for-nix.sh create mode 100755 waft/wct-configure-for-ups.sh create mode 100755 waft/wct-configure-for-view.sh diff --git a/waft/README.org b/waft/README.org new file mode 100644 index 000000000..cc5e27dad --- /dev/null +++ b/waft/README.org @@ -0,0 +1,224 @@ +#+title: Wire-Cell Builder Waf Tools + +This repository holds some [[https://waf.io][Waf]] tools to help build the [[https://wirecell.bnl.gov][Wire-Cell +Toolkit]] (or other) software. The tools are bundled into a single +program called ~wcb~, the Wire-Cell Builder. + +* Packing + +The ~wcb~ command is a packed version of waf with extra tools. A script is provided to automate rebuilding ~wcb~: + +#+begin_example + $ ./refresh-wcb -o /path/to/your/wire-cell-toolkit/wcb +#+end_example + +When WCT is updated it's a good idea to tag ~waf-tools~. Example session: + +#+begin_example + $ cd /path/to/your/waf-tools + $ ./refresh-wcb -o /path/to/your/wire-cell-toolkit/wcb + $ cd /path/to/your/wire-cell-toolkit + (...test...) + $ git commit -am "Refresh to wcb X.Y.Z" && git push + $ cd /path/to/your/waf-tools + $ git tag -am "...useful message..." X.Y.Z + $ git push --tags +#+end_example + + +Th refresh script essentially enacts this recipe: + +#+BEGIN_EXAMPLE + $ git clone https://github.com/WireCell/waf-tools.git + $ WTEXTRA=$(echo $(pwd)/waf-tools/*.py | tr ' ' ,) + + $ git clone https://gitlab.com/ita1024/waf.git + $ cd waf/ + $ ./waf-light --tools=doxygen,boost,bjam,$WTEXTRA + ... + adding /home/bv/dev/waf-tools/smplpkgs.py as waflib/extras/smplpkgs.py + adding /home/bv/dev/waf-tools/wcb.py as waflib/extras/wcb.py + ... + $ cp waf /path/to/your/wire-cell-toolkit/wcb +#+END_EXAMPLE + +* Using the ~wcb~ tool + +On well provisioned systems, ~wcb~ builds the software automatically: + +#+begin_example + $ ./wcb configure --prefix=/path/to/install + $ ./wcb + $ ./wcb install +#+end_example + +In some environments, ~wcb~ may need help to find dependencies. Hints +can be given with ~--with-*~ type flags. To see available flags use the +online help: + +#+BEGIN_EXAMPLE + $ ./wcb --help +#+END_EXAMPLE + +Packages can be included, excluded and located with the various +~--with-NAME*~ flags. The rules work as follows: + +1) If package is optional: + + - omitting a ~--with-NAME*~ option will omit use the package + + - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will omit + use of the package. + +2) If package is mandatory: + + - omitting all ~--with-NAME*~ options will use ~pkg-config~ to find the + package. + + - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will + assert. + +3) In either case: + + - explicitly using ~--with-NAME=true~ (or "~yes~" or "~on~") will use + pkg-config to find the package. + + - using ~--with-NAME*! with a path will attempt to locate the package + without using ~pkg-config~. + +When in doubt, explicitly include ~--with-NAME*~ flags. + +* Using the =smplpkgs= tool to build suites of packages + +The =smplpkgs= tool included in =waf-tools= provides a simple way to +build a suite of software packages that have interdependencies without +you, the developer, having to care much about the build system. + +** Package organization + +To achieve this simplicity, some file and directory naminging +conventions and organization must be followed, as illustrated: + +#+BEGIN_EXAMPLE + pkg1/ + ├── wscript_build + ├── inc/ + │   └── ProjectNamePkg1/*.h + ├── src/*.{cxx,h} + └── test/*.{cxx,h} + pkg2/ + ├── wscript_build + ├── inc/ + │   └── ProjectNamePkg2/*.h + ├── src/*.{cxx,h} + ├── app/*.{cxx,h} + └── test/*.{cxx,h} +#+END_EXAMPLE + +Notes on the structure: + +- All packages placed in a top-level directory (not required, but aggregating them via =git submodule= is useful). +- Public header files for the package must be placed under =/inc//= +- Library source (implementation and private headers) under =/src/= +- Application source (implementation and private headers) under =/app/= with only main application files and one application per =*.cxx= file. +- Test source (implementation and private headers) under =/test/= with main test programs named like =test_*.cxx= +- A short `wscript_build` file in each package. + +The == only matters in the top-level =wscript= file which you must provide. The == matters for inter-package dependencies. + + +** The per-package =wscript_build= file + +Each package needs a brief (generally single line) file called =wscript_build= to exist at in its top-level directory. It is responsible for declaring: + +- The package name +- Library dependencies +- Any additional application dependencies +- Any additional test dependencies + +Example: + +#+BEGIN_SRC python + bld.smplpkg('MyPackage', use='YourPackage YourOtherPackage') +#+END_SRC + +Test and application programs are allowed to have additional dependencies declared. For example: + +#+BEGIN_SRC python + bld.smplpkg('MyPackage', use='YourPackage YourOtherPackage', test_use='ROOTSYS') +#+END_SRC + +* Using ~wcb~ in your own build + +The ~wcb~ command is designed to build Wire-Cell Toolkit and is not a +general purpose build tool. However, it may be used to build packages +that are providing WCT plugin libraries or other derived packages. + +To use it follow these steps: + +1) copy WCT's [[https://github.com/WireCell/wire-cell-toolkit/blob/master/wscript][wscript]] and [[https://github.com/WireCell/wire-cell-toolkit/blob/master/wcb][wcb]] to your package directory. + +2) create directory layout (see below) + +3) possibly modify ~wscript~ (see below) + +4) follow normal ~wcb~ build instructions + +An example package is available at + + + +** Directory layout options + +You may create a package with one or more subpackages like WCT itself +in which case each subpackage should have a ~wscript_build~ file as +described above. + +Or, a simple package may be created with ~inc/~, ~src/~, etc directly in +the top-level directory. Simply put the contents of a ~wscript_build~ +file in the main ~wscript~ file in the ~build()~ function. For example: + +#+begin_src python +def build(bld): + bld.load('wcb') + bld.smplpkg('WireCellJunk', use='WireCellUtil') +#+end_src + +** Modify ~wscript~ + +The ~wcb~ tool is created to find WCT's dependencies but not WCT itself. +Nor does it predict new dependencies your own package may need. +However, it has a simple mechanism to extend the method to search for +dependencies. In the ~wscript~ file, at top level the following code +extends ~wcb~ to find WCT itself. + +#+begin_src python +from waflib.extras import wcb +wcb.package_descriptions["WCT"] = dict( + incs=["WireCellUtil/Units.h"], + libs=["WireCellUtil"], + mandatory=True) +#+end_src + + +** The top-level =wscript= file + +The ~wscript~ file is Waf's equivalent to a venerable ~Makefile~. Almost +all functionality is bundled into ~wcb~ so the ~wscript~ is relatively +empty. Refer to WCT's: + + + +* Internals + +The ~wcb.py~ file holds what might otherwise be in a top-level ~wscript~ +file. It's main thing is to list externals that can be handled in a +generic way (see next para) and also doing any non-generic +configuration. It also enacts some dependency analysis to avoid +building some sub-packages. + +The ~generic.py~ file provides a ~configure()~ method used to find most +externals. It results in defining ~HAVE__LIB~ and ~HAVE__INC~ +when libs or includes are successfully checked for a given package. +These end up in ~config.h~ for use in C++ code. + diff --git a/waft/cuda.py b/waft/cuda.py new file mode 100644 index 000000000..ec73c32f2 --- /dev/null +++ b/waft/cuda.py @@ -0,0 +1,44 @@ +from . import generic + +from waflib import Task +from waflib.TaskGen import extension +from waflib.Tools import ccroot, c_preproc +from waflib.Configure import conf + +import os + +# from waf's playground +class cuda(Task.Task): + run_str = '${NVCC} ${NVCCFLAGS} ${FRAMEWORKPATH_ST:FRAMEWORKPATH} ${CPPPATH_ST:INCPATHS} ${DEFINES_ST:DEFINES} ${CXX_SRC_F}${SRC} ${CXX_TGT_F} ${TGT}' + color = 'GREEN' + ext_in = ['.h'] + vars = ['CCDEPS'] + scan = c_preproc.scan + shell = False + +@extension('.cu', '.cuda') +def c_hook(self, node): + return self.create_compiled_task('cuda', node) + +@extension('.cxx') +def cxx_hook(self, node): + # override processing for one particular type of file + if getattr(self, 'cuda', False): + return self.create_compiled_task('cuda', node) + else: + return self.create_compiled_task('cxx', node) + +def options(opt): + generic._options(opt, "CUDA") + +def configure(cfg): + + generic._configure(cfg, "CUDA", mandatory=False, + incs=["cuda.h"], libs=["cuda","cudart"], bins=["nvcc"]) + + if not 'HAVE_CUDA' in cfg.env: + return + nvccflags = "-shared -Xcompiler -fPIC " + nvccflags += os.environ.get("NVCCFLAGS","") + cfg.env.NVCCFLAGS += nvccflags.strip().split() + print ("NVCCFLAGS = %s" % (' '.join(cfg.env.NVCCFLAGS))) diff --git a/waft/eigen.py b/waft/eigen.py new file mode 100644 index 000000000..ce4a099d6 --- /dev/null +++ b/waft/eigen.py @@ -0,0 +1,33 @@ +import os +import os.path as osp +from waflib.Configure import conf + +def options(opt): + opt = opt.add_option_group('Eigen Options') + opt.add_option('--with-eigen', type='string', + help="give Eigen3 installation location") + + +@conf +def check_eigen(ctx, mandatory=True): + instdir = ctx.options.with_eigen + + if instdir is None or instdir.lower() in ['yes','true','on']: + ctx.start_msg('Checking for Eigen in PKG_CONFIG_PATH') + # note: Eigen puts its eigen3.pc file under share as there is + # no lib. Be sure your PKG_CONFIG_PATH reflects this. + ctx.check_cfg(package='eigen3', uselib_store='EIGEN', args='--cflags --libs', mandatory=mandatory) + elif instdir.lower() in ['no','off','false']: + return + else: + ctx.start_msg('Checking for Eigen in %s' % instdir) + ctx.env.INCLUDES_EIGEN = [ osp.join(instdir,'include/eigen3') ] + + ctx.check(header_name="Eigen/Dense", use='EIGEN', mandatory=mandatory) + if len(ctx.env.INCLUDES_EIGEN): + ctx.end_msg(ctx.env.INCLUDES_EIGEN[0]) + else: + ctx.end_msg('Eigen3 not found') + +def configure(cfg): + cfg.check_eigen() diff --git a/waft/generic.py b/waft/generic.py new file mode 100644 index 000000000..204442c57 --- /dev/null +++ b/waft/generic.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +''' +This is NOT a waf tool but generic functions to be called from a waf +tool, in particular by wcb.py. + +There's probably a wafier way to do this. + +The interpretation of options are very specific so don't change them +unless you really know all the use cases. The rules are: + +If package is optional: + + - omitting all --with-NAME* options will omit use the package + + - explicitly using --with-NAME=false (or "no" or "off") will omit + use of the package. + +If package is mandatory: + + - omitting all --with-NAME* options will use pkg-config to find + the package. + + - explicitly using --with-NAME=false (or "no" or "off") will + assert. + +In either case: + + - explicitly using --with-NAME=true (or "yes" or "on") will use + pkg-config to find the package. + + - using --with-NAME* with a path will attempt to locate the + package without using pkg-config + +Note, actually, pgk-config fails often to do its job. Best to always +use explicit --with-NAME=[|]. +''' + +import sys +import os.path as osp +def _options(opt, name, incs=None, libs=None): + lower = name.lower() + opt = opt.add_option_group('%s Options' % name) + opt.add_option('--with-%s'%lower, type='string', default=None, + help="give %s installation location" % name) + if incs is None or len(incs): + opt.add_option('--with-%s-include'%lower, type='string', + help="give %s include installation location"%name) + if libs is None or len(libs): + opt.add_option('--with-%s-lib'%lower, type='string', + help="give %s lib installation location"%name) + opt.add_option('--with-%s-libs'%lower, type='string', + help="list %s link libraries"%name) + return + +def _configure(ctx, name, incs=(), libs=(), bins=(), pcname=None, mandatory=True, extuses=()): + lower = name.lower() + UPPER = name.upper() + if pcname is None: + pcname = lower + extuses = list(extuses) + + instdir = getattr(ctx.options, 'with_'+lower, None) + incdir = getattr(ctx.options, 'with_%s_include'%lower, None) + libdir = getattr(ctx.options, 'with_%s_lib'%lower, None) + bindir = getattr(ctx.options, 'with_%s_bin'%lower, None) + if libs: + maybe = getattr(ctx.options, 'with_%s_libs'%lower, None) + if maybe: + libs = [s.strip() for s in maybe.split(",") if s.strip()] + + #print ("CONFIGURE", name, instdir, incdir, libdir, mandatory, extuses) + + if mandatory: + if instdir: + assert (instdir.lower() not in ['no','off','false']) + else: # optional + # if not any([instdir, incdir, libdir]): + # print ("skipping non mandatory %s, use --with-%s=[yes|] to force" % (name, lower)) + # return + if instdir and instdir.lower() in ['no','off','false']: + return + + # rely on package config + if not any([instdir,incdir,libdir,bindir]) or (instdir and instdir.lower() in ['yes','on','true']): + ctx.start_msg('Checking for %s in PKG_CONFIG_PATH' % name) + args = "--cflags" + if libs: # things like eigen may not have libs + args += " --libs" + ctx.check_cfg(package=pcname, uselib_store=UPPER, + args=args, mandatory=mandatory) + if 'HAVE_'+UPPER in ctx.env: + ctx.end_msg("located by pkg-config") + else: + ctx.end_msg("missing from pkg-config") + return + else: # do manual setting + + if incs: + if not incdir and instdir: + incdir = osp.join(instdir, 'include') + if incdir: + setattr(ctx.env, 'INCLUDES_'+UPPER, [incdir]) + + if libs: + if not libdir and instdir: + libdir = osp.join(instdir, 'lib') + if libdir: + setattr(ctx.env, 'LIBPATH_'+UPPER, [libdir]) + + if bins: + if not bindir and instdir: + bindir = osp.join(instdir, 'bin') + if libdir: + setattr(ctx.env, 'PATH_'+UPPER, [bindir]) + + + # now check, this does some extra work in the caseof pkg-config + + if libs: + ctx.start_msg("Location for %s libs" % (name,)) + for tryl in libs: + ctx.check_cxx(lib=tryl, define_name='HAVE_'+UPPER+'_LIB', + use=[UPPER] + extuses, uselib_store=UPPER, mandatory=mandatory) + ctx.end_msg(str(getattr(ctx.env, 'LIBPATH_' + UPPER, None))) + + ctx.start_msg("Libs for %s" % UPPER) + have_libs = getattr(ctx.env, 'LIB_' + UPPER, None) + ctx.end_msg(str(have_libs)) + if ctx.is_defined('HAVE_'+UPPER+'_LIB'): + ctx.env['HAVE_'+UPPER] = 1 + print('HAVE %s libs' % UPPER) + else: + print('NO %s libs' % UPPER) + + + if incs: + ctx.start_msg("Location for %s headers" % name) + for tryh in incs: + ctx.check_cxx(header_name=tryh, define_name='HAVE_'+UPPER+'_INC', + use=[UPPER] + extuses, uselib_store=UPPER, mandatory=mandatory) + have_incs = getattr(ctx.env, 'INCLUDES_' + UPPER, None) + ctx.end_msg(str(have_incs)) + if ctx.is_defined('HAVE_'+UPPER+'_INC'): + ctx.env['HAVE_'+UPPER] = 1 + print('HAVE %s includes' % UPPER) + else: + print('NO %s includes' % UPPER) + + if bins: + ctx.start_msg("Bins for %s" % name) + found_bins = list() + for tryb in bins: + ctx.find_program(tryb, var=tryb.upper(), mandatory=mandatory) + found_bins += ctx.env[tryb.upper()] + ctx.end_msg(str(found_bins)) + diff --git a/waft/kokkos.py b/waft/kokkos.py new file mode 100644 index 000000000..138ba7c70 --- /dev/null +++ b/waft/kokkos.py @@ -0,0 +1,63 @@ +from . import generic + +from waflib import Task +from waflib.TaskGen import extension +from waflib.Tools import ccroot, c_preproc +from waflib.Configure import conf + +import os + +# from waf's playground +class kokkos_gcc(Task.Task): + run_str = '${CXX} ${KOKKOS_CXXFLAGS} ${CXXFLAGS} ${FRAMEWORKPATH_ST:FRAMEWORKPATH} ${CPPPATH_ST:INCPATHS} ${DEFINES_ST:DEFINES} ${CXX_SRC_F}${SRC} ${CXX_TGT_F} ${TGT}' + color = 'GREEN' + ext_in = ['.h'] + vars = ['CCDEPS'] + scan = c_preproc.scan + shell = False + +class kokkos_cuda(Task.Task): + run_str = '${NVCC} ${KOKKOS_NVCCFLAGS} ${NVCCFLAGS} ${FRAMEWORKPATH_ST:FRAMEWORKPATH} ${CPPPATH_ST:INCPATHS} ${DEFINES_ST:DEFINES} ${CXX_SRC_F}${SRC} ${CXX_TGT_F} ${TGT}' + color = 'GREEN' + ext_in = ['.h'] + vars = ['CCDEPS'] + scan = c_preproc.scan + shell = False + +@extension('.kokkos') +def kokkos_hook(self, node): + options = getattr(self.env, 'KOKKOS_OPTIONS', None) + if 'cuda' in options: + # print('use nvcc on ', node) + return self.create_compiled_task('kokkos_cuda', node) + else: + # print('use gcc on ', node) + return self.create_compiled_task('kokkos_gcc', node) + +def options(opt): + generic._options(opt, "KOKKOS") + opt.add_option('--kokkos-options', type='string', help="cuda, ...") + +def configure(cfg): + generic._configure(cfg, "KOKKOS", mandatory=False, + incs=["Kokkos_Macros.hpp"], libs=["kokkoscore", "kokkoscontainers", "dl"], bins=["nvcc"]) + + options = getattr(cfg.options, 'kokkos_options', None) + setattr(cfg.env, 'KOKKOS_OPTIONS', options) + options = getattr(cfg.env, 'KOKKOS_OPTIONS', None) + cfg.start_msg("KOKKOS_OPTIONS:") + cfg.end_msg(str(options)) + + if not 'HAVE_KOKKOS' in cfg.env: + return + nvccflags = "-x cu -shared -Xcompiler -fPIC " + # nvccflags += "--std=c++11 " + nvccflags += "-Xcudafe --diag_suppress=esa_on_defaulted_function_ignored -expt-extended-lambda -arch=sm_75 -Xcompiler -fopenmp " + nvccflags += os.environ.get("NVCCFLAGS","") + cfg.env.KOKKOS_NVCCFLAGS += nvccflags.strip().split() + cxxflags = " -x c++ " + cfg.env.KOKKOS_CXXFLAGS += cxxflags.strip().split() + cfg.start_msg("KOKKOS_NVCCFLAGS:") + cfg.end_msg(str(cfg.env.KOKKOS_NVCCFLAGS)) + cfg.start_msg("KOKKOS_CXXFLAGS:") + cfg.end_msg(str(cfg.env.KOKKOS_CXXFLAGS)) diff --git a/waft/libtorch.py b/waft/libtorch.py new file mode 100644 index 000000000..9316468d5 --- /dev/null +++ b/waft/libtorch.py @@ -0,0 +1,19 @@ +from . import generic + +def options(opt): + generic._options(opt, "libtorch") + +def configure(cfg): + # warning, list of libs changes over version. + + libs = getattr(cfg.options, "with_libtorch_libs", None) + if not libs: + libs = ['torch', 'torch_cpu', 'torch_cuda', 'c10_cuda', 'c10'] + + generic._configure(cfg, "libtorch", + incs=["torch/script.h"], + libs=libs, + mandatory=False) + if 'torch_cuda' in libs: + setattr(cfg.env, 'LINKFLAGS_LIBTORCH', + ['-Wl,--no-as-needed,-ltorch_cuda','-Wl,--as-needed']) diff --git a/waft/make-release.sh b/waft/make-release.sh new file mode 100755 index 000000000..3387e728a --- /dev/null +++ b/waft/make-release.sh @@ -0,0 +1,268 @@ +#!/bin/bash + +cat </dev/null +} +function goback +{ + popd >/dev/null +} + +function make-branch +{ + local branch=${1?must provide branch} ; shift + goto $branch + + if [ -n "$(git show-ref refs/heads/$branch)" ] ; then + echo "Already have local: $branch" + git checkout $branch + elif [ -n "$(git show-ref remotes/origin/$branch)" ] ; then + echo "Branch on origin: $branch" + git checkout -b $branch origin/$branch + else + echo "Initial creation of branch: $branch" + git checkout -b $branch origin/master + fi + goback +} + +function purge-submodules +{ + local branch=${1?must provide branch} ; shift + goto $branch + local submodules=$@ + + for sm in $submodules ; do + if [ -d $sm ] ; then + git submodule deinit $sm || exit 1 + git rm $sm || exit 1 + fi + done + goback +} +# alg bio dfp rio riodata rootdict rootvis tbb + +function branch-submodules +{ + local branch=${1?must provide branch} ; shift + goto $branch + + git submodule init || exit 1 + git submodule update || exit 1 + + git submodule foreach "git checkout $branch || git checkout -b $branch --track origin/master" + + goback +} + +function update-submodules +{ + local branch=${1?must provide branch} ; shift + goto $branch + git pull --recurse-submodules + goback +} + +function fix-dotgitmodules +{ + local branch=${1?must provide branch} ; shift + + local org="WireCell" + local dev_url="git@github.com:$org" + local usr_url="https://github.com/$org" + + goto $branch + + # move to anon-friend URL for releases + sed -i -e 's|'$dev_url'|'$usr_url'|'g .gitmodules + + # Crazy hack to make sure .gitmodules is updated with new branch + # for each surviving submodule. There is probably a better way to + # do this! + git submodule foreach 'branch="$(git --git-dir=../.git rev-parse --abbrev-ref HEAD)"; sm="$(basename $(pwd))"; git config -f ../.gitmodules submodule.$sm.branch $branch' + + # Do NOT actually sync + #git submodule sync + + goback +} + + +# kitchen sink function +function bring-forward +{ + local branch=${1?must give branch} ; shift + + echo -e "\ngetting source\n" + get-source $branch || exit 1 + + echo -e "\nmaking top branch\n" + make-branch $branch ||exit 1 + + unwanted_submodules="alg bio dfp rio riodata rootdict rootvis tbb" + echo -e "\npurging submodules: $unwanted_submodules\n" + purge-submodules $branch $unwanted_submodules + + echo -e "\nbranching submodules\n" + branch-submodules $branch + + echo -e "\nupdating submodules\n" + update-submodules $branch + + echo -e "\nswitch submodule URLs\n" + fix-dotgitmodules $branch + + + # fixme: specify which submodules to keep, purge all others + # fixme bonus1: hard code some (waftools, util, iface) + # fixme bonus2: figure out dependencies! + +} +# now do tag-submodules, push-submodules, submodule-urls, push-main + +function apply-submodule-tags +{ + local branch=${1?must provide branch} ; shift + local tag=${1?must give branch} ; shift + local message="$@" + + goto $branch + git submodule foreach git tag -a -m "$message" $tag + goback +} + + +function commit-tag-main +{ + local branch=${1?must provide branch} ; shift + local tag=${1?must give branch} ; shift + local message=${1?must give message} ; shift + + goto $branch + git commit -a -m "$message" + git tag -a -m "$message" "$tag" + goback +} +# now by hand: git push origin $branch + + +# kitchen sink function +function apply-tags +{ + local branch=${1?must provide branch} ; shift + local tag=${1?must give tag} ; shift + local message="$@" + + echo -e "\napplying submodule tags\n" + apply-submodule-tags $branch $tag "$message" + + echo -e "\ncommitting and tagging top\n" + commit-tag-main $branch $tag "$message" + +} + + +# warning this pushes! +function push-everything +{ + local branch=${1?must provide source directory aka branch} ; shift + goto $branch + + git submodule foreach git push origin $branch + git submodule foreach git push --tags + + git push origin $branch + git push --tags + + goback +} + +function tarball +{ + local branch=${1?must give branch} ; shift + local tag=${1?must give tag} ; shift + local base=${1:-wire-cell-toolkit} ; shift + local name="${base}-${tag}" + local tarfile="${name}.tar.gz" + + if [ -f "$tarfile" ] ; then + echo "target tarball file exists, remove to continue: $tarfile" + return + fi + if [ -f "$name" ] ; then + echo "target directory exists, remove to continue: $name" + return; + fi + + git clone --recurse-submodules --branch=$branch \ + git@github.com:WireCell/wire-cell-build.git \ + $name + + cd $name + git checkout $tag + git submodule init + git submodule update + cd .. + + tar --exclude=.git* -czf $tarfile $name + + echo "when happy:" + echo "cp $tarfile /var/www/lar/software/releases" +} + + +"$@" + diff --git a/waft/protobuf.py b/waft/protobuf.py new file mode 100644 index 000000000..1b1553db9 --- /dev/null +++ b/waft/protobuf.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# encoding: utf-8 +# Philipp Bender, 2012 + +from waflib.Task import Task +from waflib.TaskGen import extension + +import generic + +""" +A simple tool to integrate protocol buffers into your build system. + + def configure(conf): + conf.load('compiler_cxx cxx protoc_cxx') + + def build(bld): + bld.program(source = "main.cpp file1.proto proto/file2.proto", + target = "executable") + +""" + +class protoc(Task): + # the cp is to bring the .h file into the build working directory + run_str = '${PROTOC} ${SRC} --cpp_out=. -I.. && cp ${TGT[1].abspath()} .' + color = 'BLUE' + ext_out = ['.h', 'pb.cc'] + +class protopy(Task): + run_str = '${PROTOC} ${SRC} --python_out=. -I..' + color = 'BLUE' + ext_out = ['pb2.py'] + + + +@extension('.proto') +def process_protoc(self, node): + py_node = node.change_ext('_pb2.py') + cpp_node = node.change_ext('.pb.cc') + hpp_node = node.change_ext('.pb.h') + self.create_task('protopy', node, [py_node]) + self.create_task('protoc', node, [cpp_node, hpp_node]) + self.source.append(cpp_node) + self.env.append_value('INCLUDES', ['.'] ) + self.use = self.to_list(getattr(self, 'use', '')) + ['PROTOBUF'] + + + + +def options(opt): + generic._options(opt, 'protobuf') + +def configure(cfg): + generic._configure(cfg, 'protobuf', + incs=('google/protobuf/message.h',), libs=('protobuf',), + mandatory=False) + cfg.find_program('protoc', var='PROTOC', mandatory=False) diff --git a/waft/refresh-wcb b/waft/refresh-wcb new file mode 100755 index 000000000..534a620f7 --- /dev/null +++ b/waft/refresh-wcb @@ -0,0 +1,115 @@ +#!/bin/bash + +# This script will recreate the "wcb" Waf script used to build the +# Wire Cell Toolkit. Normally only developers exercise it. + +output="$(pwd)/wcb" +wafurl="https://gitlab.com/ita1024/waf.git" +wafdir="$(pwd)/waf" +waftag="waf-2.0.20" + + + +toolsurl="https://github.com/WireCell/waf-tools.git" +tooldir="$(pwd)" +# initial defaults from waf +tools="doxygen boost bjam" +# add any in cwd, assuming we are sitting in waf-tools +tools="$tools $(echo *.py| sed 's/.py//g')" + + + +usage () { + echo "refresh-wcb [options] [tools]" + echo "Default tools: $tools" + echo "Likely call:" + echo "./refresh-wcb -o /path/to/wire-cell-toolkit/wcb" + exit 1; +} + +while getopts "v:w:t:o:W:T:" opt; do + case "$opt" in + v) + waftag="waf-$OPTARG" + ;; + w) + wafdir="$(readlink -f $OPTARG)" + ;; + t) + tooldir="$(readlink -f $OPTARG)" + ;; + o) + output=$OPTARG + ;; + W) + wafurl=$OPTARG + ;; + T) + toolsurl=$OPTARG + ;; + *) + usage + ;; + esac +done +shift $((OPTIND-1)) +[ "$1" = "--" ] && shift +if [ -n "$1" ] ; then + tools="$@" +fi + +if [ -d "$wafdir" ] ; then + pushd $wafdir + git fetch + git checkout $waftag + popd +else + git clone "$wafurl" "$wafdir" + pushd $wafdir + git checkout $waftag + popd +fi +if [ ! -d "$tooldir" ] ; then + git clone "$toolsurl" "$tooldir" +fi +if [ ! -f "$tooldir/smplpkgs.py" ] ; then + echo "Tooldir does not look correct: $tooldir" + exit 1 +fi + +#toolflags="compat15" +toolflags="" +for tool in $tools ; +do + maybe="$tooldir/${tool}.py" + if [ -f $maybe ] ; then + toolflags="$toolflags,$maybe" + continue + fi + maybe="$wafdir/waflib/Tools/${tool}.py" + if [ -f $maybe ] ; then + toolflags="$toolflags,$tool" + continue + fi + maybe="$wafdir/waflib/extras/${tool}.py" + if [ -f $maybe ] ; then + toolflags="$toolflags,$tool" + continue + fi + + echo "Failed to find tool $tool" + exit 1 +done + +pushd "$wafdir" > /dev/null +#python waf-light --python=python2 --tools=$toolflags +#python2 ./waf-light configure build --tools=$toolflags +## python2 is dead, get over it +python ./waf-light configure build --tools=$toolflags +popd > /dev/null +echo "built waf with tools: $toolflags" +# echo 'WARNING: forcing python2. See GitHub Issue #8' +# sed -i -e 's,/usr/bin/env python$,/usr/bin/env python2,' "$wafdir/waf" +mv "$wafdir/waf" $output + +echo "$output" diff --git a/waft/rootsys.py b/waft/rootsys.py new file mode 100644 index 000000000..d04a0c4a2 --- /dev/null +++ b/waft/rootsys.py @@ -0,0 +1,135 @@ +import os +import os.path as osp +import waflib +import waflib.Utils +from waflib.Configure import conf + +_tooldir = osp.dirname(osp.abspath(__file__)) + + +def options(opt): + opt = opt.add_option_group('ROOT Options') + opt.add_option('--with-root', default=None, + help="give ROOT installation location") + return + +@conf +def check_root(cfg, mandatory=False): + instdir = cfg.options.with_root + + if instdir and instdir.lower() in ['no','off','false']: + if mandatory: + raise RuntimeError("ROOT is mandatory but disabled via command line") + print ("optional ROOT dependency disabled by command line") + return + + cfg.env.CXXFLAGS += ['-fPIC'] + + path_list = list() + for topdir in [getattr(cfg.options, 'with_root', None), os.getenv('ROOTSYS', None)]: + if topdir: + path_list.append(osp.join(topdir, "bin")) + + kwargs = dict(path_list=path_list) + + cfg.find_program('root-config', var='ROOT-CONFIG', mandatory=mandatory, **kwargs) + if not 'ROOT-CONFIG' in cfg.env: + if mandatory: + raise RuntimeError("root-config not found but ROOT required") + print ("skipping non mandatory ROOT, use --with-root to force") + return + + cfg.check_cfg(path=cfg.env['ROOT-CONFIG'], uselib_store='ROOTSYS', + args = '--cflags --libs --ldflags', package='', mandatory=mandatory) + + + cfg.find_program('rootcling', var='ROOTCLING', path_list=path_list, mandatory=mandatory) + # cfg.find_program('rootcint', var='ROOTCINT', path_list=path_list, mandatory=mandatory) + # cfg.find_program('rlibmap', var='RLIBMAP', path_list=path_list, mandatory=False) + + cfg.check_cxx(header_name="Rtypes.h", use='ROOTSYS', + mandatory=mandatory) + + + return + +def configure(cfg): + cfg.check_root() + +@conf +def gen_rootcling_dict(bld, name, linkdef, headers = '', includes = '', use=''): + ''' + rootcling -f NAMEDict.cxx -rml libNAME.so -rmf libNAME.rootmap myHeader1.h myHeader2.h ... LinkDef.h + ''' + use = waflib.Utils.to_list(use) + ['ROOTSYS'] + includes = waflib.Utils.to_list(includes) + for u in use: + more = bld.env['INCLUDES_'+u] + #print 'USE(%s)=%s: %s' % (name, u, more) + includes += more + + # make into -I... + incs = list() + for inc in includes: + if inc.startswith('/'): + newinc = '-I%s' % inc + else: + newinc = '-I%s' % bld.path.find_dir(inc).abspath() + if not newinc in incs: + incs.append(newinc) + incs = ' '.join(incs) + #print 'INCS(%s): %s' % (name, str(incs)) + + dict_src = name + 'Dict.cxx' + # dict_lib = 'lib' + name + 'Dict.so' # what for Mac OS X? + # dict_map = 'lib' + name + 'Dict.rootmap' + # dict_pcm = name + 'Dict_rdict.pcm' + dict_lib = 'lib' + name + '.so' # what for Mac OS X? + dict_map = 'lib' + name + '.rootmap' + dict_pcm = name + 'Dict_rdict.pcm' + + if type(linkdef) == type(""): + linkdef = bld.path.find_resource(linkdef) + source_nodes = headers + [linkdef] + sources = ' '.join([x.abspath() for x in source_nodes]) + rule = '${ROOTCLING} -f ${TGT[0].abspath()} -rml %s -rmf ${TGT[1].abspath()} %s %s' % (dict_lib, incs, sources) + #print 'RULE:',rule + bld(source = source_nodes, + target = [dict_src, dict_map, dict_pcm], + rule=rule, use = use) + + # bld.shlib(source = dict_src, + # target = name+'Dict', + # includes = includes, + # use = use + [name]) + + bld.install_files('${PREFIX}/lib/', dict_map) + bld.install_files('${PREFIX}/lib/', dict_pcm) + + +@conf +def gen_rootcint_dict(bld, name, linkdef, headers = '', includes=''): + '''Generate a rootcint dictionary, compile it to a shared lib, + produce its rootmap file and install it all. + ''' + headers = waflib.Utils.to_list(headers) + incs = ['-I%s' % bld.path.find_dir(x).abspath() for x in waflib.Utils.to_list(includes)] + incs = ' '.join(incs) + + dict_src = name + 'Dict.cxx' + + bld(source = headers + [linkdef], + target = dict_src, + rule='${ROOTCINT} -f ${TGT} -c %s ${SRC}' % incs) + + bld.shlib(source = dict_src, + target = name+'Dict', + includes = includes, + use = 'ROOTSYS') + + rootmap = 'lib%sDict.rootmap'%name + bld(source = [linkdef], target=rootmap, + rule='${RLIBMAP} -o ${TGT} -l lib%sDict.so -d lib%s.so -c ${SRC}' % (name, name)) + + bld.install_files('${PREFIX}/lib/', rootmap) + diff --git a/waft/rpathify.py b/waft/rpathify.py new file mode 100644 index 000000000..17eaef3a6 --- /dev/null +++ b/waft/rpathify.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python +# encoding: utf-8 +# Thomas Nagy, 2011 (ita) +# stolen from local_rpath to be more global +# +# The warning against using this in production code goes double. + +import copy +from waflib.TaskGen import after_method, feature + +@after_method('propagate_uselib_vars') +#@feature('cprogram', 'cshlib', 'cxxprogram', 'cxxshlib', 'fcprogram', 'fcshlib') +@feature('cxxprogram', 'cxxshlib') +def add_rpath_stuff(self): + all = copy.copy(self.to_list(getattr(self, 'use', []))) + while all: + name = all.pop() + try: + tg = self.bld.get_tgen_by_name(name) + except: + self.env['RPATH'] += self.env['LIBPATH_'+name.upper()] + continue + if hasattr(tg, 'link_task'): + self.env.append_value('RPATH', tg.link_task.outputs[0].parent.abspath()) + all.extend(self.to_list(getattr(tg, 'use', []))) diff --git a/waft/setup-for-nix.sh b/waft/setup-for-nix.sh new file mode 100644 index 000000000..c1a572b7c --- /dev/null +++ b/waft/setup-for-nix.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +mydir=$(dirname $(dirname $(readlink -f $BASH_SOURCE))) + +source $HOME/.nix-profile/etc/profile.d/nix.sh + +for maybe in "$mydir/install-nix" +do + if [ -d "$maybe" ] ; then + echo "Located likely wire-cell toolkit installation: $maybe" + export LD_LIBRARY_PATH=$maybe/lib + PATH=$maybe/bin:$PATH + break + fi +done + +echo "Starting subshell with Nix Python packages enabled, exit when done." +nix-shell -p python27Packages.matplotlib \ + -p python27Packages.virtualenv \ + -p python27Packages.jsonnet \ + -p python27Packages.click +echo "Exiting Nix Python enabled subshell" + diff --git a/waft/smplpkgs.py b/waft/smplpkgs.py new file mode 100644 index 000000000..93ecbefc3 --- /dev/null +++ b/waft/smplpkgs.py @@ -0,0 +1,257 @@ +# -*- python -*- +'''This tool implements a source package following a few contentions. + +Your source package may build any combination of the following: + + - shared libraries + - headers exposing an API to libraries + - a ROOT dictionary for this API + - main programs + - test programs + +This tool will produce various methods on the build context. You can +avoid passing to them if you set APPNAME in your wscript file. + +''' + +import os.path as osp +from waflib.Utils import to_list +from waflib.Configure import conf +import waflib.Context +from waflib.Logs import debug, info, error, warn + +class SimpleGraph(object): + colors = dict(lib='black', app='blue', tst='gray') + def __init__(self): + self._nodes = dict() + self._edges = dict() + + def __str__(self): + lines = ['digraph deps {'] + for node,attrs in self._nodes.items(): + lines.append('\t"%s";' % node) + + for edge, attrs in self._edges.items(): + for cat in attrs: + # print (edge, cat, self.colors[cat]) + extra = "" + if cat == "tst": + extra=',constraint=false' + lines.append('\t"%s" -> "%s"[color="%s"%s];' % \ + (edge[0], edge[1], self.colors[cat], extra)) + lines.append('}') + return '\n'.join(lines) + + def register(self, pkg, **kwds): + # print ("register %s" % pkg) + self.add_node(pkg) + for cat, deps in kwds.items(): + kwds = {cat: True} + for other in deps: + self.add_edge((pkg, other), **kwds) + + def add_node(self, name, **kwds): + if name in self._nodes: + self._nodes[name].update(kwds) + else: + self._nodes[name] = kwds + def add_edge(self, edge, **kwds): + if edge in self._edges: + self._edges[edge].update(kwds) + else: + self._edges[edge] = kwds + + + +_tooldir = osp.dirname(osp.abspath(__file__)) + +def options(opt): + opt.load('compiler_cxx') + opt.load('waf_unit_test') + + +def configure(cfg): + cfg.load('compiler_cxx') + cfg.load('waf_unit_test') + cfg.load('rpathify') + + cfg.env.append_unique('CXXFLAGS',['-std=c++17']) + + cfg.find_program('python', var='PYTHON', mandatory=True) + cfg.find_program('bash', var='BASH', mandatory=True) + pass + +def build(bld): + from waflib.Tools import waf_unit_test + bld.add_post_fun(waf_unit_test.summary) + #print ("smplpkgs.build()") + +@conf +def smplpkg(bld, name, use='', app_use='', test_use=''): + + if not hasattr(bld, 'smplpkg_graph'): + #print ("Make SimpleGraph") + bld.smplpkg_graph = SimpleGraph() + bld.smplpkg_graph.register( + name, + lib=set(to_list(use)), + app=set(to_list(app_use)), + tst=set(to_list(test_use))) + + use = list(set(to_list(use))) + use.sort() + app_use = list(set(use + to_list(app_use))) + app_use.sort() + test_use = list(set(use + to_list(test_use))) + test_use.sort() + + includes = [bld.out_dir] + headers = [] + source = [] + + incdir = bld.path.find_dir('inc') + srcdir = bld.path.find_dir('src') + dictdir = bld.path.find_dir('dict') + + testsrc = bld.path.ant_glob('test/test_*.cxx') + testsrc_kokkos = bld.path.ant_glob('test/test_*.kokkos') + test_scripts = bld.path.ant_glob('test/test_*.sh') + bld.path.ant_glob('test/test_*.py') + test_jsonnets = bld.path.ant_glob('test/test*.jsonnet') + + checksrc = bld.path.ant_glob('test/check_*.cxx') + + appsdir = bld.path.find_dir('apps') + + if incdir: + headers += incdir.ant_glob(name + '/*.h') + includes += ['inc'] + bld.env['INCLUDES_'+name] = [incdir.abspath()] + + if headers: + bld.install_files('${PREFIX}/include/%s' % name, headers) + + if srcdir: + source += srcdir.ant_glob('*.cxx') + source += srcdir.ant_glob('*.cu') # cuda + source += srcdir.ant_glob('*.kokkos') # kokkos + + # fixme: I should move this out of here. + # root dictionary + if dictdir: + if not headers: + error('No header files for ROOT dictionary "%s"' % name) + #print 'Building ROOT dictionary: %s using %s' % (name,use) + if 'ROOTSYS' in use: + linkdef = dictdir.find_resource('LinkDef.h') + bld.gen_rootcling_dict(name, linkdef, + headers = headers, + includes = includes, + use = use) + source.append(bld.path.find_or_declare(name+'Dict.cxx')) + else: + warn('No ROOT dictionary will be generated for "%s" unless "ROOTSYS" added to "use"' % name) + + if hasattr(bld.env, "PROTOC"): + pbs = bld.path.ant_glob('src/**/*.proto') + # if ("zpb" in name.lower()): + # print ("protobufs: %s" % (pbs,)) + source += pbs + + def get_rpath(uselst, local=True): + ret = set([bld.env["PREFIX"]+"/lib"]) + for one in uselst: + libpath = bld.env["LIBPATH_"+one] + for l in libpath: + ret.add(l) + if local: + if one.startswith("WireCell"): + sd = one[8:].lower() + blddir = bld.path.find_or_declare(bld.out_dir) + pkgdir = blddir.find_or_declare(sd).abspath() + #print pkgdir + ret.add(pkgdir) + ret = list(ret) + ret.sort() + return ret + + # the library + if srcdir: + if ("zpb" in name.lower()): + print ("Building library: %s, using %s, source: %s"%(name, use, source)) + ei = '' + if incdir: + ei = 'inc' + bld(features = 'cxx cxxshlib', + name = name, + source = source, + target = name, + #rpath = get_rpath(use), + includes = includes, # 'inc', + export_includes = ei, + use = use) + + if appsdir: + for app in appsdir.ant_glob('*.cxx'): + #print 'Building %s app: %s using %s' % (name, app, app_use) + bld.program(source = [app], + target = app.name.replace('.cxx',''), + includes = includes, # 'inc', + #rpath = get_rpath(app_use + [name], local=False), + use = app_use + [name]) + + + if (testsrc or testsrc_kokkos or test_scripts) and not bld.options.no_tests: + for test_main in testsrc_kokkos: + #print 'Building %s test: %s' % (name, test_main) + rpath = get_rpath(test_use + [name]) + #print rpath + bld.program(features = 'cxx cxxprogram test', + source = [test_main], + ut_cwd = bld.path, + target = test_main.name.replace('.kokkos',''), + install_path = None, + #rpath = rpath, + includes = ['inc','test','tests'], + use = test_use + [name]) + for test_main in testsrc: + #print 'Building %s test: %s' % (name, test_main) + rpath = get_rpath(test_use + [name]) + #print rpath + bld.program(features = 'test', + source = [test_main], + ut_cwd = bld.path, + target = test_main.name.replace('.cxx',''), + install_path = None, + #rpath = rpath, + includes = ['inc','test','tests'], + use = test_use + [name]) + for test_script in test_scripts: + interp = "${BASH}" + if test_script.abspath().endswith(".py"): + interp = "${PYTHON}" + #print 'Building %s test %s script: %s using %s' % (name, interp, test_script, test_use) + bld(features="test_scripts", + ut_cwd = bld.path, + test_scripts_source = test_script, + test_scripts_template = "pwd && " + interp + " ${SCRIPT}") + + if test_jsonnets and not bld.options.no_tests: + # print ("testing %d jsonnets in %s" % (len(test_jsonnets), bld.path )) + for test_jsonnet in test_jsonnets: + bld(features="test_scripts", + ut_cwd = bld.path, + test_scripts_source = test_jsonnet, + test_scripts_template = "pwd && ../build/apps/wcsonnet ${SCRIPT}") + + if checksrc and not bld.options.no_tests: + for check_main in checksrc: + #print 'Building %s check: %s' % (name, check_main) + rpath = get_rpath(test_use + [name]) + #print rpath + bld.program(source = [check_main], + target = check_main.name.replace('.cxx',''), + install_path = None, + #rpath = rpath, + includes = ['inc','test','tests'], + use = test_use + [name]) + diff --git a/waft/sourceme-ups.sh b/waft/sourceme-ups.sh new file mode 100644 index 000000000..c97f27b96 --- /dev/null +++ b/waft/sourceme-ups.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# source sourceme-ups.sh [version [qual]] + +# This sets up a bash shell to use UPS products to supply WCT build +# and runtime environment. For building see wct-configure-for-ups.sh +# in this same directory. Although Wire Cell Toolkit does not depend +# on LArSoft, the environment is configured by piggy-backing on a +# "larsoft" UPS product of the given version and qualifiers. + +version=${1:-v06_76_00} +quals=${2:-e15:prof} + +source /cvmfs/larsoft.opensciencegrid.org/products/setup + +setup larsoft $version -q $quals + +export CXX=clang++ +export CC=clang + +# installed system git on Fermilab systems is too old so also set it up. +setup git + + +srcdir=$(dirname $(dirname $(readlink -f $BASH_SOURCE))) + +export WIRECELL_DATA=$srcdir/cfg +echo "You will need to add your wire-cell-data directory to \$WIRECELL_DATA (currently $WIRECELL_DATA)" diff --git a/waft/test-release.sh b/waft/test-release.sh new file mode 100755 index 000000000..e187abe69 --- /dev/null +++ b/waft/test-release.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +cat < +# +# 2) Configure the source by telling it where to find externals and where to install (must not exist) +# +# $ ./test-release.sh configure-source +# +# 3) Build, install and run tests +# +# $ ./test-release.sh install +#### + + +function goto +{ + local dir=${1?no directory given} ; shift + if [ ! -d $dir ] ; then + echo "No such directory: $dir" + exit 1 + fi + pushd $dir >/dev/null +} +function goback +{ + popd >/dev/null +} + +function get-source +{ + local source=${1?no sourcedir given} ; shift + local tag=${1?no tag given} ; shift + + if [ -d $source ] ; then + echo "Source directory already exists: $source" + exit 1 + fi + + git clone https://github.com/WireCell/wire-cell-build.git $source + goto $source + git checkout -b $tag $tag + git submodule init + git submodule update + git submodule foreach git checkout -b $tag $tag + goback +} + + + +function configure-source +{ + local source=$(readlink -f ${1?must provide source directory}) ; shift + local externals=$(readlink -f ${1?must provide externals directory}) ; shift + local install=$(readlink -f ${1?must provide install directory}) ; shift + + + if [ ! -d "$externals" ] ; then + echo "No externals directory: $externals" + exit 1 + fi + if [ -d "$install" ] ; then + echo "Install directory already exits: $install" + exit 1 + fi + mkdir -p $install + + goto $source + + ./wcb configure --prefix=$install \ + --boost-includes=$externals/include \ + --boost-libs=$externals/lib \ + --boost-mt \ + --with-eigen-include=$externals/include/eigen3 \ + --with-jsoncpp=$externals \ + --with-jsonnet=$externals \ + --with-zlib=$externals \ + --with-tbb=no \ + --with-fftw=$externals \ + --with-root=$externals + + + cat <tester.sh +#!/bin/bash +env LD_LIBRARY_PATH=$externals/lib:$install/lib "\$@" +EOF + chmod +x tester.sh + + goback +} + +function install +{ + local source=$(readlink -f ${1?must provide source directory}) ; shift + goto $source + + ./wcb --notest || exit 1 + ./wcb --notest install || exit 1 + ./wcb --alltests --testcmd="$source/tester.sh %s" || exit 1 + + goback +} + + +"$@" diff --git a/waft/wcb.py b/waft/wcb.py new file mode 100644 index 000000000..1a28fc2df --- /dev/null +++ b/waft/wcb.py @@ -0,0 +1,177 @@ +# Aggregate all the waftools to make the main wscript a bit shorter. +# Note, this is specific to WC building + +from . import generic +import os.path as osp +from waflib.Utils import to_list + +mydir = osp.dirname(__file__) + +## These are packages descriptions which fit the generic functions. +## They will be checked in order so put any dependencies first. +package_descriptions = [ + + # spdlog is "header only" but use library version for faster recompilation + # wire-cell-util and ZIO both use this + # Need to build with -DCMAKE_POSITION_INDEPENDENT_CODE=ON + ('spdlog', dict(incs=['spdlog/spdlog.h'], libs=['spdlog'])), + + ('ZLib', dict(incs=['zlib.h'], libs=['z'])), + ('FFTW', dict(incs=['fftw3.h'], libs=['fftw3f'], pcname='fftw3f')), + ('FFTWThreads', dict(libs=['fftw3f_threads'], pcname='fftw3f', mandatory=False)), + ('JsonCpp', dict(incs=["json/json.h"], libs=['jsoncpp'])), + + ('Eigen', dict(incs=["Eigen/Dense"], pcname='eigen3')), + + # for faster parsing, consider: + # ./wcb configure --with-jsonnet-libs=gojsonnet + ('Jsonnet', dict(incs=["libjsonnet.h"], libs=['jsonnet'])), + ('TBB', dict(incs=["tbb/parallel_for.h"], libs=['tbb'], mandatory=False)), + ('HDF5', dict(incs=["hdf5.h"], libs=['hdf5'], mandatory=False)), + ('H5CPP', dict(incs=["h5cpp/all"], mandatory=False, extuses=('HDF5',))), + + ('ZMQ', dict(incs=["zmq.h"], libs=['zmq'], pcname='libzmq', mandatory=False)), + ('CZMQ', dict(incs=["czmq.h"], libs=['czmq'], pcname='libczmq', mandatory=False)), + ('ZYRE', dict(incs=["zyre.h"], libs=['zyre'], pcname='libzyre', mandatory=False)), + ('ZIO', dict(incs=["zio/node.hpp"], libs=['zio'], pcname='libzio', mandatory=False, + extuses=("ZYRE","CZMQ","ZMQ"))), + + # Note, this list may be modified (appended) in wscript files. + # The list here represents the minimum wire-cell-toolkit requires. +] + + +def options(opt): + + # from here + opt.load('boost') + opt.load('smplpkgs') + opt.load('rootsys') + opt.load('libtorch') + opt.load('cuda') + opt.load('kokkos') + #opt.load('protobuf') + + for name,desc in package_descriptions: + generic._options(opt, name, + desc.get("incs", None), + desc.get("libs", None)) + + opt.add_option('--build-debug', default='-O2 -ggdb3', + help="Build with debug symbols") + +def find_submodules(ctx): + sms = list() + for wb in ctx.path.ant_glob("**/wscript_build"): + sms.append(wb.parent.name) + sms.sort() + return sms + + +def configure(cfg): + print ('Compile options: %s' % cfg.options.build_debug) + + cfg.load('boost') + cfg.load('smplpkgs') + + for name, args in package_descriptions: + #print ("Configure: %s %s" % (name, args)) + generic._configure(cfg, name, **args) + #print ("configured %s" % name) + + if getattr(cfg.options, "with_libtorch", False) is False: + print ("sans libtorch") + else: + cfg.load('libtorch') + + if getattr(cfg.options, "with_cuda", False) is False: + print ("sans CUDA") + else: + cfg.load('cuda') + + if getattr(cfg.options, "with_kokkos", False) is False: + print ("sans KOKKOS") + else: + cfg.load('kokkos') + + if getattr(cfg.options, "with_root", False) is False: + print ("sans ROOT") + else: + cfg.load('rootsys') + + ### not yet used + # if cfg.options.with_protobuf is False: + # print ("sans protobuf") + # else: + # cfg.load('protobuf') + + + def haveit(one): + one=one.upper() + if 'LIB_'+one in cfg.env: + cfg.env['HAVE_'+one] = 1 + print('HAVE %s libs'%one) + else: + print('NO %s libs'%one) + + # Check for stuff not found in the wcb-generic way + + cfg.check_boost(lib='system filesystem graph thread program_options iostreams regex') + haveit('boost') + + cfg.check(header_name="dlfcn.h", uselib_store='DYNAMO', + lib=['dl'], mandatory=True) + haveit('dynamo') + + cfg.check(features='cxx cxxprogram', lib=['pthread'], uselib_store='PTHREAD') + haveit('pthread') + + + cfg.env.CXXFLAGS += to_list(cfg.options.build_debug) + cfg.env.CXXFLAGS += ['-DEIGEN_FFTW_DEFAULT=1'] + + cfg.env.LIB += ['z'] + + submodules = find_submodules(cfg) + + # submodules = 'util iface gen sigproc img pgraph apps sio dfp tbb ress cfg root'.split() + # submodules.sort() + # submodules = [sm for sm in submodules if osp.isdir(sm)] + + # Remove WCT packages if they an optional dependency wasn't found + for pkg,ext in [ + ("root","HAVE_ROOTSYS"), + ("tbb","HAVE_TBB"), + ("tbb","LIB_FFTWTHREADS"), + ("cuda","HAVE_CUDA"), + ("hio", "INCLUDES_H5CPP"), + ("pytorch", "LIB_LIBTORCH"), + ("zio", "LIB_ZIO LIB_ZYRE LIB_CZMQ LIB_ZMQ") + ]: + exts = to_list(ext) + for have in exts: + if have in cfg.env or have in cfg.env.define_key: + continue + if pkg in submodules: + print ('Removing package "%s" due to lack of external dependency "%s"'%(pkg,have)) + submodules.remove(pkg) + + cfg.env.SUBDIRS = submodules + print ('Configured for submodules: %s' % (', '.join(submodules), )) + cfg.write_config_header('config.h') + #print(cfg.env) + +def build(bld): + bld.load('smplpkgs') + + subdirs = bld.env.SUBDIRS + print ('Building: %s' % (', '.join(subdirs), )) + bld.recurse(subdirs) + + if hasattr(bld, "smplpkg_graph"): + # fixme: this writes directly. Should make it a task, including + # running graphviz to produce PNG/PDF + print ("writing wct-depos.dot") + bld.path.make_node("wct-deps.dot").write(str(bld.smplpkg_graph)) + + diff --git a/waft/wct-configure-for-nix.sh b/waft/wct-configure-for-nix.sh new file mode 100755 index 000000000..aed705840 --- /dev/null +++ b/waft/wct-configure-for-nix.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +usage () { + cat <] + + - :: optional location for installing WCT. It + defaults to installing into the view. + +Note: this assumes a Nix profile exists. New packages will be added. + +EOF + exit 1 +} + +topdir=$(dirname $(dirname $(readlink -f $BASH_SOURCE))) +inst="${1:-$topdir/install-nix}" +echo "Will 'wcb install' to $inst" + +if [ -z "$NIX_PATH" ] ; then + echo "Nix not yet configured." + nixsh="$HOME/.nix-profile/etc/profile.d/nix.sh" + if [ -f "$nixsh"] ; then + echo "source $nixsh" + else + echo "... and no nix profile even." + fi + exit 1 +fi + +view="$(dirname $(dirname $(which root-config)))" +if [ -z "$view" -o ! -d "$view" ] ; then + echo "Fail to find nix profile directory: $view" + exit 1 +fi + +assure_packages () { + echo "Assuring packages. This may take some time" + # To get "dev" outputs from multi-ouput packages requires some hoop jumping + for devpkg in fftwFloat boost zlib + do + echo "Assuring dev package: $pkg" + echo + nix-env -i -E '_: with import {}; let newmeta = ( '$devpkg'.meta // { outputsToInstall = ["out" "dev"]; } ); in '$devpkg' // { meta = newmeta; }' || exit 1 + echo + done + for pkg in gcc python jsonnet jsoncpp eigen root tbb + do + echo "Assuring package: $pkg" + echo + nix-env -iA nixpkgs.$pkg + echo + done +} +# assure_packages + + +# fixme: would like to test clang too... +export CC=`which gcc` +export CXX=`which g++` + +# needed to shut up eigen 3.2.10 +export CXXFLAGS='-Wno-misleading-indentation -Wno-int-in-bool-context -Wvla' + +"$topdir/wcb" \ + configure \ + --with-jsoncpp="$view" \ + --with-jsonnet="$view" \ + --with-eigen-include="$view/include/eigen3" \ + --with-fftw="$view" \ + --with-zlib="$view" \ + --with-root="$view" \ + --boost-includes="$view/include" \ + --boost-libs="$view/lib" \ + --boost-mt \ + --with-tbb=$view \ + --prefix="$inst" + + +# + + diff --git a/waft/wct-configure-for-ups.sh b/waft/wct-configure-for-ups.sh new file mode 100755 index 000000000..5a61a96c5 --- /dev/null +++ b/waft/wct-configure-for-ups.sh @@ -0,0 +1,69 @@ +#!/bin/bash + + +usage () { + cat < \ + -f \$(ups flavor) \ + -q e14:prof \ + -r wirecell/ \ + -z /path/to/install/products \ + -U ups \ + -m wirecell.table + +You'll have to provide the wirecell.table yourself, likely by copying +it from an existing "wirecell" UPS product. + +Then, the calling environment can be munged like: + + $ setup wirecell -q e14:prof + +UPS is such a great and simple system! /s + +EOF + exit +} + +install_dir="$1" ; shift +if [ "$install_dir" = "ups" ] ; then + install_dir="$WIRECELL_FQ_DIR" +fi + +# force to pick up GCC from PATH +wct_cc=${CC:-gcc} +wct_cxx=${CXX:-g++} +wct_fort=${FORT:-gfortran} +env CC=$wct_cc CXX=$wct_cxx FC=wct_fort \ + ./wcb configure \ + --with-tbb=no \ + --with-jsoncpp="$JSONCPP_FQ_DIR" \ + --with-jsonnet="$JSONNET_FQ_DIR" \ + --with-eigen-include="$EIGEN_DIR/include/eigen3" \ + --with-root="$ROOT_FQ_DIR" \ + --with-fftw="$FFTW_FQ_DIR" \ + --with-fftw-include="$FFTW_INC" \ + --with-fftw-lib="$FFTW_LIBRARY" \ + --boost-includes="$BOOST_INC" \ + --boost-libs="$BOOST_LIB" \ + --boost-mt \ + --prefix="$install_dir" + diff --git a/waft/wct-configure-for-view.sh b/waft/wct-configure-for-view.sh new file mode 100755 index 000000000..9790596e2 --- /dev/null +++ b/waft/wct-configure-for-view.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +usage () { + cat < [] + + - :: the file system path to the top of the view directory. + + - :: optional location for installing WCT. It + defaults to installing into the view. + +Note: A likely way to create a "view" directory is with Spack: + + spack view add -i /opt/spack/views/wct-dev wirecell-toolkit + +EOF + exit 1 +} + +view="$1" +if [ -z "$view" ] ; then + usage +fi +inst="${2:-$view}" +echo "Will 'wcb install' to $inst" + +topdir=$(dirname $(dirname $(readlink -f $BASH_SOURCE))) + +"$topdir/wcb" \ + configure \ + --with-jsoncpp="$view" \ + --with-jsonnet="$view" \ + --with-tbb="$view" \ + --with-eigen-include="$view/include/eigen3" \ + --with-root="$view" \ + --with-fftw="$view" \ + --boost-includes="$view/include" \ + --boost-libs="$view/lib" \ + --boost-mt \ + --with-tbb=false \ + --prefix="$inst" + + +#--with-fftw-include="$view/include" \ +#--with-fftw-lib="$view/lib" \ + +cat < Date: Mon, 6 Mar 2023 16:37:33 -0500 Subject: [PATCH 02/32] Add support for 'variant' tests, examples, docs --- apps/test/test_apps.bats | 13 + test/README.org | 150 ++++++++ test/wct-bats.sh | 22 ++ util/test/check_numpy_depos.cxx | 55 +-- util/test/test_bats.bats | 12 + util/test/test_check_numpy_depos.bats | 12 + util/test/test_graph_visitor.cxx | 16 +- util/wscript_build | 54 ++- waft/README.org | 179 +++++---- waft/bjam.py | 128 +++++++ waft/boost.py | 526 ++++++++++++++++++++++++++ waft/cuda.py | 2 +- waft/doxygen.py | 236 ++++++++++++ waft/kokkos.py | 2 +- waft/libtorch.py | 2 +- waft/refresh-wcb | 25 +- waft/smplpkgs.py | 415 ++++++++++++++------ waft/wcb.py | 41 +- wscript | 8 +- 19 files changed, 1640 insertions(+), 258 deletions(-) create mode 100644 apps/test/test_apps.bats create mode 100644 test/README.org create mode 100644 test/wct-bats.sh create mode 100644 util/test/test_bats.bats create mode 100644 util/test/test_check_numpy_depos.bats create mode 100644 waft/bjam.py create mode 100644 waft/boost.py create mode 100644 waft/doxygen.py diff --git a/apps/test/test_apps.bats b/apps/test/test_apps.bats new file mode 100644 index 000000000..f84b0c603 --- /dev/null +++ b/apps/test/test_apps.bats @@ -0,0 +1,13 @@ +#!/usr/bin/env bats + +load "../../test/wct-bats.sh" + +@test "test wct bats in apps" { + usepkg util apps + t=$(top) + echo "TOP $t" + [ -f "$t/build/apps/wire-cell" ] + [ -n "$util_src" ] + [ -n "$(wcsonnet)" ] +} + diff --git a/test/README.org b/test/README.org new file mode 100644 index 000000000..a5b45206f --- /dev/null +++ b/test/README.org @@ -0,0 +1,150 @@ +#+name: Wire-Cell Toolkit Testing + +The Wire-Cell Toolkit (WCT) provides a large number of tests of different types. This document describes how to exercise and develop them. The ~wire-cell-toolkit~ provides assistance to running and developing tests in this sub-package (~wire-cell-toolkit/tests/~) and as part of the build system (~wire-cell-toolkit/waft/~). + +* Types of tests + +There are two cateogries of tests in WCT: + +- atomic :: self-contained unit tests +- variant :: potentially graphs of related tests sharing files + +* Executing tests + +All tests will be built and executed by default when WCT is built. + +#+begin_example +waf [ ... ] # build everything and run all tests +#+end_example + +This can greatly add to the build time. Building and running test scan be *suppressed* with the ~--notests~ flag: + +#+begin_example +waf --notests [ ... ] # no tests +#+end_example + +To build and run only the atomic unit tests the variant tests can be suppressed with the ~--nochecks~ flag: + +#+begin_example +waf --nochecks [ ... ] # just atomic unit tests +#+end_example + +* Built-in test system + +WCT uses Waf's unit test framework. + +** Check programs + +Source files found as: + +#+begin_example +/test/check_*. +#+end_example + +may be compiled, but will not be run automatically. They are available for use in atomic or variant tests. + +** Atomic tests + +The build will compile, if needed, and automatically run *atomic tests* with source files found as: + +#+begin_example +/test/test_*. +#+end_example + +Extensions of ~cxx~ or ~kokkos~ wil be compiled prior to running. Interpreted scripts with extensions ~py, sh, bats, jsonnet~ will be run with their associated interpreter. The check programs (above) are available for calling from these atomic tests as are various others. + + +** Variant tests + +Variant tests are registered as Waf rules in the file found at: + +#+begin_example +/wscript_build +#+end_example + +That code should utilize the return from a call to ~bld.smplpkg()~ as a context manager though which to register Waf rules. Variant tests should not be registered directly via ~bld()~ as this will circumvent the test suppression. + +#+begin_src python + with bld.smplpkg('MyPackage', ...) as p: + p.rule("${WCSONNET} > ${TGT} 2>&1", target="wcsonnet-help.log") +#+end_src + +The ~${WCSONNET}~ variable will point to the ~wcsonnet~ program in the build output. + +** BATS tests + +One interpreted test format is that of the Bash automated testing system ([[https://bats-core.readthedocs.io/][BATS]]). This unit test framework allows one to quickly write simple bash functions to perform tests. One may write atomic or variant tests as BATS files. A reason to choose BATS over others is if the test is best expressed as shell code and/or to segregate a large set of related tests from bloating the Waf DAG. + +WCT provides some support for BATS tests in the file ~test/wct-bats.sh~ which can be used in a BATS test as in the following example: + +#+begin_example +#+/usr/bin/env bats + +# in /test/test_mytest.bats + +load ../test/wct-bats.sh + +@test "some test" { + usepkg util + # use $util_src, call test/check's made from util +} +#+end_example + +A BATS test can be run manually from anywhere: + +#+begin_example +cd .. +bats wire-cell-toolkit/util/test_bats.bats +#+end_example + +However, Waf will run the test from the ~build/~ directory. To fully replicate that: + +#+begin_example +cd wire-cell-toolkit/build/ +bats ../util/test/test_bats.bats +#+end_example + +When run by default, all ~@test~ functions in a BATS file will be run and they are not idempotent (but Waf tests are). Especially while developing tests i tis useful to narrow which are run by applying a filter on the strings given to the ~@test~ command. + +#+begin_example +bats -f "test wct bats" util/test/test_bats.bats. +#+end_example + +** Other unit test frameworks + +In the future, support for ~pytest~ may be added. + +* Writing atomic tests in C++ + +An atomic test, ~/test/test_*.cxx~ + +- must not *require* any arguments including input/output file names +- may create files as a side effect +- should create those based on the name of the executable from ~argv[0]~. +- may load files via the environment (eg "wires files") +- should ~assert()~ and ~return 1~ (or any non-zero) for anything that goes wrong +- such assertions should be numerous +- may use WCT ~Testing.h~ helper + +Here is a quick sampling to get started. + +#+begin_src c++ + #include "WireCellUtil/Testing.h" + #include + + int main(int argc, char* argv[]) { + std::string arg0 = argv[0]; + std::ofstream ofstr(arg0 + "_someout.ext"); + ofstr << "goodbye world!\n"; + if (do_something() != 42) return 1; + assert(do_something_else()); + Assert(yet_more()); + AssertMsg(yet_more(), "with an error message"); + return 0; + } +#+end_src + +* Writing a sub-DAG of variant tests + +Though one has to write Waf code to register them, variant tests may be extended over a subgraph of the Waf DAG by connecting them with files. See ~wire-cell-toolkit/util/wscript_build~ for various examples. + diff --git a/test/wct-bats.sh b/test/wct-bats.sh new file mode 100644 index 000000000..21dbe407e --- /dev/null +++ b/test/wct-bats.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# BATS helper functions. +# To use inside a /test/test_XXX.bats do: +# load ../../test/wct-bats.sh + +# Return the top level source directory +top () { + dirname $(dirname $(realpath $BASH_SOURCE)) +} + +# Add package (util, gen, etc) test apps into PATH, etc. define +# _src to be that sub-package source directory. +usepkg () { + for pkg in $@; do + local pkg=$1 ; shift + printf -v "${pkg}_src" "%s/%s" "$t" "$pkg" + local t=$(top) + PATH="$t/build/$pkg:$PATH" + done + echo $PATH +} diff --git a/util/test/check_numpy_depos.cxx b/util/test/check_numpy_depos.cxx index 86f54a344..8014ca8cc 100644 --- a/util/test/check_numpy_depos.cxx +++ b/util/test/check_numpy_depos.cxx @@ -1,50 +1,53 @@ - -#include "WireCellUtil/NumpyHelper.h" +#include "WireCellUtil/Stream.h" #include #include +using namespace WireCell::Stream; + using FArray = Eigen::Array; using IArray = Eigen::Array; -using WireCell::Numpy::load2d; int main(int argc, char** argv) { if (argc < 3) { - std::cerr << "usage: check_numpy_depos depo-file.npz [ []]\n"; + std::cerr << "usage: check_numpy_depos depo-file.npz\n"; } const std::string fname = argv[1]; - const std::string nname = argv[2]; - size_t row_beg=0, nrows = 10; - if (argc > 3) { - row_beg = atoi(argv[3]); - } - if (argc > 4) { - nrows = atoi(argv[4]); - } + + boost::iostreams::filtering_istream si; + input_filters(si, fname); + FArray data; + std::string dname=""; + read(si, dname, data); + assert(!dname.empty()); + std::cerr << dname << " shape: (" << data.rows() << " " << data.cols() << ")\n"; + IArray info; - load2d(data, "depo_data_" + nname, fname); - load2d(info, "depo_info_" + nname, fname); + std::string iname=""; + read(si, iname, info); + assert(!iname.empty()); + std::cerr << iname << " shape: (" << info.rows() << " " << info.cols() << ")\n"; - assert (data.cols() == 7); - assert (info.cols() == 4); + assert (data.rows() == 7); + assert (info.rows() == 4); - const size_t ndepos = data.rows(); - assert(ndepos == (size_t)info.rows()); - - const size_t row_end = std::min(row_beg + nrows, ndepos); + const size_t ndepos = data.cols(); + assert(ndepos == (size_t)info.cols()); - for (size_t irow=row_beg; irow&1 | grep 'Command line interface to the Wire-Cell Toolkit' > ${TGT}", target="wire-cell-help.log") + + ## OTOH, wcsonnet is less strict + p.rule("${WCSONNET} > ${TGT} 2>&1", target="wcsonnet-help.log") + + # Dumb example of getting an external file and comparing it to our local copy. + p.get_file('https://raw.githubusercontent.com/WireCell/wire-cell-toolkit/master/cfg/wirecell.jsonnet', + 'wirecell-copy.jsonnet') + p.script('wirecell-copy.jsonnet') + wca = p.nodify_resource("../cfg/wirecell.jsonnet") + wcb = p.nodify_declare("wirecell-copy.jsonnet") + p.diff(wca, wcb) + wca2 = p.nodify_declare("wirecell-ours.json"); + wcb2 = p.nodify_declare("wirecell-copy.json"); + p.rule('${JSONNET} ${SRC} > ${TGT}', source=[wca], target=[wca2]); + p.rule('${JSONNET} ${SRC} > ${TGT}', source=[wcb], target=[wcb2]); + p.diff(wca2, wcb2) + + # An example of a variant test using a "check" program built here. + # We must make the program itself a "source". The rule provides a + # template where ${SRC} and ${TGT} are expanded to a list of + # strings given by source and target, respectively. + for nsem in [0, 1, 2]: + p.rule("${SRC} %d > ${TGT} 2>&1"%nsem, + source="check_semaphore", target="check_semaphore_%d.log"%nsem) + + # Example of a variant test by calling an atomic test but with + # optional CLI args. This relies on the wires file variable being + # defined elsewhere (in wcb.py). It is NOT a source as the test + # program finds it out-of-band of waf. As above we must give the + # executable as a "source" so waf finds it. + p.rule("${SRC} ${WIRE_CELL_WIRES_PDSP} > ${TGT} 2>&1", + source="test_wireschema", target="test_wireschema_pdps.log") + + # Example of a variant that needs a file in the source directory. + p.rule("${SRC} > ${TGT} 2>&1", + source = ["check_numpy_depos", bld.srcnode.find_resource("test/data/muon-depos.npz")], + target = ["check_numpy_depos_muon-depos.log"]) diff --git a/waft/README.org b/waft/README.org index cc5e27dad..34b917dd9 100644 --- a/waft/README.org +++ b/waft/README.org @@ -1,103 +1,76 @@ #+title: Wire-Cell Builder Waf Tools -This repository holds some [[https://waf.io][Waf]] tools to help build the [[https://wirecell.bnl.gov][Wire-Cell -Toolkit]] (or other) software. The tools are bundled into a single -program called ~wcb~, the Wire-Cell Builder. +This repository holds some [[https://waf.io][Waf]] tools to help build the [[https://wirecell.bnl.gov][Wire-Cell Toolkit]] (or other) software. -* Packing +Historically, WCT provided a command ~wcb~ which was ~waf~ + some extra tools from Waf plus the WCT specific build tools in the ~wirecell/waf-tools~ repository. That repository has been copied to here, the `wire-cell-toolkit/waft/` sub-package. The ~wcb~ command may still be built from the contents here and used to build WCT itself or any Wire-Cell "user package". In addition, the plain ~waf~ command may be used to build WCT. -The ~wcb~ command is a packed version of waf with extra tools. A script is provided to automate rebuilding ~wcb~: +The rest of this document describes how to remake the ~wcb~ command, how to use it and describes some of the tools customized for WCT. -#+begin_example - $ ./refresh-wcb -o /path/to/your/wire-cell-toolkit/wcb -#+end_example +* Packing ~wcb~ -When WCT is updated it's a good idea to tag ~waf-tools~. Example session: +The ~wcb~ command is a packed version of ~waf~ with extra tools. A script is provided to automate rebuilding ~wcb~: #+begin_example - $ cd /path/to/your/waf-tools - $ ./refresh-wcb -o /path/to/your/wire-cell-toolkit/wcb - $ cd /path/to/your/wire-cell-toolkit - (...test...) - $ git commit -am "Refresh to wcb X.Y.Z" && git push - $ cd /path/to/your/waf-tools - $ git tag -am "...useful message..." X.Y.Z - $ git push --tags + $ ./waft/refresh-wcb -o wcb #+end_example +Be sure to do this *before* making a release of WCT if ~waft/~ has changed. -Th refresh script essentially enacts this recipe: - -#+BEGIN_EXAMPLE - $ git clone https://github.com/WireCell/waf-tools.git - $ WTEXTRA=$(echo $(pwd)/waf-tools/*.py | tr ' ' ,) - - $ git clone https://gitlab.com/ita1024/waf.git - $ cd waf/ - $ ./waf-light --tools=doxygen,boost,bjam,$WTEXTRA - ... - adding /home/bv/dev/waf-tools/smplpkgs.py as waflib/extras/smplpkgs.py - adding /home/bv/dev/waf-tools/wcb.py as waflib/extras/wcb.py - ... - $ cp waf /path/to/your/wire-cell-toolkit/wcb -#+END_EXAMPLE +* Using ~wcb~ -* Using the ~wcb~ tool +for these instructions, when building WCT itself, you may substitute ~waf~ with ~./wcb~. -On well provisioned systems, ~wcb~ builds the software automatically: +On well-provisioned systems, ~wcb~ builds the software automatically: #+begin_example - $ ./wcb configure --prefix=/path/to/install - $ ./wcb - $ ./wcb install + (1)$ ./wcb configure --prefix=/path/to/install + (2)$ ./wcb + (3)$ ./wcb install + (4)$ ./wcb test + (5)$ ./wcb check #+end_example -In some environments, ~wcb~ may need help to find dependencies. Hints -can be given with ~--with-*~ type flags. To see available flags use the -online help: +1. A one-time step so that dependencies may be located. +2. Build the project (output goes to ~./build/~). +3. Install elements of the build. +4. Build and execute "unit tests". +5. Build and execute integration/validation "checks". + +In some environments, ~wcb~ may need help to find dependencies. Hints can be given with ~--with-*~ type flags. To see available flags use the online help: #+BEGIN_EXAMPLE $ ./wcb --help #+END_EXAMPLE -Packages can be included, excluded and located with the various -~--with-NAME*~ flags. The rules work as follows: +Packages can be included, excluded and located with the various ~--with-NAME*~ flags. The rules work as follows: 1) If package is optional: - omitting a ~--with-NAME*~ option will omit use the package - - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will omit - use of the package. + - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will omit use of the package. 2) If package is mandatory: - - omitting all ~--with-NAME*~ options will use ~pkg-config~ to find the - package. + - omitting all ~--with-NAME*~ options will use ~pkg-config~ to find the package. - - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will - assert. + - explicitly using ~--with-NAME=false~ (or "~no~" or "~off~") will assert. 3) In either case: - - explicitly using ~--with-NAME=true~ (or "~yes~" or "~on~") will use - pkg-config to find the package. + - explicitly using ~--with-NAME=true~ (or "~yes~" or "~on~") will use pkg-config to find the package. - - using ~--with-NAME*! with a path will attempt to locate the package - without using ~pkg-config~. + - using ~--with-NAME*! with a path will attempt to locate the package without using ~pkg-config~. When in doubt, explicitly include ~--with-NAME*~ flags. * Using the =smplpkgs= tool to build suites of packages -The =smplpkgs= tool included in =waf-tools= provides a simple way to -build a suite of software packages that have interdependencies without -you, the developer, having to care much about the build system. +The =smplpkgs= tool included in =waf-tools= provides a simple way to build a suite of software packages that have interdependencies without you, the developer, having to care much about the build system. ** Package organization -To achieve this simplicity, some file and directory naminging -conventions and organization must be followed, as illustrated: +To achieve this simplicity, some file and directory conventions for naming and organization must be followed, as illustrated: #+BEGIN_EXAMPLE pkg1/ @@ -105,7 +78,9 @@ conventions and organization must be followed, as illustrated: ├── inc/ │   └── ProjectNamePkg1/*.h ├── src/*.{cxx,h} - └── test/*.{cxx,h} + ├── test/test_*.* + ├── test/check_*.* + └── test/wscript_check pkg2/ ├── wscript_build ├── inc/ @@ -117,12 +92,12 @@ conventions and organization must be followed, as illustrated: Notes on the structure: -- All packages placed in a top-level directory (not required, but aggregating them via =git submodule= is useful). -- Public header files for the package must be placed under =/inc//= -- Library source (implementation and private headers) under =/src/= -- Application source (implementation and private headers) under =/app/= with only main application files and one application per =*.cxx= file. -- Test source (implementation and private headers) under =/test/= with main test programs named like =test_*.cxx= -- A short `wscript_build` file in each package. +- The "sub packages" (~pgk1~, ~pkg2~) are top-level sub-directories. +- A ~wscript_build~ file in each sub-package to declare the name of the sub package, dependencies and any special case build rules. +- A header file =/inc//Xxx.h= are considered "public" and will be made available as ~#include "PackageName/Xxx.h"~. +- Library source (implementation and private headers) go under =/src/= +- Application ~main()~ source as =/app/app-name.cxx=. +- Units tests and integration/validation tests as =/test/{test,check}_*.*=. See the section [[Tests]] below. The == only matters in the top-level =wscript= file which you must provide. The == matters for inter-package dependencies. @@ -148,7 +123,69 @@ Test and application programs are allowed to have additional dependencies declar bld.smplpkg('MyPackage', use='YourPackage YourOtherPackage', test_use='ROOTSYS') #+END_SRC -* Using ~wcb~ in your own build +* Tests + +For more information on WCT testing see ~wire-cell-toolkit/tests/README.org~. What follows is a brief introduction to the support here in ~wire-cell-toolkit/waft/~. + +Atomic unit tests may be provided as: + +#+begin_example +/test/test*. +#+end_example + +Depending on ~~ of: + +- ~cxx~ or ~kokkos~ :: the source file will be compiled into a program of the same name (less ~.~). + +- ~py sh bats jsonnet~ :: the source will be interpreted by a corresponding interpreter. + +Atomic tests will also be run automatically unless suppressed with: + +#+begin_example +waf --notests [...] +#+end_example + +A "check" program with source found as: + +#+begin_example +/test/check_*. +#+end_example + +Will be compiled but not run if the ~~ is compilable as above. + +Arbitrary *variant tests* can be registered with Waf by registering their rule and inputs/outputs in + +#+begin_example +/wscript_build +#+end_example + +For example, + +#+begin_src python + with bld.smplpkg('MyPackage', use='YourPackage YourOtherPackage') as p: + p.rule("${WCSONNET} > ${TGT} 2>&1", target="wcsonnet-help.log") +#+end_src + +Additional higher level methods are also provided: + +- ~get_file(remote,local)~ :: download a remote file to a local one in the build via ~http://~, ~https://~ and ~scp:~. +- ~script(file)~ :: explicitly register a script (ie, one lacking a ~test_*.~ spelling) +- ~diff(one,two)~ :: do a diff on two files + +See ~wire-cell-toolkit/util/wscript_build~ for examples. + +** BATS tests + +A file: + +#+begin_example +/test_*.bats +#+end_example + +Will be run as an atomic unit test but may internally provide one ore BATS tests. + + +* Using ~wcb~ in your own project The ~wcb~ command is designed to build Wire-Cell Toolkit and is not a general purpose build tool. However, it may be used to build packages @@ -211,14 +248,8 @@ empty. Refer to WCT's: * Internals -The ~wcb.py~ file holds what might otherwise be in a top-level ~wscript~ -file. It's main thing is to list externals that can be handled in a -generic way (see next para) and also doing any non-generic -configuration. It also enacts some dependency analysis to avoid -building some sub-packages. +The ~wcb.py~ file holds what might otherwise be in a top-level ~wscript~ file. It's main thing is to list externals that can be handled in a generic way (see next para) and also doing any non-generic configuration. It also enacts some dependency analysis to avoid building some sub-packages. It holds things rather specific to Wire-Cell. -The ~generic.py~ file provides a ~configure()~ method used to find most -externals. It results in defining ~HAVE__LIB~ and ~HAVE__INC~ -when libs or includes are successfully checked for a given package. -These end up in ~config.h~ for use in C++ code. +The ~generic.py~ file provides a ~configure()~ method used to find most externals. It results in defining ~HAVE__LIB~ and ~HAVE__INC~ when libs or includes are successfully checked for a given package. These end up in ~config.h~ for use in C++ code. +The building of WCT packages themselves is governed by ~smplpkg.py~. This file implements WCT sub-package build policy but omits information about specific WCT targets (~wcb.py~ holds that). It thus may be used by non-WCT projects. diff --git a/waft/bjam.py b/waft/bjam.py new file mode 100644 index 000000000..8e04d3a66 --- /dev/null +++ b/waft/bjam.py @@ -0,0 +1,128 @@ +#! /usr/bin/env python +# per rosengren 2011 + +from os import sep, readlink +from waflib import Logs +from waflib.TaskGen import feature, after_method +from waflib.Task import Task, always_run + +def options(opt): + grp = opt.add_option_group('Bjam Options') + grp.add_option('--bjam_src', default=None, help='You can find it in /tools/jam/src') + grp.add_option('--bjam_uname', default='linuxx86_64', help='bjam is built in /bin./bjam') + grp.add_option('--bjam_config', default=None) + grp.add_option('--bjam_toolset', default=None) + +def configure(cnf): + if not cnf.env.BJAM_SRC: + cnf.env.BJAM_SRC = cnf.options.bjam_src + if not cnf.env.BJAM_UNAME: + cnf.env.BJAM_UNAME = cnf.options.bjam_uname + try: + cnf.find_program('bjam', path_list=[ + cnf.env.BJAM_SRC + sep + 'bin.' + cnf.env.BJAM_UNAME + ]) + except Exception: + cnf.env.BJAM = None + if not cnf.env.BJAM_CONFIG: + cnf.env.BJAM_CONFIG = cnf.options.bjam_config + if not cnf.env.BJAM_TOOLSET: + cnf.env.BJAM_TOOLSET = cnf.options.bjam_toolset + +@feature('bjam') +@after_method('process_rule') +def process_bjam(self): + if not self.bld.env.BJAM: + self.create_task('bjam_creator') + self.create_task('bjam_build') + self.create_task('bjam_installer') + if getattr(self, 'always', False): + always_run(bjam_creator) + always_run(bjam_build) + always_run(bjam_installer) + +class bjam_creator(Task): + ext_out = 'bjam_exe' + vars=['BJAM_SRC', 'BJAM_UNAME'] + def run(self): + env = self.env + gen = self.generator + bjam = gen.bld.root.find_dir(env.BJAM_SRC) + if not bjam: + Logs.error('Can not find bjam source') + return -1 + bjam_exe_relpath = 'bin.' + env.BJAM_UNAME + '/bjam' + bjam_exe = bjam.find_resource(bjam_exe_relpath) + if bjam_exe: + env.BJAM = bjam_exe.srcpath() + return 0 + bjam_cmd = ['./build.sh'] + Logs.debug('runner: ' + bjam.srcpath() + '> ' + str(bjam_cmd)) + result = self.exec_command(bjam_cmd, cwd=bjam.srcpath()) + if not result == 0: + Logs.error('bjam failed') + return -1 + bjam_exe = bjam.find_resource(bjam_exe_relpath) + if bjam_exe: + env.BJAM = bjam_exe.srcpath() + return 0 + Logs.error('bjam failed') + return -1 + +class bjam_build(Task): + ext_in = 'bjam_exe' + ext_out = 'install' + vars = ['BJAM_TOOLSET'] + def run(self): + env = self.env + gen = self.generator + path = gen.path + bld = gen.bld + if hasattr(gen, 'root'): + build_root = path.find_node(gen.root) + else: + build_root = path + jam = bld.srcnode.find_resource(env.BJAM_CONFIG) + if jam: + Logs.debug('bjam: Using jam configuration from ' + jam.srcpath()) + jam_rel = jam.relpath_gen(build_root) + else: + Logs.warn('No build configuration in build_config/user-config.jam. Using default') + jam_rel = None + bjam_exe = bld.srcnode.find_node(env.BJAM) + if not bjam_exe: + Logs.error('env.BJAM is not set') + return -1 + bjam_exe_rel = bjam_exe.relpath_gen(build_root) + cmd = ([bjam_exe_rel] + + (['--user-config=' + jam_rel] if jam_rel else []) + + ['--stagedir=' + path.get_bld().path_from(build_root)] + + ['--debug-configuration'] + + ['--with-' + lib for lib in self.generator.target] + + (['toolset=' + env.BJAM_TOOLSET] if env.BJAM_TOOLSET else []) + + ['link=' + 'shared'] + + ['variant=' + 'release'] + ) + Logs.debug('runner: ' + build_root.srcpath() + '> ' + str(cmd)) + ret = self.exec_command(cmd, cwd=build_root.srcpath()) + if ret != 0: + return ret + self.set_outputs(path.get_bld().ant_glob('lib/*') + path.get_bld().ant_glob('bin/*')) + return 0 + +class bjam_installer(Task): + ext_in = 'install' + def run(self): + gen = self.generator + path = gen.path + for idir, pat in (('${LIBDIR}', 'lib/*'), ('${BINDIR}', 'bin/*')): + files = [] + for n in path.get_bld().ant_glob(pat): + try: + t = readlink(n.srcpath()) + gen.bld.symlink_as(sep.join([idir, n.name]), t, postpone=False) + except OSError: + files.append(n) + gen.bld.install_files(idir, files, postpone=False) + return 0 + diff --git a/waft/boost.py b/waft/boost.py new file mode 100644 index 000000000..93b312a1e --- /dev/null +++ b/waft/boost.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python +# encoding: utf-8 +# +# partially based on boost.py written by Gernot Vormayr +# written by Ruediger Sonderfeld , 2008 +# modified by Bjoern Michaelsen, 2008 +# modified by Luca Fossati, 2008 +# rewritten for waf 1.5.1, Thomas Nagy, 2008 +# rewritten for waf 1.6.2, Sylvain Rouquette, 2011 + +''' + +This is an extra tool, not bundled with the default waf binary. +To add the boost tool to the waf file: +$ ./waf-light --tools=compat15,boost + or, if you have waf >= 1.6.2 +$ ./waf update --files=boost + +When using this tool, the wscript will look like: + + def options(opt): + opt.load('compiler_cxx boost') + + def configure(conf): + conf.load('compiler_cxx boost') + conf.check_boost(lib='system filesystem') + + def build(bld): + bld(source='main.cpp', target='app', use='BOOST') + +Options are generated, in order to specify the location of boost includes/libraries. +The `check_boost` configuration function allows to specify the used boost libraries. +It can also provide default arguments to the --boost-mt command-line arguments. +Everything will be packaged together in a BOOST component that you can use. + +When using MSVC, a lot of compilation flags need to match your BOOST build configuration: + - you may have to add /EHsc to your CXXFLAGS or define boost::throw_exception if BOOST_NO_EXCEPTIONS is defined. + Errors: C4530 + - boost libraries will try to be smart and use the (pretty but often not useful) auto-linking feature of MSVC + So before calling `conf.check_boost` you might want to disabling by adding + conf.env.DEFINES_BOOST += ['BOOST_ALL_NO_LIB'] + Errors: + - boost might also be compiled with /MT, which links the runtime statically. + If you have problems with redefined symbols, + self.env['DEFINES_%s' % var] += ['BOOST_ALL_NO_LIB'] + self.env['CXXFLAGS_%s' % var] += ['/MD', '/EHsc'] +Passing `--boost-linkage_autodetect` might help ensuring having a correct linkage in some basic cases. + +''' + +import sys +import re +from waflib import Utils, Logs, Errors +from waflib.Configure import conf +from waflib.TaskGen import feature, after_method + +BOOST_LIBS = ['/usr/lib', '/usr/local/lib', '/opt/local/lib', '/sw/lib', '/lib'] +BOOST_INCLUDES = ['/usr/include', '/usr/local/include', '/opt/local/include', '/sw/include'] +BOOST_VERSION_FILE = 'boost/version.hpp' +BOOST_VERSION_CODE = ''' +#include +#include +int main() { std::cout << BOOST_LIB_VERSION << ":" << BOOST_VERSION << std::endl; } +''' + +BOOST_ERROR_CODE = ''' +#include +int main() { boost::system::error_code c; } +''' + +PTHREAD_CODE = ''' +#include +static void* f(void*) { return 0; } +int main() { + pthread_t th; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_create(&th, &attr, &f, 0); + pthread_join(th, 0); + pthread_cleanup_push(0, 0); + pthread_cleanup_pop(0); + pthread_attr_destroy(&attr); +} +''' + +BOOST_THREAD_CODE = ''' +#include +int main() { boost::thread t; } +''' + +BOOST_LOG_CODE = ''' +#include +#include +#include +int main() { + using namespace boost::log; + add_common_attributes(); + add_console_log(std::clog, keywords::format = "%Message%"); + BOOST_LOG_TRIVIAL(debug) << "log is working" << std::endl; +} +''' + +# toolsets from {boost_dir}/tools/build/v2/tools/common.jam +PLATFORM = Utils.unversioned_sys_platform() +detect_intel = lambda env: (PLATFORM == 'win32') and 'iw' or 'il' +detect_clang = lambda env: (PLATFORM == 'darwin') and 'clang-darwin' or 'clang' +detect_mingw = lambda env: (re.search('MinGW', env.CXX[0])) and 'mgw' or 'gcc' +BOOST_TOOLSETS = { + 'borland': 'bcb', + 'clang': detect_clang, + 'como': 'como', + 'cw': 'cw', + 'darwin': 'xgcc', + 'edg': 'edg', + 'g++': detect_mingw, + 'gcc': detect_mingw, + 'icpc': detect_intel, + 'intel': detect_intel, + 'kcc': 'kcc', + 'kylix': 'bck', + 'mipspro': 'mp', + 'mingw': 'mgw', + 'msvc': 'vc', + 'qcc': 'qcc', + 'sun': 'sw', + 'sunc++': 'sw', + 'tru64cxx': 'tru', + 'vacpp': 'xlc' +} + + +def options(opt): + opt = opt.add_option_group('Boost Options') + opt.add_option('--boost-includes', type='string', + default='', dest='boost_includes', + help='''path to the directory where the boost includes are, + e.g., /path/to/boost_1_55_0/stage/include''') + opt.add_option('--boost-libs', type='string', + default='', dest='boost_libs', + help='''path to the directory where the boost libs are, + e.g., path/to/boost_1_55_0/stage/lib''') + opt.add_option('--boost-mt', action='store_true', + default=False, dest='boost_mt', + help='select multi-threaded libraries') + opt.add_option('--boost-abi', type='string', default='', dest='boost_abi', + help='''select libraries with tags (gd for debug, static is automatically added), + see doc Boost, Getting Started, chapter 6.1''') + opt.add_option('--boost-linkage_autodetect', action="store_true", dest='boost_linkage_autodetect', + help="auto-detect boost linkage options (don't get used to it / might break other stuff)") + opt.add_option('--boost-toolset', type='string', + default='', dest='boost_toolset', + help='force a toolset e.g. msvc, vc90, \ + gcc, mingw, mgw45 (default: auto)') + py_version = '%d%d' % (sys.version_info[0], sys.version_info[1]) + opt.add_option('--boost-python', type='string', + default=py_version, dest='boost_python', + help='select the lib python with this version \ + (default: %s)' % py_version) + + +@conf +def __boost_get_version_file(self, d): + if not d: + return None + dnode = self.root.find_dir(d) + if dnode: + return dnode.find_node(BOOST_VERSION_FILE) + return None + +@conf +def boost_get_version(self, d): + """silently retrieve the boost version number""" + node = self.__boost_get_version_file(d) + if node: + try: + txt = node.read() + except EnvironmentError: + Logs.error("Could not read the file %r", node.abspath()) + else: + re_but1 = re.compile('^#define\\s+BOOST_LIB_VERSION\\s+"(.+)"', re.M) + m1 = re_but1.search(txt) + re_but2 = re.compile('^#define\\s+BOOST_VERSION\\s+(\\d+)', re.M) + m2 = re_but2.search(txt) + if m1 and m2: + return (m1.group(1), m2.group(1)) + return self.check_cxx(fragment=BOOST_VERSION_CODE, includes=[d], execute=True, define_ret=True).split(":") + +@conf +def boost_get_includes(self, *k, **kw): + includes = k and k[0] or kw.get('includes') + if includes and self.__boost_get_version_file(includes): + return includes + for d in self.environ.get('INCLUDE', '').split(';') + BOOST_INCLUDES: + if self.__boost_get_version_file(d): + return d + if includes: + self.end_msg('headers not found in %s' % includes) + self.fatal('The configuration failed') + else: + self.end_msg('headers not found, please provide a --boost-includes argument (see help)') + self.fatal('The configuration failed') + + +@conf +def boost_get_toolset(self, cc): + toolset = cc + if not cc: + build_platform = Utils.unversioned_sys_platform() + if build_platform in BOOST_TOOLSETS: + cc = build_platform + else: + cc = self.env.CXX_NAME + if cc in BOOST_TOOLSETS: + toolset = BOOST_TOOLSETS[cc] + return isinstance(toolset, str) and toolset or toolset(self.env) + + +@conf +def __boost_get_libs_path(self, *k, **kw): + ''' return the lib path and all the files in it ''' + if 'files' in kw: + return self.root.find_dir('.'), Utils.to_list(kw['files']) + libs = k and k[0] or kw.get('libs') + if libs: + path = self.root.find_dir(libs) + files = path.ant_glob('*boost_*') + if not libs or not files: + for d in self.environ.get('LIB', '').split(';') + BOOST_LIBS: + if not d: + continue + path = self.root.find_dir(d) + if path: + files = path.ant_glob('*boost_*') + if files: + break + path = self.root.find_dir(d + '64') + if path: + files = path.ant_glob('*boost_*') + if files: + break + if not path: + if libs: + self.end_msg('libs not found in %s' % libs) + self.fatal('The configuration failed') + else: + self.end_msg('libs not found, please provide a --boost-libs argument (see help)') + self.fatal('The configuration failed') + + self.to_log('Found the boost path in %r with the libraries:' % path) + for x in files: + self.to_log(' %r' % x) + return path, files + +@conf +def boost_get_libs(self, *k, **kw): + ''' + return the lib path and the required libs + according to the parameters + ''' + path, files = self.__boost_get_libs_path(**kw) + files = sorted(files, key=lambda f: (len(f.name), f.name), reverse=True) + toolset = self.boost_get_toolset(kw.get('toolset', '')) + toolset_pat = '(-%s[0-9]{0,3})' % toolset + version = '-%s' % self.env.BOOST_VERSION + + def find_lib(re_lib, files): + for file in files: + if re_lib.search(file.name): + self.to_log('Found boost lib %s' % file) + return file + return None + + # extensions from Tools.ccroot.lib_patterns + wo_ext = re.compile(r"\.(a|so|lib|dll|dylib)(\.[0-9\.]+)?$") + def format_lib_name(name): + if name.startswith('lib') and self.env.CC_NAME != 'msvc': + name = name[3:] + return wo_ext.sub("", name) + + def match_libs(lib_names, is_static): + libs = [] + lib_names = Utils.to_list(lib_names) + if not lib_names: + return libs + t = [] + if kw.get('mt', False): + t.append('-mt') + if kw.get('abi'): + t.append('%s%s' % (is_static and '-s' or '-', kw['abi'])) + elif is_static: + t.append('-s') + tags_pat = t and ''.join(t) or '' + ext = is_static and self.env.cxxstlib_PATTERN or self.env.cxxshlib_PATTERN + ext = ext.partition('%s')[2] # remove '%s' or 'lib%s' from PATTERN + + for lib in lib_names: + if lib == 'python': + # for instance, with python='27', + # accepts '-py27', '-py2', '27', '-2.7' and '2' + # but will reject '-py3', '-py26', '26' and '3' + tags = '({0})?((-py{2})|(-py{1}(?=[^0-9]))|({2})|(-{1}.{3})|({1}(?=[^0-9]))|(?=[^0-9])(?!-py))'.format(tags_pat, kw['python'][0], kw['python'], kw['python'][1]) + else: + tags = tags_pat + # Trying libraries, from most strict match to least one + for pattern in ['boost_%s%s%s%s%s$' % (lib, toolset_pat, tags, version, ext), + 'boost_%s%s%s%s$' % (lib, tags, version, ext), + # Give up trying to find the right version + 'boost_%s%s%s%s$' % (lib, toolset_pat, tags, ext), + 'boost_%s%s%s$' % (lib, tags, ext), + 'boost_%s%s$' % (lib, ext), + 'boost_%s' % lib]: + self.to_log('Trying pattern %s' % pattern) + file = find_lib(re.compile(pattern), files) + if file: + libs.append(format_lib_name(file.name)) + break + else: + self.end_msg('lib %s not found in %s' % (lib, path.abspath())) + self.fatal('The configuration failed') + return libs + + return path.abspath(), match_libs(kw.get('lib'), False), match_libs(kw.get('stlib'), True) + +@conf +def _check_pthread_flag(self, *k, **kw): + ''' + Computes which flags should be added to CXXFLAGS and LINKFLAGS to compile in multi-threading mode + + Yes, we *need* to put the -pthread thing in CPPFLAGS because with GCC3, + boost/thread.hpp will trigger a #error if -pthread isn't used: + boost/config/requires_threads.hpp:47:5: #error "Compiler threading support + is not turned on. Please set the correct command line options for + threading: -pthread (Linux), -pthreads (Solaris) or -mthreads (Mingw32)" + + Based on _BOOST_PTHREAD_FLAG(): https://github.com/tsuna/boost.m4/blob/master/build-aux/boost.m4 + ''' + + var = kw.get('uselib_store', 'BOOST') + + self.start_msg('Checking the flags needed to use pthreads') + + # The ordering *is* (sometimes) important. Some notes on the + # individual items follow: + # (none): in case threads are in libc; should be tried before -Kthread and + # other compiler flags to prevent continual compiler warnings + # -lpthreads: AIX (must check this before -lpthread) + # -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h) + # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) + # -llthread: LinuxThreads port on FreeBSD (also preferred to -pthread) + # -pthread: GNU Linux/GCC (kernel threads), BSD/GCC (userland threads) + # -pthreads: Solaris/GCC + # -mthreads: MinGW32/GCC, Lynx/GCC + # -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it + # doesn't hurt to check since this sometimes defines pthreads too; + # also defines -D_REENTRANT) + # ... -mt is also the pthreads flag for HP/aCC + # -lpthread: GNU Linux, etc. + # --thread-safe: KAI C++ + if Utils.unversioned_sys_platform() == "sunos": + # On Solaris (at least, for some versions), libc contains stubbed + # (non-functional) versions of the pthreads routines, so link-based + # tests will erroneously succeed. (We need to link with -pthreads/-mt/ + # -lpthread.) (The stubs are missing pthread_cleanup_push, or rather + # a function called by this macro, so we could check for that, but + # who knows whether they'll stub that too in a future libc.) So, + # we'll just look for -pthreads and -lpthread first: + boost_pthread_flags = ["-pthreads", "-lpthread", "-mt", "-pthread"] + else: + boost_pthread_flags = ["", "-lpthreads", "-Kthread", "-kthread", "-llthread", "-pthread", + "-pthreads", "-mthreads", "-lpthread", "--thread-safe", "-mt"] + + for boost_pthread_flag in boost_pthread_flags: + try: + self.env.stash() + self.env.append_value('CXXFLAGS_%s' % var, boost_pthread_flag) + self.env.append_value('LINKFLAGS_%s' % var, boost_pthread_flag) + self.check_cxx(code=PTHREAD_CODE, msg=None, use=var, execute=False) + + self.end_msg(boost_pthread_flag) + return + except self.errors.ConfigurationError: + self.env.revert() + self.end_msg('None') + +@conf +def check_boost(self, *k, **kw): + """ + Initialize boost libraries to be used. + + Keywords: you can pass the same parameters as with the command line (without "--boost-"). + Note that the command line has the priority, and should preferably be used. + """ + if not self.env['CXX']: + self.fatal('load a c++ compiler first, conf.load("compiler_cxx")') + + params = { + 'lib': k and k[0] or kw.get('lib'), + 'stlib': kw.get('stlib') + } + for key, value in self.options.__dict__.items(): + if not key.startswith('boost_'): + continue + key = key[len('boost_'):] + params[key] = value and value or kw.get(key, '') + + var = kw.get('uselib_store', 'BOOST') + + self.find_program('dpkg-architecture', var='DPKG_ARCHITECTURE', mandatory=False) + if self.env.DPKG_ARCHITECTURE: + deb_host_multiarch = self.cmd_and_log([self.env.DPKG_ARCHITECTURE[0], '-qDEB_HOST_MULTIARCH']) + BOOST_LIBS.insert(0, '/usr/lib/%s' % deb_host_multiarch.strip()) + + self.start_msg('Checking boost includes') + self.env['INCLUDES_%s' % var] = inc = self.boost_get_includes(**params) + versions = self.boost_get_version(inc) + self.env.BOOST_VERSION = versions[0] + self.env.BOOST_VERSION_NUMBER = int(versions[1]) + self.end_msg("%d.%d.%d" % (int(versions[1]) / 100000, + int(versions[1]) / 100 % 1000, + int(versions[1]) % 100)) + if Logs.verbose: + Logs.pprint('CYAN', ' path : %s' % self.env['INCLUDES_%s' % var]) + + if not params['lib'] and not params['stlib']: + return + if 'static' in kw or 'static' in params: + Logs.warn('boost: static parameter is deprecated, use stlib instead.') + self.start_msg('Checking boost libs') + path, libs, stlibs = self.boost_get_libs(**params) + self.env['LIBPATH_%s' % var] = [path] + self.env['STLIBPATH_%s' % var] = [path] + self.env['LIB_%s' % var] = libs + self.env['STLIB_%s' % var] = stlibs + self.end_msg('ok') + if Logs.verbose: + Logs.pprint('CYAN', ' path : %s' % path) + Logs.pprint('CYAN', ' shared libs : %s' % libs) + Logs.pprint('CYAN', ' static libs : %s' % stlibs) + + def has_shlib(lib): + return params['lib'] and lib in params['lib'] + def has_stlib(lib): + return params['stlib'] and lib in params['stlib'] + def has_lib(lib): + return has_shlib(lib) or has_stlib(lib) + if has_lib('thread'): + # not inside try_link to make check visible in the output + self._check_pthread_flag(k, kw) + + def try_link(): + if has_lib('system'): + self.check_cxx(fragment=BOOST_ERROR_CODE, use=var, execute=False) + if has_lib('thread'): + self.check_cxx(fragment=BOOST_THREAD_CODE, use=var, execute=False) + if has_lib('log'): + if not has_lib('thread'): + self.env['DEFINES_%s' % var] += ['BOOST_LOG_NO_THREADS'] + if has_shlib('log'): + self.env['DEFINES_%s' % var] += ['BOOST_LOG_DYN_LINK'] + self.check_cxx(fragment=BOOST_LOG_CODE, use=var, execute=False) + + if params.get('linkage_autodetect', False): + self.start_msg("Attempting to detect boost linkage flags") + toolset = self.boost_get_toolset(kw.get('toolset', '')) + if toolset in ('vc',): + # disable auto-linking feature, causing error LNK1181 + # because the code wants to be linked against + self.env['DEFINES_%s' % var] += ['BOOST_ALL_NO_LIB'] + + # if no dlls are present, we guess the .lib files are not stubs + has_dlls = False + for x in Utils.listdir(path): + if x.endswith(self.env.cxxshlib_PATTERN % ''): + has_dlls = True + break + if not has_dlls: + self.env['STLIBPATH_%s' % var] = [path] + self.env['STLIB_%s' % var] = libs + del self.env['LIB_%s' % var] + del self.env['LIBPATH_%s' % var] + + # we attempt to play with some known-to-work CXXFLAGS combinations + for cxxflags in (['/MD', '/EHsc'], []): + self.env.stash() + self.env["CXXFLAGS_%s" % var] += cxxflags + try: + try_link() + except Errors.ConfigurationError as e: + self.env.revert() + exc = e + else: + self.end_msg("ok: winning cxxflags combination: %s" % (self.env["CXXFLAGS_%s" % var])) + exc = None + self.env.commit() + break + + if exc is not None: + self.end_msg("Could not auto-detect boost linking flags combination, you may report it to boost.py author", ex=exc) + self.fatal('The configuration failed') + else: + self.end_msg("Boost linkage flags auto-detection not implemented (needed ?) for this toolchain") + self.fatal('The configuration failed') + else: + self.start_msg('Checking for boost linkage') + try: + try_link() + except Errors.ConfigurationError as e: + self.end_msg("Could not link against boost libraries using supplied options") + self.fatal('The configuration failed') + self.end_msg('ok') + + +@feature('cxx') +@after_method('apply_link') +def install_boost(self): + if install_boost.done or not Utils.is_win32 or not self.bld.cmd.startswith('install'): + return + install_boost.done = True + inst_to = getattr(self, 'install_path', '${BINDIR}') + for lib in self.env.LIB_BOOST: + try: + file = self.bld.find_file(self.env.cxxshlib_PATTERN % lib, self.env.LIBPATH_BOOST) + self.add_install_files(install_to=inst_to, install_from=self.bld.root.find_node(file)) + except: + continue +install_boost.done = False diff --git a/waft/cuda.py b/waft/cuda.py index ec73c32f2..5a46c4f35 100644 --- a/waft/cuda.py +++ b/waft/cuda.py @@ -1,4 +1,4 @@ -from . import generic +import generic from waflib import Task from waflib.TaskGen import extension diff --git a/waft/doxygen.py b/waft/doxygen.py new file mode 100644 index 000000000..0fda70361 --- /dev/null +++ b/waft/doxygen.py @@ -0,0 +1,236 @@ +#! /usr/bin/env python +# encoding: UTF-8 +# Thomas Nagy 2008-2010 (ita) + +""" + +Doxygen support + +Variables passed to bld(): +* doxyfile -- the Doxyfile to use +* doxy_tar -- destination archive for generated documentation (if desired) +* install_path -- where to install the documentation +* pars -- dictionary overriding doxygen configuration settings + +When using this tool, the wscript will look like: + + def options(opt): + opt.load('doxygen') + + def configure(conf): + conf.load('doxygen') + # check conf.env.DOXYGEN, if it is mandatory + + def build(bld): + if bld.env.DOXYGEN: + bld(features="doxygen", doxyfile='Doxyfile', ...) +""" + +import os, os.path, re +from collections import OrderedDict +from waflib import Task, Utils, Node +from waflib.TaskGen import feature + +DOXY_STR = '"${DOXYGEN}" - ' +DOXY_FMTS = 'html latex man rft xml'.split() +DOXY_FILE_PATTERNS = '*.' + ' *.'.join(''' +c cc cxx cpp c++ java ii ixx ipp i++ inl h hh hxx hpp h++ idl odl cs php php3 +inc m mm py f90c cc cxx cpp c++ java ii ixx ipp i++ inl h hh hxx +'''.split()) + +re_rl = re.compile('\\\\\r*\n', re.MULTILINE) +re_nl = re.compile('\r*\n', re.M) +def parse_doxy(txt): + ''' + Parses a doxygen file. + Returns an ordered dictionary. We cannot return a default dictionary, as the + order in which the entries are reported does matter, especially for the + '@INCLUDE' lines. + ''' + tbl = OrderedDict() + txt = re_rl.sub('', txt) + lines = re_nl.split(txt) + for x in lines: + x = x.strip() + if not x or x.startswith('#') or x.find('=') < 0: + continue + if x.find('+=') >= 0: + tmp = x.split('+=') + key = tmp[0].strip() + if key in tbl: + tbl[key] += ' ' + '+='.join(tmp[1:]).strip() + else: + tbl[key] = '+='.join(tmp[1:]).strip() + else: + tmp = x.split('=') + tbl[tmp[0].strip()] = '='.join(tmp[1:]).strip() + return tbl + +class doxygen(Task.Task): + vars = ['DOXYGEN', 'DOXYFLAGS'] + color = 'BLUE' + ext_in = [ '.py', '.c', '.h', '.java', '.pb.cc' ] + + def runnable_status(self): + ''' + self.pars are populated in runnable_status - because this function is being + run *before* both self.pars "consumers" - scan() and run() + + set output_dir (node) for the output + ''' + + for x in self.run_after: + if not x.hasrun: + return Task.ASK_LATER + + if not getattr(self, 'pars', None): + txt = self.inputs[0].read() + self.pars = parse_doxy(txt) + + # Override with any parameters passed to the task generator + if getattr(self.generator, 'pars', None): + for k, v in self.generator.pars.items(): + self.pars[k] = v + + if self.pars.get('OUTPUT_DIRECTORY'): + # Use the path parsed from the Doxyfile as an absolute path + output_node = self.inputs[0].parent.get_bld().make_node(self.pars['OUTPUT_DIRECTORY']) + else: + # If no OUTPUT_PATH was specified in the Doxyfile, build path from the Doxyfile name + '.doxy' + output_node = self.inputs[0].parent.get_bld().make_node(self.inputs[0].name + '.doxy') + output_node.mkdir() + self.pars['OUTPUT_DIRECTORY'] = output_node.abspath() + + self.doxy_inputs = getattr(self, 'doxy_inputs', []) + if not self.pars.get('INPUT'): + self.doxy_inputs.append(self.inputs[0].parent) + else: + for i in self.pars.get('INPUT').split(): + if os.path.isabs(i): + node = self.generator.bld.root.find_node(i) + else: + node = self.inputs[0].parent.find_node(i) + if not node: + self.generator.bld.fatal('Could not find the doxygen input %r' % i) + self.doxy_inputs.append(node) + + if not getattr(self, 'output_dir', None): + bld = self.generator.bld + # Output path is always an absolute path as it was transformed above. + self.output_dir = bld.root.find_dir(self.pars['OUTPUT_DIRECTORY']) + + self.signature() + ret = Task.Task.runnable_status(self) + if ret == Task.SKIP_ME: + # in case the files were removed + self.add_install() + return ret + + def scan(self): + exclude_patterns = self.pars.get('EXCLUDE_PATTERNS','').split() + exclude_patterns = [pattern.replace('*/', '**/') for pattern in exclude_patterns] + file_patterns = self.pars.get('FILE_PATTERNS','').split() + if not file_patterns: + file_patterns = DOXY_FILE_PATTERNS.split() + if self.pars.get('RECURSIVE') == 'YES': + file_patterns = ["**/%s" % pattern for pattern in file_patterns] + nodes = [] + names = [] + for node in self.doxy_inputs: + if os.path.isdir(node.abspath()): + for m in node.ant_glob(incl=file_patterns, excl=exclude_patterns): + nodes.append(m) + else: + nodes.append(node) + return (nodes, names) + + def run(self): + dct = self.pars.copy() + code = '\n'.join(['%s = %s' % (x, dct[x]) for x in self.pars]) + code = code.encode() # for python 3 + #fmt = DOXY_STR % (self.inputs[0].parent.abspath()) + cmd = Utils.subst_vars(DOXY_STR, self.env) + env = self.env.env or None + proc = Utils.subprocess.Popen(cmd, shell=True, stdin=Utils.subprocess.PIPE, env=env, cwd=self.inputs[0].parent.abspath()) + proc.communicate(code) + return proc.returncode + + def post_run(self): + nodes = self.output_dir.ant_glob('**/*', quiet=True) + for x in nodes: + self.generator.bld.node_sigs[x] = self.uid() + self.add_install() + return Task.Task.post_run(self) + + def add_install(self): + nodes = self.output_dir.ant_glob('**/*', quiet=True) + self.outputs += nodes + if getattr(self.generator, 'install_path', None): + if not getattr(self.generator, 'doxy_tar', None): + self.generator.add_install_files(install_to=self.generator.install_path, + install_from=self.outputs, + postpone=False, + cwd=self.output_dir, + relative_trick=True) + +class tar(Task.Task): + "quick tar creation" + run_str = '${TAR} ${TAROPTS} ${TGT} ${SRC}' + color = 'RED' + after = ['doxygen'] + def runnable_status(self): + for x in getattr(self, 'input_tasks', []): + if not x.hasrun: + return Task.ASK_LATER + + if not getattr(self, 'tar_done_adding', None): + # execute this only once + self.tar_done_adding = True + for x in getattr(self, 'input_tasks', []): + self.set_inputs(x.outputs) + if not self.inputs: + return Task.SKIP_ME + return Task.Task.runnable_status(self) + + def __str__(self): + tgt_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.outputs]) + return '%s: %s\n' % (self.__class__.__name__, tgt_str) + +@feature('doxygen') +def process_doxy(self): + if not getattr(self, 'doxyfile', None): + self.bld.fatal('no doxyfile variable specified??') + + node = self.doxyfile + if not isinstance(node, Node.Node): + node = self.path.find_resource(node) + if not node: + self.bld.fatal('doxygen file %s not found' % self.doxyfile) + + # the task instance + dsk = self.create_task('doxygen', node, always_run=getattr(self, 'always', False)) + + if getattr(self, 'doxy_tar', None): + tsk = self.create_task('tar', always_run=getattr(self, 'always', False)) + tsk.input_tasks = [dsk] + tsk.set_outputs(self.path.find_or_declare(self.doxy_tar)) + if self.doxy_tar.endswith('bz2'): + tsk.env['TAROPTS'] = ['cjf'] + elif self.doxy_tar.endswith('gz'): + tsk.env['TAROPTS'] = ['czf'] + else: + tsk.env['TAROPTS'] = ['cf'] + if getattr(self, 'install_path', None): + self.add_install_files(install_to=self.install_path, install_from=tsk.outputs) + +def configure(conf): + ''' + Check if doxygen and tar commands are present in the system + + If the commands are present, then conf.env.DOXYGEN and conf.env.TAR + variables will be set. Detection can be controlled by setting DOXYGEN and + TAR environmental variables. + ''' + + conf.find_program('doxygen', var='DOXYGEN', mandatory=False) + conf.find_program('tar', var='TAR', mandatory=False) diff --git a/waft/kokkos.py b/waft/kokkos.py index 138ba7c70..c87b8e7aa 100644 --- a/waft/kokkos.py +++ b/waft/kokkos.py @@ -1,4 +1,4 @@ -from . import generic +import generic from waflib import Task from waflib.TaskGen import extension diff --git a/waft/libtorch.py b/waft/libtorch.py index 9316468d5..48c2a62e5 100644 --- a/waft/libtorch.py +++ b/waft/libtorch.py @@ -1,4 +1,4 @@ -from . import generic +import generic def options(opt): generic._options(opt, "libtorch") diff --git a/waft/refresh-wcb b/waft/refresh-wcb index 534a620f7..ae58c3f61 100755 --- a/waft/refresh-wcb +++ b/waft/refresh-wcb @@ -5,18 +5,16 @@ output="$(pwd)/wcb" wafurl="https://gitlab.com/ita1024/waf.git" -wafdir="$(pwd)/waf" -waftag="waf-2.0.20" +waftag="waf-2.0.25" +wafdir="/tmp/wcb/$waftag" - -toolsurl="https://github.com/WireCell/waf-tools.git" -tooldir="$(pwd)" -# initial defaults from waf -tools="doxygen boost bjam" +tooldir="$(dirname $BASH_SOURCE)" +# these are copied from waf +# tools="doxygen boost bjam" # add any in cwd, assuming we are sitting in waf-tools -tools="$tools $(echo *.py| sed 's/.py//g')" - +tools="$(cd $tooldir && echo *.py | sed 's/.py//g')" +echo $tools usage () { @@ -44,9 +42,6 @@ while getopts "v:w:t:o:W:T:" opt; do W) wafurl=$OPTARG ;; - T) - toolsurl=$OPTARG - ;; *) usage ;; @@ -69,10 +64,12 @@ else git checkout $waftag popd fi +echo "using waf in $wafdir" if [ ! -d "$tooldir" ] ; then - git clone "$toolsurl" "$tooldir" + echo "This script must exist in the wire-cell-toolkit/waft/ directory" + exit 1 fi -if [ ! -f "$tooldir/smplpkgs.py" ] ; then +if [ ! -f "$tooldir/boost.py" ] ; then echo "Tooldir does not look correct: $tooldir" exit 1 fi diff --git a/waft/smplpkgs.py b/waft/smplpkgs.py index 93ecbefc3..bede8fa74 100644 --- a/waft/smplpkgs.py +++ b/waft/smplpkgs.py @@ -3,18 +3,22 @@ Your source package may build any combination of the following: - - shared libraries - - headers exposing an API to libraries + - shared libraries (src/*.cxx) + - headers exposing an API to libraries (inc/NAME/*.h) - a ROOT dictionary for this API - - main programs - - test programs + - main programs (apps/*.cxx) + - test programs (test/test_*, test/check_*, wscript_build) This tool will produce various methods on the build context. You can avoid passing to them if you set APPNAME in your wscript file. +This file is part of the wire-cell-toolkit but we keep it free of +direct concepts specific to building WCT. See wcb.py for those. + ''' -import os.path as osp +import os +from contextlib import contextmanager from waflib.Utils import to_list from waflib.Configure import conf import waflib.Context @@ -33,7 +37,6 @@ def __str__(self): for edge, attrs in self._edges.items(): for cat in attrs: - # print (edge, cat, self.colors[cat]) extra = "" if cat == "tst": extra=',constraint=false' @@ -43,7 +46,6 @@ def __str__(self): return '\n'.join(lines) def register(self, pkg, **kwds): - # print ("register %s" % pkg) self.add_node(pkg) for cat, deps in kwds.items(): kwds = {cat: True} @@ -63,12 +65,13 @@ def add_edge(self, edge, **kwds): -_tooldir = osp.dirname(osp.abspath(__file__)) +_tooldir = os.path.dirname(os.path.abspath(__file__)) def options(opt): opt.load('compiler_cxx') opt.load('waf_unit_test') - + opt.add_option('--nochecks', action='store_true', default=False, + help='Exec no checks', dest='no_checks') def configure(cfg): cfg.load('compiler_cxx') @@ -77,20 +80,301 @@ def configure(cfg): cfg.env.append_unique('CXXFLAGS',['-std=c++17']) + # Do not add any things specific to WCT here. see wcb.py instead. + + # interpreters cfg.find_program('python', var='PYTHON', mandatory=True) cfg.find_program('bash', var='BASH', mandatory=True) + cfg.find_program('bats', var='BATS', mandatory=False) + cfg.find_program('jsonnet', var='JSONNET', mandatory=False) + + # For testing + cfg.find_program('diff', var='DIFF', mandatory=False) pass def build(bld): from waflib.Tools import waf_unit_test bld.add_post_fun(waf_unit_test.summary) - #print ("smplpkgs.build()") + +@conf +def cycle_group(bld, gname): + if gname in bld.group_names: + bld.set_group(gname) + else: + bld.add_group(gname) + + +# from waflib import Task, TaskGen +# @TaskGen.feature('test_variant') +# @TaskGen.after_method('process_source', 'apply_link') +# def make_test_variant(self): +# cmdline = self.ut_str +# progname, argline = cmdline.split(' ',1) + +# tvp = 'TEST_VARIANT_PROGRAM' + +# output = self.path.find_or_declare(self.name + ".passed") + +# source = getattr(self, 'source', None) +# srcnodes = self.to_nodes(source) + +# if not source and progname.startswith("${"): +# warn("parameterized program name with lacking inputs is not supported: " + cmdline) +# return + +# if not progname.startswith("${"): +# prognode = self.path.find_or_declare(progname) +# progname = "${%s}" % tvp + +# cmdline = "%s %s && touch %s" % (progname, argline, output.abspath()) + +# tsk = self.create_task('utest', srcnodes, [output]) + +# if tvp in progname: +# tsk.env[tvp] = prognode.abspath() +# tsk.vars.append(tvp) +# tsk.dep_nodes.append(prognode) + +# self.ut_run, lst = Task.compile_fun(cmdline, shell=True) +# tsk.vars = lst + tsk.vars + +# if getattr(self, 'ut_cwd', None): +# self.handle_ut_cwd('ut_cwd') +# else: +# self.ut_cwd = self.path + +# if not self.ut_cwd.exists(): +# self.ut_cwd.mkdir() + +# if not hasattr(self, 'ut_env'): +# self.ut_env = dict(os.environ) +# def add_paths(var, lst): +# # Add list of paths to a variable, lst can contain strings or nodes +# lst = [ str(n) for n in lst ] +# debug("ut: %s: Adding paths %s=%s", self, var, lst) +# self.ut_env[var] = os.pathsep.join(lst) + os.pathsep + self.ut_env.get(var, '') + + +# A "fake" waf context (but real Python context manager) which will +# return the Waf "group" to "libraries" on exit and which provides +# several task generators and which defines tasks for the default +# convention of test/test_*.* and test/check_*.cxx. This is returned +# by smplpkg(). +class ValidationContext: + + compiled_extensions = ['.cxx', '.kokkos'] + script_interpreters = {'.py':"python", '.bats':'bats', '.sh':'bash', '.jsonnet':'jsonnet'} + + def __init__(self, bld, uses): + ''' + Uses must include list of dependencies in "uses". + ''' + self.bld = bld + self.uses = to_list(uses) + + if self.bld.options.no_tests: + self.bld.options.no_checks = True # need tests for checks, in general + info("atomic unit tests will not be built nor run for " + self.bld.path.name) + return + + self.bld.cycle_group("validations") + + # Fake out: checks need tests but tests do not need checks but + # tests and checks are defined by the same functions which + # honor no_checks. + no_checks = self.bld.options.no_checks + self.bld.options.no_checks = False + + # Default patterns + for one in self.bld.path.ant_glob('test/check_*.cxx'): + self.program(one) + + # Atomic unit tests + for ext in self.compiled_extensions: + for one in self.bld.path.ant_glob('test/test_*'+ext): + self.program(one, "test") + + for ext in self.script_interpreters: + for one in self.bld.path.ant_glob('test/test*'+ext): + self.script(one) + + self.bld.options.no_checks = no_checks + + def __enter__(self): + if self.bld.options.no_checks: + info("variant checks will not be built nor run for " + self.bld.path.name) + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.bld.cycle_group("libraries") + return + + def nodify_resource(self, name_or_node, path=None): + 'Return a resource node' + + if path is None: + path = self.bld.path + if isinstance(name_or_node, waflib.Node.Node): + return name_or_node + return path.find_resource(name_or_node) + + def nodify_declare(self, name_or_node, path=None): + 'Return a resource node' + + if path is None: + path = self.bld.path + if isinstance(name_or_node, waflib.Node.Node): + return name_or_node + return path.find_or_declare(name_or_node) + + def program(self, source, features=""): + '''Compile a C++ program to use in validation. + + Add "test" as a feature to also run as a unit test. + + ''' + if self.bld.options.no_checks: + return + features = ["cxx","cxxprogram"] + to_list(features) + rpath = self.bld.get_rpath(self.uses) # fixme + source = self.nodify_resource(source) + ext = source.suffix() + self.bld.program(source = [source], + target = source.name.replace(ext,''), + features = features, + install_path = None, + #rpath = rpath, + includes = ['inc','test','tests'], + use = self.uses) + + def script(self, source): + 'Create a task for atomic unit test with an interpreted script' + if self.bld.options.no_checks: + return + source = self.nodify_resource(source) + ext = source.suffix() + + interp = self.script_interpreters.get(ext, None) + + if interp is None: + warn(f'skipping script with no known interpreter: {source}') + return + + INTERP = interp.upper() + if INTERP not in self.bld.env: + warn(f'skipping script with no found interpreter: {source}') + return + + # info(f'{interp} {source}') + self.bld(features="test_scripts", + ut_cwd = self.bld.path, + use = self.uses, + test_scripts_source = source, + test_scripts_template = "${%s} ${SCRIPT}" % INTERP) + + + # def variant(self, cmdline, **kwds): + # name = cmdline.replace(" ","_").replace("/","_").replace("$","_").replace("{","_").replace("}","_") + # self.bld(name=name, features="test_variant", ut_str = cmdline) + + + def rule(self, rule, source="", target="", **kwds): + 'Simple wrapper for arbitrary rule' + if self.bld.options.no_checks: + return + self.bld(rule=rule, source=source, target=target, **kwds) + + + def rule_http_get(self, task): + 'A rule function transform a URL file into its target via HTTP(s)' + if self.bld.options.no_checks: + return + from urllib.request import urlopen + unode = task.inputs[0] + remote = unode.read() + text = urlopen(remote).read().decode() + onode = task.outputs[0] + onode.write(text) + + + def rule_scp_get(self, task): + 'A rule function to transform a URL file its target via scp' + if self.bld.options.no_checks: + return + unode = task.inputs[0] + remote = unode.read()[4:] + local = task.outputs[0].abspath(); + return task.exec_command(f'scp {remote} {local}') + + + # def rule_cp_get(self, task): + # 'A rule function to copy input to output' + # remote = task.inputs[0].abspath() + # local = task.outputs[0].abspath() + # return task.exec_command(f'cp {remote} {local}') + + + def get_file(self, remote='...', local='...'): + 'Make task to bring remote file to local file' + if self.bld.options.no_checks: + return + if remote.startswith(("http://","https://")): + rule_get = lambda task: self.rule_http_get(task) + elif remote.startswith("scp:"): + rule_get = lambda task: self.rule_scp_get(task) + else: + raise ValueError("get file from absolute path not allowed") + + lnode = self.bld.path.find_or_declare(local) + unode = lnode.parent.make_node(lnode.name + ".url") + unode.write(remote) + # make task instead of doing this immediately + self.bld(rule=rule_get, source = unode, target = lnode) + + def put_file(self, local='...', remote='...'): + if self.bld.options.no_checks: + return + raise Unimplemented() + + def diff(self, one, two): + 'Make a task to output a diff between two files' + if self.bld.options.no_checks: + return + one = self.nodify_declare(one) + two = self.nodify_declare(two) + dnode = one.parent.find_or_declare(one.name +"_"+ two.name +".diff") + self.bld(rule="${DIFF} ${SRC} > ${TGT}", + source=[one, two], target=[dnode], shell=True) + + +@conf +def get_rpath(bld, uselst, local=True): + ret = set([bld.env["PREFIX"]+"/lib"]) + for one in uselst: + libpath = bld.env["LIBPATH_"+one] + for l in libpath: + ret.add(l) + if local: + if one.startswith("WireCell"): + sd = one[8:].lower() + blddir = bld.path.find_or_declare(bld.out_dir) + pkgdir = blddir.find_or_declare(sd).abspath() + ret.add(pkgdir) + ret = list(ret) + ret.sort() + return ret @conf def smplpkg(bld, name, use='', app_use='', test_use=''): + use = list(set(to_list(use))) + use.sort() + app_use = list(set(use + to_list(app_use))) + app_use.sort() + test_use = list(set(use + to_list(test_use))) + test_use.sort() + if not hasattr(bld, 'smplpkg_graph'): - #print ("Make SimpleGraph") bld.smplpkg_graph = SimpleGraph() bld.smplpkg_graph.register( name, @@ -98,13 +382,6 @@ def smplpkg(bld, name, use='', app_use='', test_use=''): app=set(to_list(app_use)), tst=set(to_list(test_use))) - use = list(set(to_list(use))) - use.sort() - app_use = list(set(use + to_list(app_use))) - app_use.sort() - test_use = list(set(use + to_list(test_use))) - test_use.sort() - includes = [bld.out_dir] headers = [] source = [] @@ -112,16 +389,10 @@ def smplpkg(bld, name, use='', app_use='', test_use=''): incdir = bld.path.find_dir('inc') srcdir = bld.path.find_dir('src') dictdir = bld.path.find_dir('dict') - - testsrc = bld.path.ant_glob('test/test_*.cxx') - testsrc_kokkos = bld.path.ant_glob('test/test_*.kokkos') - test_scripts = bld.path.ant_glob('test/test_*.sh') + bld.path.ant_glob('test/test_*.py') - test_jsonnets = bld.path.ant_glob('test/test*.jsonnet') - - checksrc = bld.path.ant_glob('test/check_*.cxx') - appsdir = bld.path.find_dir('apps') + bld.cycle_group("libraries") + if incdir: headers += incdir.ant_glob(name + '/*.h') includes += ['inc'] @@ -140,7 +411,6 @@ def smplpkg(bld, name, use='', app_use='', test_use=''): if dictdir: if not headers: error('No header files for ROOT dictionary "%s"' % name) - #print 'Building ROOT dictionary: %s using %s' % (name,use) if 'ROOTSYS' in use: linkdef = dictdir.find_resource('LinkDef.h') bld.gen_rootcling_dict(name, linkdef, @@ -153,31 +423,10 @@ def smplpkg(bld, name, use='', app_use='', test_use=''): if hasattr(bld.env, "PROTOC"): pbs = bld.path.ant_glob('src/**/*.proto') - # if ("zpb" in name.lower()): - # print ("protobufs: %s" % (pbs,)) source += pbs - def get_rpath(uselst, local=True): - ret = set([bld.env["PREFIX"]+"/lib"]) - for one in uselst: - libpath = bld.env["LIBPATH_"+one] - for l in libpath: - ret.add(l) - if local: - if one.startswith("WireCell"): - sd = one[8:].lower() - blddir = bld.path.find_or_declare(bld.out_dir) - pkgdir = blddir.find_or_declare(sd).abspath() - #print pkgdir - ret.add(pkgdir) - ret = list(ret) - ret.sort() - return ret - # the library if srcdir: - if ("zpb" in name.lower()): - print ("Building library: %s, using %s, source: %s"%(name, use, source)) ei = '' if incdir: ei = 'inc' @@ -185,73 +434,25 @@ def get_rpath(uselst, local=True): name = name, source = source, target = name, - #rpath = get_rpath(use), + #rpath = bld.get_rpath(use), includes = includes, # 'inc', export_includes = ei, use = use) + bld.cycle_group("applications") + if appsdir: for app in appsdir.ant_glob('*.cxx'): - #print 'Building %s app: %s using %s' % (name, app, app_use) + appbin = bld.path.find_or_declare(app.name.replace('.cxx','')) bld.program(source = [app], - target = app.name.replace('.cxx',''), + target = appbin, includes = includes, # 'inc', - #rpath = get_rpath(app_use + [name], local=False), + #rpath = bld.get_rpath(app_use + [name], local=False), use = app_use + [name]) + keyname = appbin.name.upper().replace("-","_") + bld.env[keyname] = appbin.abspath() + + bld.cycle_group("libraries") + return ValidationContext(bld, test_use + [name]) - if (testsrc or testsrc_kokkos or test_scripts) and not bld.options.no_tests: - for test_main in testsrc_kokkos: - #print 'Building %s test: %s' % (name, test_main) - rpath = get_rpath(test_use + [name]) - #print rpath - bld.program(features = 'cxx cxxprogram test', - source = [test_main], - ut_cwd = bld.path, - target = test_main.name.replace('.kokkos',''), - install_path = None, - #rpath = rpath, - includes = ['inc','test','tests'], - use = test_use + [name]) - for test_main in testsrc: - #print 'Building %s test: %s' % (name, test_main) - rpath = get_rpath(test_use + [name]) - #print rpath - bld.program(features = 'test', - source = [test_main], - ut_cwd = bld.path, - target = test_main.name.replace('.cxx',''), - install_path = None, - #rpath = rpath, - includes = ['inc','test','tests'], - use = test_use + [name]) - for test_script in test_scripts: - interp = "${BASH}" - if test_script.abspath().endswith(".py"): - interp = "${PYTHON}" - #print 'Building %s test %s script: %s using %s' % (name, interp, test_script, test_use) - bld(features="test_scripts", - ut_cwd = bld.path, - test_scripts_source = test_script, - test_scripts_template = "pwd && " + interp + " ${SCRIPT}") - - if test_jsonnets and not bld.options.no_tests: - # print ("testing %d jsonnets in %s" % (len(test_jsonnets), bld.path )) - for test_jsonnet in test_jsonnets: - bld(features="test_scripts", - ut_cwd = bld.path, - test_scripts_source = test_jsonnet, - test_scripts_template = "pwd && ../build/apps/wcsonnet ${SCRIPT}") - - if checksrc and not bld.options.no_tests: - for check_main in checksrc: - #print 'Building %s check: %s' % (name, check_main) - rpath = get_rpath(test_use + [name]) - #print rpath - bld.program(source = [check_main], - target = check_main.name.replace('.cxx',''), - install_path = None, - #rpath = rpath, - includes = ['inc','test','tests'], - use = test_use + [name]) - diff --git a/waft/wcb.py b/waft/wcb.py index 1a28fc2df..d4740847f 100644 --- a/waft/wcb.py +++ b/waft/wcb.py @@ -1,9 +1,10 @@ # Aggregate all the waftools to make the main wscript a bit shorter. # Note, this is specific to WC building -from . import generic +import generic import os.path as osp from waflib.Utils import to_list +from waflib.Logs import debug, info, error, warn mydir = osp.dirname(__file__) @@ -69,33 +70,31 @@ def find_submodules(ctx): def configure(cfg): - print ('Compile options: %s' % cfg.options.build_debug) + info ('Compile options: %s' % cfg.options.build_debug) cfg.load('boost') cfg.load('smplpkgs') for name, args in package_descriptions: - #print ("Configure: %s %s" % (name, args)) generic._configure(cfg, name, **args) - #print ("configured %s" % name) if getattr(cfg.options, "with_libtorch", False) is False: - print ("sans libtorch") + info ("sans libtorch") else: cfg.load('libtorch') if getattr(cfg.options, "with_cuda", False) is False: - print ("sans CUDA") + info ("sans CUDA") else: cfg.load('cuda') if getattr(cfg.options, "with_kokkos", False) is False: - print ("sans KOKKOS") + info ("sans KOKKOS") else: cfg.load('kokkos') if getattr(cfg.options, "with_root", False) is False: - print ("sans ROOT") + info ("sans ROOT") else: cfg.load('rootsys') @@ -110,9 +109,9 @@ def haveit(one): one=one.upper() if 'LIB_'+one in cfg.env: cfg.env['HAVE_'+one] = 1 - print('HAVE %s libs'%one) + info('HAVE %s libs'%one) else: - print('NO %s libs'%one) + info('NO %s libs'%one) # Check for stuff not found in the wcb-generic way @@ -153,25 +152,33 @@ def haveit(one): if have in cfg.env or have in cfg.env.define_key: continue if pkg in submodules: - print ('Removing package "%s" due to lack of external dependency "%s"'%(pkg,have)) + info('Removing package "%s" due to lack of external dependency "%s"'%(pkg,have)) submodules.remove(pkg) cfg.env.SUBDIRS = submodules - print ('Configured for submodules: %s' % (', '.join(submodules), )) + info ('Configured for submodules: %s' % (', '.join(submodules), )) cfg.write_config_header('config.h') - #print(cfg.env) + + # Define env vars for wire-cell-python CLI's. + # Extend this list manually as more are developed! + for one in to_list("aux gen img pgraph plot pytorch resp sigproc test util validate"): + cmd = 'wirecell-' + one + var = 'WC' + one.upper() + cfg.find_program(cmd, var=var, mandatory=False) + cfg.env.WIRE_CELL_WIRES_UBOONE = "microboone-celltree-wires-v2.1.json.bz2" + cfg.env.WIRE_CELL_WIRES_PDSP = "protodune-wires-larsoft-v4.json.bz2" + + debug(cfg.env) def build(bld): bld.load('smplpkgs') subdirs = bld.env.SUBDIRS - print ('Building: %s' % (', '.join(subdirs), )) + info ('Building: %s' % (', '.join(subdirs), )) bld.recurse(subdirs) if hasattr(bld, "smplpkg_graph"): # fixme: this writes directly. Should make it a task, including # running graphviz to produce PNG/PDF - print ("writing wct-depos.dot") + debug ("writing wct-depos.dot") bld.path.make_node("wct-deps.dot").write(str(bld.smplpkg_graph)) - - diff --git a/wscript b/wscript index f4e34cf51..861317aab 100644 --- a/wscript +++ b/wscript @@ -7,6 +7,11 @@ APPNAME = 'WireCell' VERSION = os.popen("git describe --tags").read().strip() +# to avoid adding tooldir="waft" in all the load()'s +import os +import sys +sys.path.insert(0, os.path.realpath("./waft")) + def options(opt): opt.load("wcb") @@ -44,9 +49,6 @@ int main(int argc,const char *argv[]) mandatory=False) - # fixme: should go into wcb.py - cfg.find_program("jsonnet", var='JSONNET') - # boost 1.59 uses auto_ptr and GCC 5 deprecates it vociferously. cfg.env.CXXFLAGS += ['-Wno-deprecated-declarations'] cfg.env.CXXFLAGS += ['-Wall', '-Wno-unused-local-typedefs', '-Wno-unused-function'] From baa586a5645b65e0c212677d5cb1ff0204474c95 Mon Sep 17 00:00:00 2001 From: Brett Viren Date: Wed, 8 Mar 2023 11:15:16 -0500 Subject: [PATCH 03/32] Prepare to merge master --- waft/rootsys.py | 6 +++--- waft/wcb.py | 26 +++++++++++++++++++++++++- wscript | 3 +++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/waft/rootsys.py b/waft/rootsys.py index d04a0c4a2..a9b30c429 100644 --- a/waft/rootsys.py +++ b/waft/rootsys.py @@ -32,14 +32,14 @@ def check_root(cfg, mandatory=False): kwargs = dict(path_list=path_list) - cfg.find_program('root-config', var='ROOT-CONFIG', mandatory=mandatory, **kwargs) - if not 'ROOT-CONFIG' in cfg.env: + cfg.find_program('root-config', var='ROOT_CONFIG', mandatory=mandatory, **kwargs) + if not 'ROOT_CONFIG' in cfg.env: if mandatory: raise RuntimeError("root-config not found but ROOT required") print ("skipping non mandatory ROOT, use --with-root to force") return - cfg.check_cfg(path=cfg.env['ROOT-CONFIG'], uselib_store='ROOTSYS', + cfg.check_cfg(path=cfg.env['ROOT_CONFIG'], uselib_store='ROOTSYS', args = '--cflags --libs --ldflags', package='', mandatory=mandatory) diff --git a/waft/wcb.py b/waft/wcb.py index d4740847f..85c955ee8 100644 --- a/waft/wcb.py +++ b/waft/wcb.py @@ -5,6 +5,7 @@ import os.path as osp from waflib.Utils import to_list from waflib.Logs import debug, info, error, warn +from waflib.Build import BuildContext mydir = osp.dirname(__file__) @@ -157,7 +158,11 @@ def haveit(one): cfg.env.SUBDIRS = submodules info ('Configured for submodules: %s' % (', '.join(submodules), )) - cfg.write_config_header('config.h') + info ('Configured for submodules: %s' % (', '.join(submodules), )) + bch = 'WireCellUtil/BuildConfig.h' + cfg.env['BuildConfig'] = bch + info ('Writing build config: %s' % bch) + cfg.write_config_header(bch) # Define env vars for wire-cell-python CLI's. # Extend this list manually as more are developed! @@ -173,6 +178,9 @@ def haveit(one): def build(bld): bld.load('smplpkgs') + bch = bld.path.find_or_declare(bld.env.BuildConfig) + bld.install_files('${PREFIX}/include/' + bch.parent.name, bch) + subdirs = bld.env.SUBDIRS info ('Building: %s' % (', '.join(subdirs), )) bld.recurse(subdirs) @@ -182,3 +190,19 @@ def build(bld): # running graphviz to produce PNG/PDF debug ("writing wct-depos.dot") bld.path.make_node("wct-deps.dot").write(str(bld.smplpkg_graph)) + +def dumpenv(bld): + 'print build environment' + for key in bld.env: + val = bld.env[key] + if isinstance(val, list): + val = ' '.join(val) + if "-" in key: + warn("replace '-' with '_' in: %s" % key) + key = key.replace("-","_") + print('%s="%s"' % (key, val)) + + +class DumpenvContext(BuildContext): + cmd = 'dumpenv' + fun = 'dumpenv' diff --git a/wscript b/wscript index 861317aab..f63598060 100644 --- a/wscript +++ b/wscript @@ -69,3 +69,6 @@ int main(int argc,const char *argv[]) def build(bld): bld.load('wcb') + +def dumpenv(bld): + bld.load('wcb') From 361f73e035774bce1f80e41719e81c37f64b51b1 Mon Sep 17 00:00:00 2001 From: Brett Viren Date: Thu, 9 Mar 2023 10:24:30 -0500 Subject: [PATCH 04/32] git subrepo clone (merge) https://github.com/bats-core/bats-core.git test/bats subrepo: subdir: "test/bats" merged: "a7106392" upstream: origin: "https://github.com/bats-core/bats-core.git" branch: "master" commit: "a7106392" git-subrepo: version: "0.4.5" origin: "???" commit: "???" --- test/bats/.devcontainer/Dockerfile | 15 + test/bats/.devcontainer/devcontainer.json | 5 + test/bats/.editorconfig | 35 + test/bats/.gitattributes | 3 + .../bats/.github/ISSUE_TEMPLATE/bug_report.md | 29 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + test/bats/.github/workflows/check_pr_label.sh | 10 + test/bats/.github/workflows/release.yml | 30 + .../.github/workflows/release_dockerhub.yml | 47 + test/bats/.github/workflows/set_nounset.bash | 1 + test/bats/.github/workflows/tests.yml | 265 +++ test/bats/.gitignore | 10 + test/bats/.gitrepo | 12 + test/bats/.readthedocs.yml | 9 + test/bats/AUTHORS | 4 + test/bats/Dockerfile | 43 + test/bats/LICENSE.md | 53 + test/bats/README.md | 127 ++ test/bats/bin/bats | 59 + test/bats/contrib/release.sh | 178 ++ test/bats/contrib/rpm/bats.spec | 66 + test/bats/contrib/semver | 358 ++++ test/bats/docker-compose.override.dist | 8 + test/bats/docker-compose.yml | 13 + test/bats/docker/install_libs.sh | 51 + test/bats/docker/install_tini.sh | 30 + test/bats/docker/tini.pubkey.gpg | 107 ++ test/bats/docs/.markdownlint.json | 3 + test/bats/docs/CHANGELOG.md | 522 ++++++ test/bats/docs/CODEOWNERS | 4 + test/bats/docs/CODE_OF_CONDUCT.md | 92 + test/bats/docs/CONTRIBUTING.md | 392 +++++ test/bats/docs/Makefile | 20 + test/bats/docs/PULL_REQUEST_TEMPLATE.md | 5 + test/bats/docs/examples/README.md | 6 + test/bats/docs/examples/package-tarball | 16 + test/bats/docs/examples/package-tarball.bats | 51 + test/bats/docs/make.bat | 35 + test/bats/docs/releasing.md | 127 ++ test/bats/docs/source/_static/.gitkeep | 0 test/bats/docs/source/_templates/.gitkeep | 0 test/bats/docs/source/conf.py | 72 + test/bats/docs/source/docker-usage.md | 58 + test/bats/docs/source/faq.rst | 170 ++ test/bats/docs/source/gotchas.rst | 132 ++ test/bats/docs/source/index.rst | 18 + test/bats/docs/source/installation.rst | 138 ++ test/bats/docs/source/requirements.txt | 2 + test/bats/docs/source/support-matrix.rst | 26 + test/bats/docs/source/tutorial.rst | 661 +++++++ test/bats/docs/source/usage.md | 114 ++ test/bats/docs/source/warnings/BW01.rst | 17 + test/bats/docs/source/warnings/BW02.rst | 46 + test/bats/docs/source/warnings/BW03.rst | 15 + test/bats/docs/source/warnings/index.rst | 28 + test/bats/docs/source/writing-tests.md | 614 +++++++ test/bats/docs/versions.md | 9 + test/bats/install.sh | 23 + test/bats/lib/bats-core/common.bash | 249 +++ test/bats/lib/bats-core/formatter.bash | 143 ++ test/bats/lib/bats-core/preprocessing.bash | 22 + test/bats/lib/bats-core/semaphore.bash | 113 ++ test/bats/lib/bats-core/test_functions.bash | 371 ++++ test/bats/lib/bats-core/tracing.bash | 399 +++++ test/bats/lib/bats-core/validator.bash | 37 + test/bats/lib/bats-core/warnings.bash | 44 + test/bats/libexec/bats-core/bats | 504 ++++++ test/bats/libexec/bats-core/bats-exec-file | 383 ++++ test/bats/libexec/bats-core/bats-exec-suite | 467 +++++ test/bats/libexec/bats-core/bats-exec-test | 351 ++++ test/bats/libexec/bats-core/bats-format-cat | 6 + test/bats/libexec/bats-core/bats-format-junit | 250 +++ .../bats/libexec/bats-core/bats-format-pretty | 348 ++++ test/bats/libexec/bats-core/bats-format-tap | 55 + test/bats/libexec/bats-core/bats-format-tap13 | 89 + test/bats/libexec/bats-core/bats-preprocess | 119 ++ test/bats/man/Makefile | 23 + test/bats/man/README.md | 5 + test/bats/man/bats.1 | 143 ++ test/bats/man/bats.1.ronn | 192 ++ test/bats/man/bats.7 | 239 +++ test/bats/man/bats.7.ronn | 333 ++++ test/bats/package.json | 34 + test/bats/shellcheck.sh | 22 + test/bats/test/.bats/run-logs/.gitkeep | 0 test/bats/test/bats.bats | 1564 +++++++++++++++++ test/bats/test/common.bats | 204 +++ test/bats/test/concurrent-coordination.bash | 65 + test/bats/test/file_setup_teardown.bats | 198 +++ test/bats/test/fixtures/bats/BATS_TMPDIR.bats | 8 + ...variables_dont_contain_double_slashes.bats | 9 + .../test/fixtures/bats/cmd_using_stdin.bash | 19 + .../test/fixtures/bats/comment_style.bats | 34 + .../fixtures/bats/dos_line_no_shellcheck.bats | 3 + .../bats/duplicate-tests_no_shellcheck.bats | 13 + test/bats/test/fixtures/bats/empty.bats | 1 + test/bats/test/fixtures/bats/environment.bats | 10 + .../fixtures/bats/evaluation_count/file1.bats | 5 + .../fixtures/bats/evaluation_count/file2.bats | 9 + test/bats/test/fixtures/bats/exit_11.bash | 2 + .../bats/expand_var_in_test_name.bats | 3 + .../test/fixtures/bats/exported_function.bats | 8 + .../bats/external_function_calls.bats | 76 + .../fixtures/bats/external_functions.bash | 15 + .../fixtures/bats/external_functions.bats | 5 + test/bats/test/fixtures/bats/failing.bats | 5 + .../fixtures/bats/failing_and_passing.bats | 7 + .../test/fixtures/bats/failing_helper.bats | 6 + .../test/fixtures/bats/failing_setup.bats | 7 + .../test/fixtures/bats/failing_teardown.bats | 7 + .../fixtures/bats/failing_with_bash_cond.bats | 5 + .../bats/failing_with_bash_expression.bats | 4 + .../bats/failing_with_negated_command.bats | 5 + .../fixtures/bats/failure_in_free_code.bats | 9 + test/bats/test/fixtures/bats/focus.bats | 8 + ...obble_up_stdin_sleep_and_print_finish.bash | 4 + .../test/fixtures/bats/hang_after_run.bats | 9 + test/bats/test/fixtures/bats/hang_in_run.bats | 8 + .../fixtures/bats/hang_in_setup_file.bats | 9 + .../test/fixtures/bats/hang_in_teardown.bats | 9 + .../fixtures/bats/hang_in_teardown_file.bats | 9 + .../bats/test/fixtures/bats/hang_in_test.bats | 9 + test/bats/test/fixtures/bats/intact.bats | 6 + test/bats/test/fixtures/bats/issue-205.bats | 103 ++ .../test/fixtures/bats/issue-433/repro1.bats | 7 + .../test/fixtures/bats/issue-433/repro2.bats | 7 + test/bats/test/fixtures/bats/issue-519.bats | 3 + test/bats/test/fixtures/bats/load.bats | 6 + .../test/fixtures/bats/loop_keep_IFS.bats | 16 + .../bats/many_passing_and_one_failing.bats | 15 + .../test/fixtures/bats/no-final-newline.bats | 9 + test/bats/test/fixtures/bats/output.bats | 19 + test/bats/test/fixtures/bats/parallel.bats | 39 + test/bats/test/fixtures/bats/passing.bats | 3 + .../fixtures/bats/passing_and_failing.bats | 7 + .../fixtures/bats/passing_and_skipping.bats | 11 + .../bats/passing_failing_and_skipping.bats | 11 + .../bats/print_output_on_failure.bats | 12 + .../print_output_on_failure_with_stderr.bats | 13 + ...and_unquoted_test_names_no_shellcheck.bats | 11 + .../test/fixtures/bats/read_from_stdin.bats | 21 + .../bats/reference_unset_parameter.bats | 5 + .../reference_unset_parameter_in_setup.bats | 14 + ...reference_unset_parameter_in_teardown.bats | 9 + test/bats/test/fixtures/bats/retry.bats | 38 + .../test/fixtures/bats/retry_success.bats | 6 + .../test/fixtures/bats/run_long_command.bats | 3 + .../bats/set_-eu_in_setup_and_teardown.bats | 23 + test/bats/test/fixtures/bats/setup.bats | 17 + .../bats/show-output-of-passing-tests.bats | 3 + .../fixtures/bats/sigint_in_failing_test.bats | 11 + .../bats/single_line_no_shellcheck.bats | 9 + test/bats/test/fixtures/bats/skipped.bats | 7 + .../fixtures/bats/skipped_with_parens.bats | 3 + .../bats/source_nonexistent_file.bats | 4 + .../source_nonexistent_file_in_setup.bats | 13 + .../source_nonexistent_file_in_teardown.bats | 8 + .../test/fixtures/bats/tab in filename.bats | 3 + test/bats/test/fixtures/bats/teardown.bats | 17 + .../bats/teardown_file_override_status.bats | 9 + .../bats/teardown_override_status.bats | 8 + .../setup_suite.bash | 9 + .../teardown_suite_override_status/test.bats | 3 + test/bats/test/fixtures/bats/test_helper.bash | 19 + .../test/fixtures/bats/unbound_variable.bats | 16 + .../bats/unofficial_bash_strict_mode.bash | 3 + .../bats/unofficial_bash_strict_mode.bats | 4 + .../test/fixtures/bats/update_path_env.bats | 6 + test/bats/test/fixtures/bats/verbose-run.bats | 4 + .../bats/whitespace_no_shellcheck.bats | 33 + .../bats/without_trailing_newline.bats | 3 + .../error_in_setup_and_teardown_file.bats | 11 + .../file_setup_teardown/no_setup_file.bats | 3 + .../file_setup_teardown/no_teardown_file.bats | 3 + .../file_setup_teardown/setup_file.bats | 11 + .../file_setup_teardown/setup_file2.bats | 7 + .../setup_file_does_not_leak_env.bats | 7 + .../setup_file_does_not_leak_env2.bats | 3 + ...up_file_even_if_all_tests_are_skipped.bats | 7 + .../setup_file_failed.bats | 11 + .../setup_file_halfway_error.bats | 9 + .../file_setup_teardown/teardown_file.bats | 11 + .../file_setup_teardown/teardown_file2.bats | 11 + .../teardown_file_after_failing_test.bats | 7 + .../teardown_file_after_long_test.bats | 8 + .../teardown_file_does_not_leak.bats | 7 + .../teardown_file_does_not_leak2.bats | 3 + ...wn_file_even_if_all_tests_are_skipped.bats | 7 + .../teardown_file_failed.bats | 7 + .../teardown_file_halfway_error.bats | 9 + .../test/fixtures/formatter/dummy-formatter | 5 + .../bats/test/fixtures/formatter/failing.bats | 5 + .../bats/test/fixtures/formatter/passing.bats | 3 + .../formatter/passing_and_skipping.bats | 11 + .../passing_failing_and_skipping.bats | 11 + .../formatter/skipped_with_parens.bats | 3 + .../duplicate/first/file1.bats | 3 + .../duplicate/second/file1.bats | 3 + .../fixtures/junit-formatter/issue_360.bats | 22 + .../fixtures/junit-formatter/issue_531.bats | 16 + .../fixtures/junit-formatter/skipped.bats | 9 + .../fixtures/junit-formatter/suite/file1.bats | 3 + .../fixtures/junit-formatter/suite/file2.bats | 3 + .../fixtures/junit-formatter/xml-escape.bats | 11 + test/bats/test/fixtures/load/ambiguous | 5 + test/bats/test/fixtures/load/ambiguous.bash | 6 + .../test/fixtures/load/bats_load_library.bats | 6 + test/bats/test/fixtures/load/exit1.bash | 1 + .../load/failing_bats_load_library.bats | 5 + .../bats/test/fixtures/load/failing_load.bats | 5 + .../fixtures/load/find_library_helper.bats | 5 + .../load/find_library_helper_err.bats | 4 + test/bats/test/fixtures/load/load.bats | 6 + .../load/load_in_teardown_after_failure.bats | 7 + test/bats/test/fixtures/load/return1.bash | 1 + test/bats/test/fixtures/load/test_helper.bash | 19 + .../file1.bats | 14 + .../file2.bats | 19 + .../must_not_parallelize_within_file.bats | 52 + .../parallel-preserve-environment.bats | 8 + .../bats/test/fixtures/parallel/parallel.bats | 20 + .../fixtures/parallel/parallel_factor.bats | 48 + .../parallel/setup_file/setup_file.bats | 13 + .../parallel/setup_file/setup_file1.bats | 1 + .../parallel/setup_file/setup_file2.bats | 1 + .../parallel/setup_file/setup_file3.bats | 1 + .../fixtures/parallel/suite/parallel1.bats | 20 + .../fixtures/parallel/suite/parallel2.bats | 1 + .../fixtures/parallel/suite/parallel3.bats | 1 + .../fixtures/parallel/suite/parallel4.bats | 1 + test/bats/test/fixtures/run/failing.bats | 23 + test/bats/test/fixtures/run/invalid.bats | 7 + test/bats/test/fixtures/suite/empty/.gitkeep | 0 test/bats/test/fixtures/suite/filter/a.bats | 3 + test/bats/test/fixtures/suite/filter/b.bats | 3 + test/bats/test/fixtures/suite/filter/c.bats | 3 + test/bats/test/fixtures/suite/multiple/a.bats | 3 + test/bats/test/fixtures/suite/multiple/b.bats | 7 + .../subfolder/test.other_extension | 3 + .../override_BATS_FILE_EXTENSION/test.bats | 3 + .../override_BATS_FILE_EXTENSION/test.test | 3 + .../suite/recursive/subsuite/test2.bats | 3 + .../test/fixtures/suite/recursive/test.bats | 3 + .../suite/recursive_with_symlinks/subsuite | 1 + .../suite/recursive_with_symlinks/test.bats | 1 + .../bats/test/fixtures/suite/single/test.bats | 3 + .../skip/skip-in-setup-and-teardown.bats | 17 + .../fixtures/suite/skip/skip-in-setup.bats | 13 + .../fixtures/suite/skip/skip-in-teardown.bats | 9 + .../suite/skip/skip-in-test-and-teardown.bats | 9 + .../fixtures/suite/skip/skip-in-test.bats | 5 + .../fixtures/suite/test_number/file1.bats | 18 + .../fixtures/suite/test_number/file2.bats | 24 + .../call_load/setup_suite.bash | 3 + .../suite_setup_teardown/call_load/test.bats | 3 + .../call_load/test_helper.bash | 1 + .../default_name/setup_suite.bash | 7 + .../default_name/test.bats | 3 + .../error_in_free_code/setup_suite.bash | 5 + .../error_in_free_code/test.bats | 1 + .../error_in_setup_suite/setup_suite.bash | 3 + .../error_in_setup_suite/test.bats | 1 + .../error_in_teardown_suite/setup_suite.bash | 7 + .../error_in_teardown_suite/test.bats | 1 + .../exported_vars/setup_suite.bash | 3 + .../exported_vars/test.bats | 11 + .../failure_in_setup_suite/setup_suite.bash | 9 + .../failure_in_setup_suite/test.bats | 1 + .../setup_suite.bash | 9 + .../failure_in_teardown_suite/test.bats | 1 + .../no_failure_no_output/setup_suite.bash | 7 + .../no_failure_no_output/test.bats | 3 + .../no_setup_suite_function/setup_suite.bash | 3 + .../no_setup_suite_function/test.bats | 3 + .../setup_suite_non_default.bash | 7 + .../non_default_name/test.bats | 3 + .../output_with_failure/setup_suite.bash | 8 + .../output_with_failure/test.bats | 3 + .../pick_up_toplevel/folder1/setup_suite.bash | 1 + .../pick_up_toplevel/folder1/test.bats | 3 + .../pick_up_toplevel/folder2/setup_suite.bash | 1 + .../pick_up_toplevel/folder2/test.bats | 3 + .../pick_up_toplevel/setup_suite.bash | 7 + .../pick_up_toplevel/test.bats | 3 + .../setup_suite.bash | 10 + .../test.bats | 1 + .../skip_in_setup_file.bats | 11 + .../setup_suite.bash | 10 + .../stderr_in_setup_teardown_suite/test.bats | 3 + .../syntax_error/setup_suite_no_shellcheck | 1 + .../syntax_error/test.bats | 1 + .../test/fixtures/tagging/BATS_TEST_TAGS.bats | 26 + .../test/fixtures/tagging/invalid_tags.bats | 8 + .../fixtures/tagging/print_tags_on_error.bats | 5 + test/bats/test/fixtures/tagging/tagged.bats | 25 + test/bats/test/fixtures/tagging/trimming.bats | 5 + test/bats/test/fixtures/timeout/sleep2.bats | 3 + .../test/fixtures/trace/failing_complex.bats | 5 + .../fixtures/trace/failing_recursive.bats | 13 + test/bats/test/fixtures/warnings/BW01.bats | 5 + .../warnings/BW01_check_exit_code_is_127.bats | 4 + ...1_no_exit_code_check_no_exit_code_127.bats | 3 + test/bats/test/fixtures/warnings/BW02.bats | 3 + .../define_setup_suite_in_wrong_file.bats | 7 + .../BW03/non_default_setup_suite.bash | 3 + .../warnings/BW03/suppress_warning.bats | 10 + test/bats/test/formatter.bats | 101 ++ test/bats/test/install.bats | 151 ++ test/bats/test/junit-formatter.bats | 160 ++ test/bats/test/load.bats | 190 ++ test/bats/test/parallel.bats | 227 +++ test/bats/test/pretty-formatter.bats | 69 + test/bats/test/root.bats | 102 ++ test/bats/test/run.bats | 139 ++ test/bats/test/suite.bats | 209 +++ test/bats/test/suite_setup_teardown.bats | 153 ++ test/bats/test/tagging.bats | 85 + test/bats/test/tap13-formatter.bats | 53 + test/bats/test/test_helper.bash | 70 + test/bats/test/timeout.bats | 29 + test/bats/test/trace.bats | 84 + test/bats/test/warnings.bats | 81 + test/bats/uninstall.sh | 39 + 323 files changed, 15857 insertions(+) create mode 100644 test/bats/.devcontainer/Dockerfile create mode 100644 test/bats/.devcontainer/devcontainer.json create mode 100644 test/bats/.editorconfig create mode 100755 test/bats/.gitattributes create mode 100644 test/bats/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 test/bats/.github/ISSUE_TEMPLATE/feature_request.md create mode 100755 test/bats/.github/workflows/check_pr_label.sh create mode 100644 test/bats/.github/workflows/release.yml create mode 100644 test/bats/.github/workflows/release_dockerhub.yml create mode 100644 test/bats/.github/workflows/set_nounset.bash create mode 100644 test/bats/.github/workflows/tests.yml create mode 100644 test/bats/.gitignore create mode 100644 test/bats/.gitrepo create mode 100644 test/bats/.readthedocs.yml create mode 100644 test/bats/AUTHORS create mode 100644 test/bats/Dockerfile create mode 100644 test/bats/LICENSE.md create mode 100644 test/bats/README.md create mode 100755 test/bats/bin/bats create mode 100755 test/bats/contrib/release.sh create mode 100644 test/bats/contrib/rpm/bats.spec create mode 100755 test/bats/contrib/semver create mode 100644 test/bats/docker-compose.override.dist create mode 100644 test/bats/docker-compose.yml create mode 100755 test/bats/docker/install_libs.sh create mode 100755 test/bats/docker/install_tini.sh create mode 100644 test/bats/docker/tini.pubkey.gpg create mode 100644 test/bats/docs/.markdownlint.json create mode 100644 test/bats/docs/CHANGELOG.md create mode 100644 test/bats/docs/CODEOWNERS create mode 100644 test/bats/docs/CODE_OF_CONDUCT.md create mode 100644 test/bats/docs/CONTRIBUTING.md create mode 100644 test/bats/docs/Makefile create mode 100644 test/bats/docs/PULL_REQUEST_TEMPLATE.md create mode 100644 test/bats/docs/examples/README.md create mode 100644 test/bats/docs/examples/package-tarball create mode 100755 test/bats/docs/examples/package-tarball.bats create mode 100644 test/bats/docs/make.bat create mode 100644 test/bats/docs/releasing.md create mode 100644 test/bats/docs/source/_static/.gitkeep create mode 100644 test/bats/docs/source/_templates/.gitkeep create mode 100644 test/bats/docs/source/conf.py create mode 100644 test/bats/docs/source/docker-usage.md create mode 100644 test/bats/docs/source/faq.rst create mode 100644 test/bats/docs/source/gotchas.rst create mode 100644 test/bats/docs/source/index.rst create mode 100644 test/bats/docs/source/installation.rst create mode 100644 test/bats/docs/source/requirements.txt create mode 100644 test/bats/docs/source/support-matrix.rst create mode 100644 test/bats/docs/source/tutorial.rst create mode 100644 test/bats/docs/source/usage.md create mode 100644 test/bats/docs/source/warnings/BW01.rst create mode 100644 test/bats/docs/source/warnings/BW02.rst create mode 100644 test/bats/docs/source/warnings/BW03.rst create mode 100644 test/bats/docs/source/warnings/index.rst create mode 100644 test/bats/docs/source/writing-tests.md create mode 100644 test/bats/docs/versions.md create mode 100755 test/bats/install.sh create mode 100644 test/bats/lib/bats-core/common.bash create mode 100644 test/bats/lib/bats-core/formatter.bash create mode 100644 test/bats/lib/bats-core/preprocessing.bash create mode 100644 test/bats/lib/bats-core/semaphore.bash create mode 100644 test/bats/lib/bats-core/test_functions.bash create mode 100644 test/bats/lib/bats-core/tracing.bash create mode 100644 test/bats/lib/bats-core/validator.bash create mode 100644 test/bats/lib/bats-core/warnings.bash create mode 100755 test/bats/libexec/bats-core/bats create mode 100755 test/bats/libexec/bats-core/bats-exec-file create mode 100755 test/bats/libexec/bats-core/bats-exec-suite create mode 100755 test/bats/libexec/bats-core/bats-exec-test create mode 100755 test/bats/libexec/bats-core/bats-format-cat create mode 100755 test/bats/libexec/bats-core/bats-format-junit create mode 100755 test/bats/libexec/bats-core/bats-format-pretty create mode 100755 test/bats/libexec/bats-core/bats-format-tap create mode 100755 test/bats/libexec/bats-core/bats-format-tap13 create mode 100755 test/bats/libexec/bats-core/bats-preprocess create mode 100644 test/bats/man/Makefile create mode 100644 test/bats/man/README.md create mode 100644 test/bats/man/bats.1 create mode 100644 test/bats/man/bats.1.ronn create mode 100644 test/bats/man/bats.7 create mode 100644 test/bats/man/bats.7.ronn create mode 100644 test/bats/package.json create mode 100755 test/bats/shellcheck.sh create mode 100644 test/bats/test/.bats/run-logs/.gitkeep create mode 100755 test/bats/test/bats.bats create mode 100644 test/bats/test/common.bats create mode 100644 test/bats/test/concurrent-coordination.bash create mode 100644 test/bats/test/file_setup_teardown.bats create mode 100755 test/bats/test/fixtures/bats/BATS_TMPDIR.bats create mode 100644 test/bats/test/fixtures/bats/BATS_variables_dont_contain_double_slashes.bats create mode 100755 test/bats/test/fixtures/bats/cmd_using_stdin.bash create mode 100644 test/bats/test/fixtures/bats/comment_style.bats create mode 100644 test/bats/test/fixtures/bats/dos_line_no_shellcheck.bats create mode 100644 test/bats/test/fixtures/bats/duplicate-tests_no_shellcheck.bats create mode 100644 test/bats/test/fixtures/bats/empty.bats create mode 100644 test/bats/test/fixtures/bats/environment.bats create mode 100644 test/bats/test/fixtures/bats/evaluation_count/file1.bats create mode 100644 test/bats/test/fixtures/bats/evaluation_count/file2.bats create mode 100755 test/bats/test/fixtures/bats/exit_11.bash create mode 100644 test/bats/test/fixtures/bats/expand_var_in_test_name.bats create mode 100644 test/bats/test/fixtures/bats/exported_function.bats create mode 100644 test/bats/test/fixtures/bats/external_function_calls.bats create mode 100644 test/bats/test/fixtures/bats/external_functions.bash create mode 100644 test/bats/test/fixtures/bats/external_functions.bats create mode 100644 test/bats/test/fixtures/bats/failing.bats create mode 100644 test/bats/test/fixtures/bats/failing_and_passing.bats create mode 100644 test/bats/test/fixtures/bats/failing_helper.bats create mode 100644 test/bats/test/fixtures/bats/failing_setup.bats create mode 100644 test/bats/test/fixtures/bats/failing_teardown.bats create mode 100644 test/bats/test/fixtures/bats/failing_with_bash_cond.bats create mode 100644 test/bats/test/fixtures/bats/failing_with_bash_expression.bats create mode 100644 test/bats/test/fixtures/bats/failing_with_negated_command.bats create mode 100644 test/bats/test/fixtures/bats/failure_in_free_code.bats create mode 100644 test/bats/test/fixtures/bats/focus.bats create mode 100755 test/bats/test/fixtures/bats/gobble_up_stdin_sleep_and_print_finish.bash create mode 100644 test/bats/test/fixtures/bats/hang_after_run.bats create mode 100644 test/bats/test/fixtures/bats/hang_in_run.bats create mode 100644 test/bats/test/fixtures/bats/hang_in_setup_file.bats create mode 100644 test/bats/test/fixtures/bats/hang_in_teardown.bats create mode 100644 test/bats/test/fixtures/bats/hang_in_teardown_file.bats create mode 100644 test/bats/test/fixtures/bats/hang_in_test.bats create mode 100644 test/bats/test/fixtures/bats/intact.bats create mode 100644 test/bats/test/fixtures/bats/issue-205.bats create mode 100644 test/bats/test/fixtures/bats/issue-433/repro1.bats create mode 100644 test/bats/test/fixtures/bats/issue-433/repro2.bats create mode 100644 test/bats/test/fixtures/bats/issue-519.bats create mode 100644 test/bats/test/fixtures/bats/load.bats create mode 100644 test/bats/test/fixtures/bats/loop_keep_IFS.bats create mode 100644 test/bats/test/fixtures/bats/many_passing_and_one_failing.bats create mode 100644 test/bats/test/fixtures/bats/no-final-newline.bats create mode 100644 test/bats/test/fixtures/bats/output.bats create mode 100644 test/bats/test/fixtures/bats/parallel.bats create mode 100644 test/bats/test/fixtures/bats/passing.bats create mode 100644 test/bats/test/fixtures/bats/passing_and_failing.bats create mode 100644 test/bats/test/fixtures/bats/passing_and_skipping.bats create mode 100644 test/bats/test/fixtures/bats/passing_failing_and_skipping.bats create mode 100644 test/bats/test/fixtures/bats/print_output_on_failure.bats create mode 100644 test/bats/test/fixtures/bats/print_output_on_failure_with_stderr.bats create mode 100644 test/bats/test/fixtures/bats/quoted_and_unquoted_test_names_no_shellcheck.bats create mode 100644 test/bats/test/fixtures/bats/read_from_stdin.bats create mode 100644 test/bats/test/fixtures/bats/reference_unset_parameter.bats create mode 100644 test/bats/test/fixtures/bats/reference_unset_parameter_in_setup.bats create mode 100644 test/bats/test/fixtures/bats/reference_unset_parameter_in_teardown.bats create mode 100644 test/bats/test/fixtures/bats/retry.bats create mode 100644 test/bats/test/fixtures/bats/retry_success.bats create mode 100644 test/bats/test/fixtures/bats/run_long_command.bats create mode 100644 test/bats/test/fixtures/bats/set_-eu_in_setup_and_teardown.bats create mode 100644 test/bats/test/fixtures/bats/setup.bats create mode 100644 test/bats/test/fixtures/bats/show-output-of-passing-tests.bats create mode 100644 test/bats/test/fixtures/bats/sigint_in_failing_test.bats create mode 100644 test/bats/test/fixtures/bats/single_line_no_shellcheck.bats create mode 100644 test/bats/test/fixtures/bats/skipped.bats create mode 100644 test/bats/test/fixtures/bats/skipped_with_parens.bats create mode 100644 test/bats/test/fixtures/bats/source_nonexistent_file.bats create mode 100644 test/bats/test/fixtures/bats/source_nonexistent_file_in_setup.bats create mode 100644 test/bats/test/fixtures/bats/source_nonexistent_file_in_teardown.bats create mode 100644 test/bats/test/fixtures/bats/tab in filename.bats create mode 100644 test/bats/test/fixtures/bats/teardown.bats create mode 100644 test/bats/test/fixtures/bats/teardown_file_override_status.bats create mode 100644 test/bats/test/fixtures/bats/teardown_override_status.bats create mode 100644 test/bats/test/fixtures/bats/teardown_suite_override_status/setup_suite.bash create mode 100644 test/bats/test/fixtures/bats/teardown_suite_override_status/test.bats create mode 100644 test/bats/test/fixtures/bats/test_helper.bash create mode 100644 test/bats/test/fixtures/bats/unbound_variable.bats create mode 100644 test/bats/test/fixtures/bats/unofficial_bash_strict_mode.bash create mode 100644 test/bats/test/fixtures/bats/unofficial_bash_strict_mode.bats create mode 100644 test/bats/test/fixtures/bats/update_path_env.bats create mode 100644 test/bats/test/fixtures/bats/verbose-run.bats create mode 100644 test/bats/test/fixtures/bats/whitespace_no_shellcheck.bats create mode 100644 test/bats/test/fixtures/bats/without_trailing_newline.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/error_in_setup_and_teardown_file.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/no_setup_file.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/no_teardown_file.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file2.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file_does_not_leak_env.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file_does_not_leak_env2.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file_even_if_all_tests_are_skipped.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file_failed.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/setup_file_halfway_error.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file2.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_after_failing_test.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_after_long_test.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_does_not_leak.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_does_not_leak2.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_even_if_all_tests_are_skipped.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_failed.bats create mode 100644 test/bats/test/fixtures/file_setup_teardown/teardown_file_halfway_error.bats create mode 100755 test/bats/test/fixtures/formatter/dummy-formatter create mode 100644 test/bats/test/fixtures/formatter/failing.bats create mode 100644 test/bats/test/fixtures/formatter/passing.bats create mode 100644 test/bats/test/fixtures/formatter/passing_and_skipping.bats create mode 100644 test/bats/test/fixtures/formatter/passing_failing_and_skipping.bats create mode 100644 test/bats/test/fixtures/formatter/skipped_with_parens.bats create mode 100644 test/bats/test/fixtures/junit-formatter/duplicate/first/file1.bats create mode 100644 test/bats/test/fixtures/junit-formatter/duplicate/second/file1.bats create mode 100644 test/bats/test/fixtures/junit-formatter/issue_360.bats create mode 100644 test/bats/test/fixtures/junit-formatter/issue_531.bats create mode 100644 test/bats/test/fixtures/junit-formatter/skipped.bats create mode 100644 test/bats/test/fixtures/junit-formatter/suite/file1.bats create mode 100644 test/bats/test/fixtures/junit-formatter/suite/file2.bats create mode 100644 test/bats/test/fixtures/junit-formatter/xml-escape.bats create mode 100644 test/bats/test/fixtures/load/ambiguous create mode 100644 test/bats/test/fixtures/load/ambiguous.bash create mode 100644 test/bats/test/fixtures/load/bats_load_library.bats create mode 100644 test/bats/test/fixtures/load/exit1.bash create mode 100644 test/bats/test/fixtures/load/failing_bats_load_library.bats create mode 100644 test/bats/test/fixtures/load/failing_load.bats create mode 100644 test/bats/test/fixtures/load/find_library_helper.bats create mode 100644 test/bats/test/fixtures/load/find_library_helper_err.bats create mode 100644 test/bats/test/fixtures/load/load.bats create mode 100644 test/bats/test/fixtures/load/load_in_teardown_after_failure.bats create mode 100644 test/bats/test/fixtures/load/return1.bash create mode 100644 test/bats/test/fixtures/load/test_helper.bash create mode 100644 test/bats/test/fixtures/parallel/must_not_parallelize_across_files/file1.bats create mode 100644 test/bats/test/fixtures/parallel/must_not_parallelize_across_files/file2.bats create mode 100644 test/bats/test/fixtures/parallel/must_not_parallelize_within_file.bats create mode 100644 test/bats/test/fixtures/parallel/parallel-preserve-environment.bats create mode 100644 test/bats/test/fixtures/parallel/parallel.bats create mode 100644 test/bats/test/fixtures/parallel/parallel_factor.bats create mode 100644 test/bats/test/fixtures/parallel/setup_file/setup_file.bats create mode 120000 test/bats/test/fixtures/parallel/setup_file/setup_file1.bats create mode 120000 test/bats/test/fixtures/parallel/setup_file/setup_file2.bats create mode 120000 test/bats/test/fixtures/parallel/setup_file/setup_file3.bats create mode 100644 test/bats/test/fixtures/parallel/suite/parallel1.bats create mode 120000 test/bats/test/fixtures/parallel/suite/parallel2.bats create mode 120000 test/bats/test/fixtures/parallel/suite/parallel3.bats create mode 120000 test/bats/test/fixtures/parallel/suite/parallel4.bats create mode 100644 test/bats/test/fixtures/run/failing.bats create mode 100644 test/bats/test/fixtures/run/invalid.bats create mode 100644 test/bats/test/fixtures/suite/empty/.gitkeep create mode 100644 test/bats/test/fixtures/suite/filter/a.bats create mode 100644 test/bats/test/fixtures/suite/filter/b.bats create mode 100644 test/bats/test/fixtures/suite/filter/c.bats create mode 100644 test/bats/test/fixtures/suite/multiple/a.bats create mode 100644 test/bats/test/fixtures/suite/multiple/b.bats create mode 100644 test/bats/test/fixtures/suite/override_BATS_FILE_EXTENSION/subfolder/test.other_extension create mode 100644 test/bats/test/fixtures/suite/override_BATS_FILE_EXTENSION/test.bats create mode 100644 test/bats/test/fixtures/suite/override_BATS_FILE_EXTENSION/test.test create mode 100644 test/bats/test/fixtures/suite/recursive/subsuite/test2.bats create mode 100644 test/bats/test/fixtures/suite/recursive/test.bats create mode 120000 test/bats/test/fixtures/suite/recursive_with_symlinks/subsuite create mode 120000 test/bats/test/fixtures/suite/recursive_with_symlinks/test.bats create mode 100644 test/bats/test/fixtures/suite/single/test.bats create mode 100644 test/bats/test/fixtures/suite/skip/skip-in-setup-and-teardown.bats create mode 100644 test/bats/test/fixtures/suite/skip/skip-in-setup.bats create mode 100644 test/bats/test/fixtures/suite/skip/skip-in-teardown.bats create mode 100644 test/bats/test/fixtures/suite/skip/skip-in-test-and-teardown.bats create mode 100644 test/bats/test/fixtures/suite/skip/skip-in-test.bats create mode 100644 test/bats/test/fixtures/suite/test_number/file1.bats create mode 100644 test/bats/test/fixtures/suite/test_number/file2.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/call_load/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/call_load/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/call_load/test_helper.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/default_name/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/default_name/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_free_code/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_free_code/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_setup_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_setup_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_teardown_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/error_in_teardown_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/exported_vars/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/exported_vars/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/failure_in_setup_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/failure_in_setup_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/failure_in_teardown_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/failure_in_teardown_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/no_failure_no_output/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/no_failure_no_output/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/no_setup_suite_function/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/no_setup_suite_function/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/non_default_name/setup_suite_non_default.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/non_default_name/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/output_with_failure/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/output_with_failure/test.bats create mode 120000 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/folder1/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/folder1/test.bats create mode 120000 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/folder2/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/folder2/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/pick_up_toplevel/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/return_nonzero_in_teardown_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/return_nonzero_in_teardown_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/skip_in_setup_file.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/stderr_in_setup_teardown_suite/setup_suite.bash create mode 100644 test/bats/test/fixtures/suite_setup_teardown/stderr_in_setup_teardown_suite/test.bats create mode 100644 test/bats/test/fixtures/suite_setup_teardown/syntax_error/setup_suite_no_shellcheck create mode 100644 test/bats/test/fixtures/suite_setup_teardown/syntax_error/test.bats create mode 100644 test/bats/test/fixtures/tagging/BATS_TEST_TAGS.bats create mode 100644 test/bats/test/fixtures/tagging/invalid_tags.bats create mode 100644 test/bats/test/fixtures/tagging/print_tags_on_error.bats create mode 100644 test/bats/test/fixtures/tagging/tagged.bats create mode 100644 test/bats/test/fixtures/tagging/trimming.bats create mode 100644 test/bats/test/fixtures/timeout/sleep2.bats create mode 100644 test/bats/test/fixtures/trace/failing_complex.bats create mode 100644 test/bats/test/fixtures/trace/failing_recursive.bats create mode 100644 test/bats/test/fixtures/warnings/BW01.bats create mode 100644 test/bats/test/fixtures/warnings/BW01_check_exit_code_is_127.bats create mode 100644 test/bats/test/fixtures/warnings/BW01_no_exit_code_check_no_exit_code_127.bats create mode 100644 test/bats/test/fixtures/warnings/BW02.bats create mode 100644 test/bats/test/fixtures/warnings/BW03/define_setup_suite_in_wrong_file.bats create mode 100644 test/bats/test/fixtures/warnings/BW03/non_default_setup_suite.bash create mode 100644 test/bats/test/fixtures/warnings/BW03/suppress_warning.bats create mode 100644 test/bats/test/formatter.bats create mode 100644 test/bats/test/install.bats create mode 100644 test/bats/test/junit-formatter.bats create mode 100644 test/bats/test/load.bats create mode 100644 test/bats/test/parallel.bats create mode 100644 test/bats/test/pretty-formatter.bats create mode 100644 test/bats/test/root.bats create mode 100644 test/bats/test/run.bats create mode 100755 test/bats/test/suite.bats create mode 100644 test/bats/test/suite_setup_teardown.bats create mode 100644 test/bats/test/tagging.bats create mode 100644 test/bats/test/tap13-formatter.bats create mode 100644 test/bats/test/test_helper.bash create mode 100644 test/bats/test/timeout.bats create mode 100644 test/bats/test/trace.bats create mode 100644 test/bats/test/warnings.bats create mode 100755 test/bats/uninstall.sh diff --git a/test/bats/.devcontainer/Dockerfile b/test/bats/.devcontainer/Dockerfile new file mode 100644 index 000000000..f3c3ed9a6 --- /dev/null +++ b/test/bats/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +ARG bashver=latest + +FROM bash:${bashver} + +# Install parallel and accept the citation notice (we aren't using this in a +# context where it make sense to cite GNU Parallel). +RUN echo "@edgecomm http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ + apk update && \ + apk add --no-cache parallel ncurses shellcheck@edgecomm && \ + mkdir -p ~/.parallel && touch ~/.parallel/will-cite + +RUN ln -s /opt/bats/bin/bats /usr/sbin/bats +COPY . /opt/bats/ + +ENTRYPOINT ["bash", "/usr/sbin/bats"] diff --git a/test/bats/.devcontainer/devcontainer.json b/test/bats/.devcontainer/devcontainer.json new file mode 100644 index 000000000..2b81e3f20 --- /dev/null +++ b/test/bats/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "name": "Bats core development environment", + "dockerFile": "Dockerfile", + "build": {"args": {"bashver": "4.3"}} +} \ No newline at end of file diff --git a/test/bats/.editorconfig b/test/bats/.editorconfig new file mode 100644 index 000000000..457107ead --- /dev/null +++ b/test/bats/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +# The JSON files contain newlines inconsistently +[*.json] +indent_size = 2 +insert_final_newline = ignore + +# YAML +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# Makefiles always use tabs for recipe indentation +[{Makefile,*.mak}] +indent_style = tab + +# Markdown +[*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}] +max_line_length = 80 +# tabs behave as if they were replaced by spaces with a tab stop of 4 characters +tab_width = 4 +# trailing spaces indicates word wrap +trim_trailing_spaces = false +trim_trailing_whitespace = false + +[test/fixtures/bats/*_no_shellcheck.bats] +ignore = true \ No newline at end of file diff --git a/test/bats/.gitattributes b/test/bats/.gitattributes new file mode 100755 index 000000000..20cad1f8b --- /dev/null +++ b/test/bats/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.sh eol=lf +libexec/* eol=lf diff --git a/test/bats/.github/ISSUE_TEMPLATE/bug_report.md b/test/bats/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..8c6ea2148 --- /dev/null +++ b/test/bats/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'Priority: NeedsTriage, Type: Bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - Bats Version [e.g. 1.4.0 or commit hash] + - OS: [e.g. Linux, FreeBSD, MacOS] + - Bash version: [e.g. 5.1] + +**Additional context** +Add any other context about the problem here. diff --git a/test/bats/.github/ISSUE_TEMPLATE/feature_request.md b/test/bats/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..506b98beb --- /dev/null +++ b/test/bats/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'Priority: NeedsTriage, Type: Enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/test/bats/.github/workflows/check_pr_label.sh b/test/bats/.github/workflows/check_pr_label.sh new file mode 100755 index 000000000..2ff5723c7 --- /dev/null +++ b/test/bats/.github/workflows/check_pr_label.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash + +get_pr_json() { + curl -s -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/bats-core/bats-core/pulls/$1" +} + +PR_NUMBER="$1" +LABEL="$2" + +get_pr_json "$PR_NUMBER" | jq .labels[].name | grep "$LABEL" diff --git a/test/bats/.github/workflows/release.yml b/test/bats/.github/workflows/release.yml new file mode 100644 index 000000000..b38f8201d --- /dev/null +++ b/test/bats/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release + +on: + release: { types: [published] } + workflow_dispatch: + +jobs: + npmjs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + registry-url: "https://registry.npmjs.org" + - run: npm publish --ignore-scripts + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + github-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + registry-url: "https://npm.pkg.github.com" + - name: scope package name as required by GitHub Packages + run: npm init -y --scope ${{ github.repository_owner }} + - run: npm publish --ignore-scripts + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/test/bats/.github/workflows/release_dockerhub.yml b/test/bats/.github/workflows/release_dockerhub.yml new file mode 100644 index 000000000..e5faec1c8 --- /dev/null +++ b/test/bats/.github/workflows/release_dockerhub.yml @@ -0,0 +1,47 @@ +name: Release to docker hub + +on: + release: { types: [published] } + workflow_dispatch: + inputs: + version: + description: 'Version to simulate for deploy' + required: true + +jobs: + dockerhub: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - id: version + run: | + EXPECTED_VERSION=${{ github.event.inputs.version }} + TAG_VERSION=${GITHUB_REF#refs/tags/v} # refs/tags/v1.2.3 -> 1.2.3 + echo ::set-output name=version::${EXPECTED_VERSION:-$TAG_VERSION} + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - uses: docker/build-push-action@v2 + with: + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6 + tags: ${{ secrets.DOCKER_USERNAME }}/bats:${{ steps.version.outputs.version }},${{ secrets.DOCKER_USERNAME }}/bats:latest + push: true + + - uses: docker/build-push-action@v2 + with: + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6 + tags: ${{ secrets.DOCKER_USERNAME }}/bats:${{ steps.version.outputs.version }}-no-faccessat2,${{ secrets.DOCKER_USERNAME }}/bats:latest-no-faccessat2 + push: true + build-args: bashver=5.1.4 diff --git a/test/bats/.github/workflows/set_nounset.bash b/test/bats/.github/workflows/set_nounset.bash new file mode 100644 index 000000000..8925c2d81 --- /dev/null +++ b/test/bats/.github/workflows/set_nounset.bash @@ -0,0 +1 @@ +set -u diff --git a/test/bats/.github/workflows/tests.yml b/test/bats/.github/workflows/tests.yml new file mode 100644 index 000000000..448c9bdcb --- /dev/null +++ b/test/bats/.github/workflows/tests.yml @@ -0,0 +1,265 @@ +name: Tests + +# Controls when the action will run. +on: [push, pull_request, workflow_dispatch] + +jobs: + changelog: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Check that PR is mentioned in Changelog + run: | + if ! ./.github/workflows/check_pr_label.sh "${{github.event.pull_request.number}}" "no changelog"; then + grep "#${{github.event.pull_request.number}}" docs/CHANGELOG.md + fi + if: ${{github.event.pull_request}} + + shfmt: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - run: | + curl https://github.com/mvdan/sh/releases/download/v3.5.1/shfmt_v3.5.1_linux_amd64 -o shfmt + chmod a+x shfmt + - run: ./shfmt --diff . + + shellcheck: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Run shellcheck + run: | + sudo apt-get update -y + sudo apt-get install shellcheck + ./shellcheck.sh + + linux: + strategy: + matrix: + os: ['ubuntu-20.04', 'ubuntu-22.04'] + env_vars: + - '' + # allow for some parallelity without GNU parallel, since it is not installed by default + - 'BATS_NO_PARALLELIZE_ACROSS_FILES=1 BATS_NUMBER_OF_PARALLEL_JOBS=2' + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Run test on OS ${{ matrix.os }} + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: | + bash --version + bash -c "time ${{ matrix.env_vars }} bin/bats --print-output-on-failure --formatter tap test" + + unset_variables: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check unset variables + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + BASH_ENV: ${GITHUB_WORKSPACE}/.github/workflows/set_nounset.bash + run: bin/bats test --print-output-on-failure + + npm_on_linux: + strategy: + matrix: + os: ['ubuntu-20.04', 'ubuntu-22.04'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - name: Run test on OS ${{ matrix.os }} + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: | + npm pack ./ + sudo npm install -g ./bats-*.tgz + bats test --print-output-on-failure + + windows: + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + - run: | + bash --version + bash -c "time bin/bats --print-output-on-failure --formatter tap test" + + npm_on_windows: + strategy: + matrix: + os: ['windows-2019'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - run: npm pack ./ + - run: npm install -g (get-item .\bats-*.tgz).FullName + - run: bats -T --print-output-on-failure test + + macos: + strategy: + matrix: + os: ['macos-11', 'macos-12'] + env_vars: + - '' + # allow for some parallelity without GNU parallel, since it is not installed by default + - 'BATS_NO_PARALLELIZE_ACROSS_FILES=1 BATS_NUMBER_OF_PARALLEL_JOBS=2' + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Install unbuffer via expect + run: brew install expect + - name: Run test on OS ${{ matrix.os }} + shell: 'unbuffer bash {0}' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: | + bash --version + bash -c "time ${{ matrix.env_vars }} bin/bats --print-output-on-failure --formatter tap test" + + npm_on_macos: + strategy: + matrix: + os: ['macos-11', 'macos-12'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - name: Install unbuffer via expect + run: brew install expect + - name: Run test on OS ${{ matrix.os }} + shell: 'unbuffer bash {0}' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: | + npm pack ./ + # somehow there is already an installed bats version around + npm install --force -g ./bats-*.tgz + bats --print-output-on-failure test + + bash-version: + strategy: + matrix: + version: ['3.2', '4.0', '4.1', '4.2', '4.3', '4.4', '4', '5.0', '5.1', '5', 'rc'] + env_vars: + - '' + # also test running (recursively!) in parallel + - '-e BATS_NUMBER_OF_PARALLEL_JOBS=2' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run test on bash version ${{ matrix.version }} + shell: 'script -q -e -c "bash {0}"' # work around tty issues + run: | + set -e + docker build --build-arg bashver="${{ matrix.version }}" --tag "bats/bats:bash-${{ matrix.version }}" . + docker run -it "bash:${{ matrix.version }}" --version + time docker run -it ${{ matrix.env_vars }} "bats/bats:bash-${{ matrix.version }}" --print-output-on-failure --tap /opt/bats/test + + alpine: + runs-on: ubuntu-latest + container: alpine:latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: apk add bash ncurses util-linux + - name: Run test on bash version ${{ matrix.version }} + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: + time ./bin/bats --print-output-on-failure test/ + + freebsd: + runs-on: macos-12 + strategy: + matrix: + packages: + - flock + - "" + steps: + - uses: actions/checkout@v2 + - uses: vmactions/freebsd-vm@v0 + with: + prepare: pkg install -y bash parallel ${{ matrix.packages }} + run: | + time ./bin/bats --print-output-on-failure test/ + + find_broken_symlinks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + # list symlinks that are broken and force non-zero exit if there are any + - run: "! find . -xtype l | grep ." + + rpm: + runs-on: ubuntu-latest + container: almalinux:8 + steps: + - uses: actions/checkout@v2 + - run: dnf install -y rpm-build rpmdevtools + - name: Build and install RPM and dependencies + run: | + rpmdev-setuptree + version=$(rpmspec -q --qf '%{version}' contrib/rpm/bats.spec) + tar --transform "s,^,bats-core-${version}/," -cf /github/home/rpmbuild/SOURCES/v${version}.tar.gz ./ + rpmbuild -v -bb ./contrib/rpm/bats.spec + ls -al /github/home/rpmbuild/RPMS/noarch/ + dnf install -y /github/home/rpmbuild/RPMS/noarch/bats-*.rpm + dnf -y install procps-ng # avoid timeout failure + - name: Run tests + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + run: bats --print-output-on-failure --filter-tags !dep:install_sh test/ + + dockerfile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - uses: docker/build-push-action@v2 + with: + platforms: linux/amd64 + tags: bats:test + load: true + + - run: docker run -itv "$PWD":/code bats:test --tap --print-output-on-failure test/ + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around + + - uses: actions/checkout@v2 + with: + repository: bats-core/bats-assert + path: bats-assert + + - uses: actions/checkout@v2 + with: + repository: bats-core/bats-support + path: bats-support + + - uses: actions/checkout@v2 + with: + repository: bats-core/bats-file + path: bats-file + + - run: | + <test.sh + apk add sudo python3 # install bats-file's dependencies + ln -sf python3 /usr/bin/python # bats-file uses python without version + bats --tap --print-output-on-failure bats-*/test/ + EOF + docker run -itv "$PWD":/code --entrypoint bash bats:test test.sh + shell: 'script -q -e -c "bash {0}"' # work around tty issues + env: + TERM: linux # fix tput for tty issue work around \ No newline at end of file diff --git a/test/bats/.gitignore b/test/bats/.gitignore new file mode 100644 index 000000000..9895e9ffe --- /dev/null +++ b/test/bats/.gitignore @@ -0,0 +1,10 @@ +/docker-compose.override.yml +/docs/build + +# npm +/bats-*.tgz +# we don't have any deps; un-ignore if that changes +/package-lock.json +test/.bats/run-logs/ +# scratch file that should never be committed +/test.bats \ No newline at end of file diff --git a/test/bats/.gitrepo b/test/bats/.gitrepo new file mode 100644 index 000000000..8ffad77c2 --- /dev/null +++ b/test/bats/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = https://github.com/bats-core/bats-core.git + branch = master + commit = a710639259917292afd3a997390586f126a4b9dc + parent = bd90b968876af2fd3d53c93d5404cb22c55296d9 + method = merge + cmdver = 0.4.5 diff --git a/test/bats/.readthedocs.yml b/test/bats/.readthedocs.yml new file mode 100644 index 000000000..4959a3ac4 --- /dev/null +++ b/test/bats/.readthedocs.yml @@ -0,0 +1,9 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + version: 3.7 + install: + - requirements: docs/source/requirements.txt \ No newline at end of file diff --git a/test/bats/AUTHORS b/test/bats/AUTHORS new file mode 100644 index 000000000..71df331b1 --- /dev/null +++ b/test/bats/AUTHORS @@ -0,0 +1,4 @@ +Andrew Martin (https://control-plane.io/) +Bianca Tamayo (https://biancatamayo.me/) +Jason Karns (http://jasonkarns.com/) +Mike Bland (https://mike-bland.com/) diff --git a/test/bats/Dockerfile b/test/bats/Dockerfile new file mode 100644 index 000000000..85f3c5fc5 --- /dev/null +++ b/test/bats/Dockerfile @@ -0,0 +1,43 @@ +ARG bashver=latest + +FROM bash:${bashver} +ARG TINI_VERSION=v0.19.0 +ARG TARGETPLATFORM +ARG LIBS_VER_SUPPORT=0.3.0 +ARG LIBS_VER_FILE=0.3.0 +ARG LIBS_VER_ASSERT=2.1.0 +ARG LIBS_VER_DETIK=1.1.0 +ARG UID=1001 +ARG GID=115 + + +# https://github.com/opencontainers/image-spec/blob/main/annotations.md +LABEL maintainer="Bats-core Team" +LABEL org.opencontainers.image.authors="Bats-core Team" +LABEL org.opencontainers.image.title="Bats" +LABEL org.opencontainers.image.description="Bash Automated Testing System" +LABEL org.opencontainers.image.url="https://hub.docker.com/r/bats/bats" +LABEL org.opencontainers.image.source="https://github.com/bats-core/bats-core" +LABEL org.opencontainers.image.base.name="docker.io/bash" + +COPY ./docker /tmp/docker +# default to amd64 when not running in buildx environment that provides target platform +RUN /tmp/docker/install_tini.sh "${TARGETPLATFORM-linux/amd64}" +# Install bats libs +RUN /tmp/docker/install_libs.sh support ${LIBS_VER_SUPPORT} +RUN /tmp/docker/install_libs.sh file ${LIBS_VER_FILE} +RUN /tmp/docker/install_libs.sh assert ${LIBS_VER_ASSERT} +RUN /tmp/docker/install_libs.sh detik ${LIBS_VER_DETIK} + +# Install parallel and accept the citation notice (we aren't using this in a +# context where it make sense to cite GNU Parallel). +RUN apk add --no-cache parallel ncurses && \ + mkdir -p ~/.parallel && touch ~/.parallel/will-cite \ + && mkdir /code + +RUN ln -s /opt/bats/bin/bats /usr/local/bin/bats +COPY . /opt/bats/ + +WORKDIR /code/ + +ENTRYPOINT ["/tini", "--", "bash", "bats"] diff --git a/test/bats/LICENSE.md b/test/bats/LICENSE.md new file mode 100644 index 000000000..0c7429978 --- /dev/null +++ b/test/bats/LICENSE.md @@ -0,0 +1,53 @@ +Copyright (c) 2017 bats-core contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +* [bats-core] is a continuation of [bats]. Copyright for portions of the + bats-core project are held by Sam Stephenson, 2014 as part of the project + [bats], licensed under MIT: + +Copyright (c) 2014 Sam Stephenson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For details, please see the [version control history][commits]. + +[bats-core]: https://github.com/bats-core/bats-core +[bats]:https://github.com/sstephenson/bats +[commits]:https://github.com/bats-core/bats-core/commits/master diff --git a/test/bats/README.md b/test/bats/README.md new file mode 100644 index 000000000..b41b3c0e3 --- /dev/null +++ b/test/bats/README.md @@ -0,0 +1,127 @@ +# Bats-core: Bash Automated Testing System + +[![Latest release](https://img.shields.io/github/release/bats-core/bats-core.svg)](https://github.com/bats-core/bats-core/releases/latest) +[![npm package](https://img.shields.io/npm/v/bats.svg)](https://www.npmjs.com/package/bats) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/bats-core/bats-core/blob/master/LICENSE.md) +[![Continuous integration status](https://github.com/bats-core/bats-core/workflows/Tests/badge.svg)](https://github.com/bats-core/bats-core/actions?query=workflow%3ATests) +[![Read the docs status](https://readthedocs.org/projects/bats-core/badge/)](https://bats-core.readthedocs.io) + +[![Join the chat in bats-core/bats-core on gitter](https://badges.gitter.im/bats-core/bats-core.svg)][gitter] + +Bats is a [TAP](https://testanything.org/)-compliant testing framework for Bash. It provides a simple +way to verify that the UNIX programs you write behave as expected. + +A Bats test file is a Bash script with special syntax for defining test cases. +Under the hood, each test case is just a function with a description. + +```bash +#!/usr/bin/env bats + +@test "addition using bc" { + result="$(echo 2+2 | bc)" + [ "$result" -eq 4 ] +} + +@test "addition using dc" { + result="$(echo 2 2+p | dc)" + [ "$result" -eq 4 ] +} +``` + +Bats is most useful when testing software written in Bash, but you can use it to +test any UNIX program. + +Test cases consist of standard shell commands. Bats makes use of Bash's +`errexit` (`set -e`) option when running test cases. If every command in the +test case exits with a `0` status code (success), the test passes. In this way, +each line is an assertion of truth. + +## Table of contents + +**NOTE** The documentation has moved to + + + +- [Testing](#testing) +- [Support](#support) +- [Contributing](#contributing) +- [Contact](#contact) +- [Version history](#version-history) +- [Background](#background) + * [What's the plan and why?](#whats-the-plan-and-why) + * [Why was this fork created?](#why-was-this-fork-created) +- [Copyright](#copyright) + + + +## Testing + +```sh +bin/bats --tap test +``` + +See also the [CI](./.github/workflows/tests.yml) settings for the current test environment and +scripts. + +## Support + +The Bats source code repository is [hosted on +GitHub](https://github.com/bats-core/bats-core). There you can file bugs on the +issue tracker or submit tested pull requests for review. + +For real-world examples from open-source projects using Bats, see [Projects +Using Bats](https://github.com/bats-core/bats-core/wiki/Projects-Using-Bats) on +the wiki. + +To learn how to set up your editor for Bats syntax highlighting, see [Syntax +Highlighting](https://github.com/bats-core/bats-core/wiki/Syntax-Highlighting) +on the wiki. + +## Contributing + +For now see the [`docs`](docs) folder for project guides, work with us on the wiki +or look at the other communication channels. + +## Contact + +- You can find and chat with us on our [Gitter]. + +## Version history + +See `docs/CHANGELOG.md`. + +## Background + + +### What's the plan and why? + + +**Tuesday, September 19, 2017:** This was forked from [Bats][bats-orig] at +commit [0360811][]. It was created via `git clone --bare` and `git push +--mirror`. + +[bats-orig]: https://github.com/sstephenson/bats +[0360811]: https://github.com/sstephenson/bats/commit/03608115df2071fff4eaaff1605768c275e5f81f + +This [bats-core repo](https://github.com/bats-core/bats-core) is the community-maintained Bats project. + + +### Why was this fork created? + + +There was an initial [call for maintainers][call-maintain] for the original Bats repository, but write access to it could not be obtained. With development activity stalled, this fork allowed ongoing maintenance and forward progress for Bats. + +[call-maintain]: https://github.com/sstephenson/bats/issues/150 + +## Copyright + +© 2017-2022 bats-core organization + +© 2011-2016 Sam Stephenson + +Bats is released under an MIT-style license; see `LICENSE.md` for details. + +See the [parent project](https://github.com/bats-core) at GitHub or the +[AUTHORS](AUTHORS) file for the current project maintainer team. + +[gitter]: https://gitter.im/bats-core/bats-core diff --git a/test/bats/bin/bats b/test/bats/bin/bats new file mode 100755 index 000000000..892470c1e --- /dev/null +++ b/test/bats/bin/bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v greadlink >/dev/null; then + bats_readlinkf() { + greadlink -f "$1" + } +else + bats_readlinkf() { + readlink -f "$1" + } +fi + +fallback_to_readlinkf_posix() { + bats_readlinkf() { + [ "${1:-}" ] || return 1 + max_symlinks=40 + CDPATH='' # to avoid changing to an unexpected directory + + target=$1 + [ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes + [ -d "${target:-/}" ] && target="$target/" + + cd -P . 2>/dev/null || return 1 + while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do + if [ ! "$target" = "${target%/*}" ]; then + case $target in + /*) cd -P "${target%/*}/" 2>/dev/null || break ;; + *) cd -P "./${target%/*}" 2>/dev/null || break ;; + esac + target=${target##*/} + fi + + if [ ! -L "$target" ]; then + target="${PWD%/}${target:+/}${target}" + printf '%s\n' "${target:-/}" + return 0 + fi + + # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n", + # , , , , + # , , , + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html + link=$(ls -dl -- "$target" 2>/dev/null) || break + target=${link#*" $target -> "} + done + return 1 + } +} + +if ! BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}" 2>/dev/null); then + fallback_to_readlinkf_posix + BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}") +fi + +export BATS_ROOT=${BATS_PATH%/*/*} +export -f bats_readlinkf +exec env BATS_ROOT="$BATS_ROOT" "$BATS_ROOT/libexec/bats-core/bats" "$@" diff --git a/test/bats/contrib/release.sh b/test/bats/contrib/release.sh new file mode 100755 index 000000000..2e4805e1f --- /dev/null +++ b/test/bats/contrib/release.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# +# bats-core git releaser +# +## Usage: %SCRIPT_NAME% [options] +## +## Options: +## --major Major version bump +## --minor Minor version bump +## --patch Patch version bump +## +## -v, --version Print version +## --debug Enable debug mode +## -h, --help Display this message +## + +set -Eeuo pipefail + +DIR=$(cd "$(dirname "${0}")" && pwd) +THIS_SCRIPT="${DIR}/$(basename "${0}")" +BATS_VERSION=$( + # shellcheck disable=SC1090 + source <(grep '^export BATS_VERSION=' libexec/bats-core/bats) + echo "${BATS_VERSION}" +) +declare -r DIR +declare -r THIS_SCRIPT +declare -r BATS_VERSION + +BUMP_INTERVAL="" +NEW_BATS_VERSION="" + +main() { + handle_arguments "${@}" + + if [[ "${BUMP_INTERVAL:-}" == "" ]]; then + echo "${BATS_VERSION}" + exit 0 + fi + + local NEW_BATS_VERSION + NEW_BATS_VERSION=$(semver bump "${BUMP_INTERVAL}" "${BATS_VERSION}") + declare -r NEW_BATS_VERSION + + local BATS_RELEASE_NOTES="/tmp/bats-release-${NEW_BATS_VERSION}" + + echo "Releasing: ${BATS_VERSION} to ${NEW_BATS_VERSION}" + echo + + echo "Ensure docs/CHANGELOG.md is correctly updated" + + replace_in_files + + write_changelog + + git diff --staged + + cat </dev/null + +get_version() { + echo "${THIS_SCRIPT_VERSION:-0.1}" +} + +main "${@}" diff --git a/test/bats/contrib/rpm/bats.spec b/test/bats/contrib/rpm/bats.spec new file mode 100644 index 000000000..91c383026 --- /dev/null +++ b/test/bats/contrib/rpm/bats.spec @@ -0,0 +1,66 @@ +%global provider github.com +%global project bats-core +%global repo bats-core + +Name: bats +Version: 1.9.0 +Release: 1%{?dist} +Summary: Bash Automated Testing System + +Group: Development/Libraries +License: MIT +URL: https://%{provider}/%{project}/%{repo} +Source0: https://%{provider}/%{project}/%{repo}/archive/v%{version}.tar.gz + +BuildArch: noarch + +Requires: bash + +%description +Bats is a TAP-compliant testing framework for Bash. +It provides a simple way to verify that the UNIX programs you write behave as expected. +Bats is most useful when testing software written in Bash, but you can use it to test any UNIX program. + +%prep +%setup -q -n %{repo}-%{version} + +%install +mkdir -p ${RPM_BUILD_ROOT}%{_prefix} ${RPM_BUILD_ROOT}%{_libexecdir} ${RPM_BUILD_ROOT}%{_mandir} +./install.sh ${RPM_BUILD_ROOT}%{_prefix} + +%clean +rm -rf $RPM_BUILD_ROOT + +%check + +%files +%doc README.md LICENSE.md +%{_bindir}/%{name} +%{_libexecdir}/%{repo} +%{_mandir}/man1/%{name}.1.gz +%{_mandir}/man7/%{name}.7.gz +/usr/lib/%{repo}/common.bash +/usr/lib/%{repo}/formatter.bash +/usr/lib/%{repo}/preprocessing.bash +/usr/lib/%{repo}/semaphore.bash +/usr/lib/%{repo}/test_functions.bash +/usr/lib/%{repo}/tracing.bash +/usr/lib/%{repo}/validator.bash +/usr/lib/%{repo}/warnings.bash + +%changelog +* Wed Sep 07 2022 Marcel Hecko - 1.2.0-1 +- Fix and test RPM build on Rocky Linux release 8.6 + +* Sun Jul 08 2018 mbland - 1.1.0-1 +- Increase version to match upstream release + +* Mon Jun 18 2018 pixdrift - 1.0.2-1 +- Increase version to match upstream release +- Relocate libraries to bats-core subdirectory + +* Sat Jun 09 2018 pixdrift - 1.0.1-1 +- Increase version to match upstream release + +* Fri Jun 08 2018 pixdrift - 1.0.0-1 +- Initial package build of forked (bats-core) github project diff --git a/test/bats/contrib/semver b/test/bats/contrib/semver new file mode 100755 index 000000000..ab75586cc --- /dev/null +++ b/test/bats/contrib/semver @@ -0,0 +1,358 @@ +#!/usr/bin/env bash + +# v3.0.0 +# https://github.com/fsaintjacques/semver-tool + +set -o errexit -o nounset -o pipefail + +NAT='0|[1-9][0-9]*' +ALPHANUM='[0-9]*[A-Za-z-][0-9A-Za-z-]*' +IDENT="$NAT|$ALPHANUM" +FIELD='[0-9A-Za-z-]+' + +SEMVER_REGEX="\ +^[vV]?\ +($NAT)\\.($NAT)\\.($NAT)\ +(\\-(${IDENT})(\\.(${IDENT}))*)?\ +(\\+${FIELD}(\\.${FIELD})*)?$" + +PROG=semver +PROG_VERSION="3.0.0" + +USAGE="\ +Usage: + $PROG bump (major|minor|patch|release|prerel |build ) + $PROG compare + $PROG get (major|minor|patch|release|prerel|build) + $PROG --help + $PROG --version + +Arguments: + A version must match the following regular expression: + \"${SEMVER_REGEX}\" + In English: + -- The version must match X.Y.Z[-PRERELEASE][+BUILD] + where X, Y and Z are non-negative integers. + -- PRERELEASE is a dot separated sequence of non-negative integers and/or + identifiers composed of alphanumeric characters and hyphens (with + at least one non-digit). Numeric identifiers must not have leading + zeros. A hyphen (\"-\") introduces this optional part. + -- BUILD is a dot separated sequence of identifiers composed of alphanumeric + characters and hyphens. A plus (\"+\") introduces this optional part. + + See definition. + + A string as defined by PRERELEASE above. + + A string as defined by BUILD above. + +Options: + -v, --version Print the version of this tool. + -h, --help Print this help message. + +Commands: + bump Bump by one of major, minor, patch; zeroing or removing + subsequent parts. \"bump prerel\" sets the PRERELEASE part and + removes any BUILD part. \"bump build\" sets the BUILD part. + \"bump release\" removes any PRERELEASE or BUILD parts. + The bumped version is written to stdout. + + compare Compare with , output to stdout the + following values: -1 if is newer, 0 if equal, 1 if + older. The BUILD part is not used in comparisons. + + get Extract given part of , where part is one of major, minor, + patch, prerel, build, or release. + +See also: + https://semver.org -- Semantic Versioning 2.0.0" + +function error { + echo -e "$1" >&2 + exit 1 +} + +function usage-help { + error "$USAGE" +} + +function usage-version { + echo -e "${PROG}: $PROG_VERSION" + exit 0 +} + +function validate-version { + local version=$1 + if [[ "$version" =~ $SEMVER_REGEX ]]; then + # if a second argument is passed, store the result in var named by $2 + if [ "$#" -eq "2" ]; then + local major=${BASH_REMATCH[1]} + local minor=${BASH_REMATCH[2]} + local patch=${BASH_REMATCH[3]} + local prere=${BASH_REMATCH[4]} + local build=${BASH_REMATCH[8]} + eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$prere\" \"$build\")" + else + echo "$version" + fi + else + error "version $version does not match the semver scheme 'X.Y.Z(-PRERELEASE)(+BUILD)'. See help for more information." + fi +} + +function is-nat { + [[ "$1" =~ ^($NAT)$ ]] +} + +function is-null { + [ -z "$1" ] +} + +function order-nat { + [ "$1" -lt "$2" ] && { + echo -1 + return + } + [ "$1" -gt "$2" ] && { + echo 1 + return + } + echo 0 +} + +function order-string { + [[ $1 < $2 ]] && { + echo -1 + return + } + [[ $1 > $2 ]] && { + echo 1 + return + } + echo 0 +} + +# given two (named) arrays containing NAT and/or ALPHANUM fields, compare them +# one by one according to semver 2.0.0 spec. Return -1, 0, 1 if left array ($1) +# is less-than, equal, or greater-than the right array ($2). The longer array +# is considered greater-than the shorter if the shorter is a prefix of the longer. +# +function compare-fields { + local l="$1[@]" + local r="$2[@]" + local leftfield=("${!l}") + local rightfield=("${!r}") + local left + local right + + local i=$((-1)) + local order=$((0)) + + while true; do + [ $order -ne 0 ] && { + echo $order + return + } + + : $((i++)) + left="${leftfield[$i]}" + right="${rightfield[$i]}" + + is-null "$left" && is-null "$right" && { + echo 0 + return + } + is-null "$left" && { + echo -1 + return + } + is-null "$right" && { + echo 1 + return + } + + is-nat "$left" && is-nat "$right" && { + order=$(order-nat "$left" "$right") + continue + } + is-nat "$left" && { + echo -1 + return + } + is-nat "$right" && { + echo 1 + return + } + { + order=$(order-string "$left" "$right") + continue + } + done +} + +# shellcheck disable=SC2206 # checked by "validate"; ok to expand prerel id's into array +function compare-version { + local order + validate-version "$1" V + validate-version "$2" V_ + + # compare major, minor, patch + + local left=("${V[0]}" "${V[1]}" "${V[2]}") + local right=("${V_[0]}" "${V_[1]}" "${V_[2]}") + + order=$(compare-fields left right) + [ "$order" -ne 0 ] && { + echo "$order" + return + } + + # compare pre-release ids when M.m.p are equal + + local prerel="${V[3]:1}" + local prerel_="${V_[3]:1}" + local left=(${prerel//./ }) + local right=(${prerel_//./ }) + + # if left and right have no pre-release part, then left equals right + # if only one of left/right has pre-release part, that one is less than simple M.m.p + + [ -z "$prerel" ] && [ -z "$prerel_" ] && { + echo 0 + return + } + [ -z "$prerel" ] && { + echo 1 + return + } + [ -z "$prerel_" ] && { + echo -1 + return + } + + # otherwise, compare the pre-release id's + + compare-fields left right +} + +function command-bump { + local new + local version + local sub_version + local command + + case $# in + 2) case $1 in + major | minor | patch | release) + command=$1 + version=$2 + ;; + *) usage-help ;; + esac ;; + 3) case $1 in + prerel | build) + command=$1 + sub_version=$2 version=$3 + ;; + *) usage-help ;; + esac ;; + *) usage-help ;; + esac + + validate-version "$version" parts + # shellcheck disable=SC2154 + local major="${parts[0]}" + local minor="${parts[1]}" + local patch="${parts[2]}" + local prere="${parts[3]}" + local build="${parts[4]}" + + case "$command" in + major) new="$((major + 1)).0.0" ;; + minor) new="${major}.$((minor + 1)).0" ;; + patch) new="${major}.${minor}.$((patch + 1))" ;; + release) new="${major}.${minor}.${patch}" ;; + prerel) new=$(validate-version "${major}.${minor}.${patch}-${sub_version}") ;; + build) new=$(validate-version "${major}.${minor}.${patch}${prere}+${sub_version}") ;; + *) usage-help ;; + esac + + echo "$new" + exit 0 +} + +function command-compare { + local v + local v_ + + case $# in + 2) + v=$(validate-version "$1") + v_=$(validate-version "$2") + ;; + *) usage-help ;; + esac + + set +u # need unset array element to evaluate to null + compare-version "$v" "$v_" + exit 0 +} + +# shellcheck disable=SC2034 +function command-get { + local part version + + if [[ "$#" -ne "2" ]] || [[ -z "$1" ]] || [[ -z "$2" ]]; then + usage-help + exit 0 + fi + + part="$1" + version="$2" + + validate-version "$version" parts + local major="${parts[0]}" + local minor="${parts[1]}" + local patch="${parts[2]}" + local prerel="${parts[3]:1}" + local build="${parts[4]:1}" + local release="${major}.${minor}.${patch}" + + case "$part" in + major | minor | patch | release | prerel | build) echo "${!part}" ;; + *) usage-help ;; + esac + + exit 0 +} + +case $# in +0) + echo "Unknown command: $*" + usage-help + ;; +esac + +case $1 in +--help | -h) + echo -e "$USAGE" + exit 0 + ;; +--version | -v) usage-version ;; +bump) + shift + command-bump "$@" + ;; +get) + shift + command-get "$@" + ;; +compare) + shift + command-compare "$@" + ;; +*) + echo "Unknown arguments: $*" + usage-help + ;; +esac diff --git a/test/bats/docker-compose.override.dist b/test/bats/docker-compose.override.dist new file mode 100644 index 000000000..f2b1ed8fc --- /dev/null +++ b/test/bats/docker-compose.override.dist @@ -0,0 +1,8 @@ +# Copy this file to docker-compose.override.yml +version: '3.6' +services: + bats: + entrypoint: + - "bash" +networks: + default: diff --git a/test/bats/docker-compose.yml b/test/bats/docker-compose.yml new file mode 100644 index 000000000..21cd99481 --- /dev/null +++ b/test/bats/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.6' +services: + bats: + build: + context: "." + dockerfile: "Dockerfile" + networks: + - "default" + user: "root" + volumes: + - "./:/opt/bats" +networks: + default: diff --git a/test/bats/docker/install_libs.sh b/test/bats/docker/install_libs.sh new file mode 100755 index 000000000..455865375 --- /dev/null +++ b/test/bats/docker/install_libs.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + + +LIBNAME="${1:-support}" +LIVERSION="${2:-0.3.0}" +BASEURL='https://github.com/bats-core' +DESTDIR="${BATS_LIBS_DEST_DIR:-/usr/lib/bats}" +TMPDIR=$(mktemp -d -t bats-libs-XXXXXX) +USAGE="Please provide the bats libe name and version \nFor example: install_libs.sh support 2.0.0\n" + +trap 'test -d "${TMPDIR}" && rm -fr "${TMPDIR}"' EXIT ERR SIGINT SIGTERM + +[[ $# -ne 2 ]] && { _log FATAL "$USAGE"; exit 1; } + +_log() { + printf "$(date "+%Y-%m-%d %H:%M:%S") - %s - %s\n" "${1}" "${2}" +} + +create_temp_dirs() { + mkdir -p "${TMPDIR}/${1}" + if [[ ${LIBNAME} != "detik" ]]; then + mkdir -p "${DESTDIR}/bats-${1}/src" + else + _log INFO "Skipping src 'cause Detik does not need it" + fi +} + +download_extract_source() { + wget -qO- ${BASEURL}/bats-"${1}"/archive/refs/tags/v"${2}".tar.gz | tar xz -C "${TMPDIR}/${1}" --strip-components 1 +} + +install_files() { + if [[ ${LIBNAME} != "detik" ]]; then + install -Dm755 "${TMPDIR}/${1}/load.bash" "${DESTDIR}/bats-${1}/load.bash" + for fn in "${TMPDIR}/${1}/src/"*.bash; do install -Dm755 "$fn" "${DESTDIR}/bats-${1}/src/$(basename "$fn")"; done + else + for fn in "${TMPDIR}/${1}/lib/"*.bash; do install -Dm755 "$fn" "${DESTDIR}/bats-${1}/$(basename "$fn")"; done + fi +} + +_log INFO "Starting to install ${LIBNAME} ver ${LIVERSION}" +_log INFO "Creating directories" +create_temp_dirs "${LIBNAME}" +_log INFO "Downloading" +download_extract_source "${LIBNAME}" "${LIVERSION}" +_log INFO "Installation" +install_files "${LIBNAME}" +_log INFO "Done, cleaning.." diff --git a/test/bats/docker/install_tini.sh b/test/bats/docker/install_tini.sh new file mode 100755 index 000000000..8d98da14c --- /dev/null +++ b/test/bats/docker/install_tini.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +case ${1#linux/} in +386) + TINI_PLATFORM=i386 + ;; +arm/v7) + TINI_PLATFORM=armhf + ;; +arm/v6) + TINI_PLATFORM=armel + ;; +*) + TINI_PLATFORM=${1#linux/} + ;; +esac + +echo "Installing tini for $TINI_PLATFORM" + +wget "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TINI_PLATFORM}" -O /tini +wget "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TINI_PLATFORM}.asc" -O /tini.asc + +chmod +x /tini + +apk add gnupg +gpg --import ` (without space) (#657) + +## [1.8.0] - 2022-09-15 + +### Added + +* using external formatters via `--formatter ` (also works for + `--report-formatter`) (#602) +* running only tests that failed in the last run via `--filter-status failed` (#483) +* variable `BATS_TEST_RETRIES` that specifies how often a test should be + reattempted before it is considered failed (#618) +* Docker tags `latest-no-faccessat2` and `-no-faccessat2` for + avoiding `bash: bats: No such file or directory` on `docker<20.10` (or + `runc`/`# bats file_tags=` and + `--filter-tags ` for tagging tests for execution filters (#642) +* warning BW03: inform about `setup_suite` in wrong file (`.bats` instead of `setup_suite.bash`) (#652) + +#### Documentation + +* update gotcha about negated statements: Recommend using `run !` on Bats + versions >=1.5.0 (#593) +* add documentation for `bats_require_minimum_version` (#595) +* improve documentation about `setup_suite` (#652) + +### Fixed + +* added missing shebang (#597) +* remaining instances of `run -` being incorrectly documented as `run =` (#599) +* allow `--gather-test-outputs-in ` to work with existing, empty + directories (#603) + * also add `--clean-and-gather-test-outputs-in ` for improved UX +* double slashes in paths derived from TMPDIR on MacOS (#607) +* fix `load` in `teardown` marking failed tests as not run (#612) +* fix unset variable errors (with set -u) and add regression test (#621) +* `teardown_file` errors don't swallow `setup_file` errors anymore, the behavior + is more like `teardown`'s now (only `return`/last command can trigger `teardown` + errors) (#623) +* upgraded from deprecated CI envs for MacOS (10 -> 11,12) and Ubuntu + (18.04 -> 22.04) (#630) +* add `/usr/lib/bats` as default value for `BATS_LIB_PATH` (#628) +* fix unset variable in `bats-formatter-junit` when `setup_file` fails (#632) +* unify error behavior of `teardown`/`teardown_file`/`teardown_suite` functions: + only fail via return code, not via ERREXIT (#633) +* fix unbound variable errors with `set -u` on `setup_suite` failures (#643) +* fix `load` not being available in `setup_suite` (#644) +* fix RPM spec, add regression test (#648) +* fix handling of `IFS` by `run` (#650) +* only print `setup_suite`'s stderr on errors (#649) + +#### Documentation + +* fix typos, spelling and links (#596, #604, #619, #627) +* fix redirection order of an example in the tutorial (#617) + +## [1.7.0] - 2022-05-14 + +### Added + +* Pretty formatter print filename when entering file (#561) +* BATS_TEST_NAME_PREFIX allows prefixing test names on stdout and in reports (#561) +* setup_suite and teardown_suite (#571, #585) +* out-of-band warning infrastructure, with following warnings: + * BW01: run command not found (exit code 127) (#586) + * BW02: run uses flags without proper `bats_require_minimum_version` guard (#587) +* `bats_require_minimum_version` to guard code that would not run on older + versions (#587) + +#### Documentation + +* document `$BATS_VERSION` (#557) +* document new warning infrastructure (#589, #587, #586) + +### Fixed + +* unbound variable errors in formatters when using `SHELLOPTS=nounset` (`-u`) (#558) +* don't require `flock` *and* `shlock` for parallel mode test (#554) +* print name of failing test when using TAP13 with timing information (#559, #555) +* removed broken symlink, added regression test (#560) +* don't show empty lines as `#` with pretty formatter (#561) +* prevent `teardown`, `teardown_file`, and `teardown_suite` from overriding bats' + exit code by setting `$status` (e.g. via calling `run`) (#581, #575) + * **CRITICAL**: this can return exit code 0 despite failed tests, thus preventing + your CI from reporting test failures! The regression happened in version 1.6.0. +* `run --keep-empty-lines` now reports 0 lines on empty `$output` (#583) + +#### Documentation + +* remove 2018 in title, update copyright dates in README.md (#567) +* fix broken links (#568) +* corrected invalid documentation of `run -N` (had `=N` instead) (#579) + * **CRITICAL**: using the incorrect form can lead to silent errors. See + [issue #578](https://github.com/bats-core/bats-core/issues/578) for more + details and how to find out if your tests are affected. + +## [1.6.1] - 2022-05-14 + +### Fixed + +* prevent `teardown`, `teardown_file`, and `teardown_suite` from overriding bats' + exit code by setting `$status` (e.g. via calling `run`) (#581, #575) + * **CRITICAL**: this can return exit code 0 despite failed tests, thus preventing + your CI from reporting test failures! The regression happened in version 1.6.0. + +#### Documentation + +* corrected invalid documentation of `run -N` (had `=N` instead) (#579) + * **CRITICAL**: using the incorrect form can lead to silent errors. See + [issue #578](https://github.com/bats-core/bats-core/issues/578) for more + details and how to find out if your tests are affected. + +## [1.6.0] - 2022-02-24 + +### Added + +* new flag `--code-quote-style` (and `$BATS_CODE_QUOTE_STYLE`) to customize +quotes around code blocks in error output (#506) +* an example/regression test for running background tasks without blocking the + test run (#525, #535) +* `bats_load_library` for loading libraries from the search path + `$BATS_LIB_PATH` (#548) + +### Fixed + +* improved error trace for some broken cases (#279) +* removed leftover debug file `/tmp/latch` in selftest suite + (single use latch) (#516) +* fix recurring errors on CTRL+C tests with NPM on Windows in selftest suite (#516) +* fixed leaking of local variables from debug trap (#520) +* don't mark FD3 output from `teardown_file` as `` in junit output (#532) +* fix unbound variable error with Bash pre 4.4 (#550) + +#### Documentation + +* remove links to defunct freenode IRC channel (#515) +* improved grammar (#534) +* fixed link to TAP spec (#537) + +## [1.5.0] - 2021-10-22 + +### Added + +* new command line flags (#488) + * `--verbose-run`: Make `run` print `$output` by default + * `-x`, `--trace`: Print test commands as they are executed (like `set -x`)` + * `--show-output-of-passing-tests`: Print output of passing tests + * `--print-output-on-failure`: Automatically print the value of `$output` on + failed tests + * `--gather-test-outputs-in `: Gather the output of failing **and** + passing tests as files in directory +* Experimental: add return code checks to `run` via `!`/`-` (#367, #507) +* `install.sh` and `uninstall.sh` take an optional second parameter for the lib + folder name to allow for multilib install, e.g. into lib64 (#452) +* add `run` flag `--keep-empty-lines` to retain empty lines in `${lines[@]}` (#224, + a894fbfa) +* add `run` flag `--separate-stderr` which also fills `$stderr` and + `$stderr_lines` (#47, 5c9b173d, #507) + +### Fixed + +* don't glob `run`'s `$output` when splitting into `${lines[@]}` + (#151, #152, #158, #156, #281, #289) +* remove empty line after test with pretty formatter on some terminals (#481) +* don't run setup_file/teardown_file on files without tests, e.g. due to + filtering (#484) +* print final line without newline on Bash 3.2 for midtest (ERREXIT) failures + too (#495, #145) +* abort with error on missing flock/shlock when running in parallel mode (#496) +* improved `set -u` test and fixed some unset variable accesses (#498, #501) +* shorten suite/file/test temporary folder paths to leave enough space even on + restricted systems (#503) + +#### Documentation + +* minor edits (#478) + +## [1.4.1] - 2021-07-24 + +### Added + +* Docker image architectures amd64, 386, arm64, arm/v7, arm/v6, ppc64le, s390x (#438) + +### Fixed + +* automatic push to Dockerhub (#438) + +## [1.4.0] - 2021-07-23 + +### Added + +* added BATS_TEST_TMPDIR, BATS_FILE_TMPDIR, BATS_SUITE_TMPDIR (#413) +* added checks and improved documentation for `$BATS_TMPDIR` (#410) +* the docker container now uses [tini](https://github.com/krallin/tini) as the + container entrypoint to improve signal forwarding (#407) +* script to uninstall bats from a given prefix (#400) +* replace preprocessed file path (e.g. `/tmp/bats-run-22908-NP0f9h/bats.23102.src`) + with original filename in stdout/err (but not FD3!) (#429) +* print aborted command on SIGINT/CTRL+C (#368) +* print error message when BATS_RUN_TMPDIR could not be created (#422) + +#### Documentation + +* added tutorial for new users (#397) +* fixed example invocation of docker container (#440) +* minor edits (#431, #439, #445, #463, #464, #465) + +### Fixed + +* fix `bats_tap_stream_unknown: command not found` with pretty formatter, when + writing non compliant extended output (#412) +* avoid collisions on `$BATS_RUN_TMPDIR` with `--no-tempdir-cleanup` and docker + by using `mktemp` additionally to PID (#409) +* pretty printer now puts text that is printed to FD 3 below the test name (#426) +* `rm semaphores/slot-: No such file or directory` in parallel mode on MacOS + (#434, #433) +* fix YAML blocks in TAP13 formatter using `...` instead of `---` to start + a block (#442) +* fixed some typos in comments (#441, #447) +* ensure `/code` exists in docker container, to make examples work again (#440) +* also display error messages from free code (#429) +* npm installed version on Windows: fix broken internal LIBEXEC paths (#459) + +## [1.3.0] - 2021-03-08 + +### Added + +* custom test-file extension via `BATS_FILE_EXTENSION` when searching for test + files in a directory (#376) +* TAP13 formatter, including millisecond timing (#337) +* automatic release to NPM via GitHub Actions (#406) + +#### Documentation + +* added documentation about overusing `run` (#343) +* improved documentation of `load` (#332) + +### Changed + +* recursive suite mode will follow symlinks now (#370) +* split options for (file-) `--report-formatter` and (stdout) `--formatter` (#345) + * **WARNING**: This changes the meaning of `--formatter junit`. + stdout will now show unified xml instead of TAP. From now on, please use + `--report-formatter junit` to obtain the `.xml` report file! +* removed `--parallel-preserve-environment` flag, as this is the default + behavior (#324) +* moved CI from Travis/AppVeyor to GitHub Actions (#405) +* preprocessed files are no longer removed if `--no-tempdir-cleanup` is + specified (#395) + +#### Documentation + +* moved documentation to [readthedocs](https://bats-core.readthedocs.io/en/latest/) + +### Fixed + +#### Correctness + +* fix internal failures due to unbound variables when test files use `set -u` (#392) +* fix internal failures due to changes to `$PATH` in test files (#387) +* fix test duration always being 0 on busybox installs (#363) +* fix hangs on CTRL+C (#354) +* make `BATS_TEST_NUMBER` count per file again (#326) +* include `lib/` in npm package (#352) + +#### Performance + +* don't fork bomb in parallel mode (#339) +* preprocess each file only once (#335) +* avoid running duplicate files n^2 times (#338) + +#### Documentation + +* fix documentation for `--formatter junit` (#334) +* fix documentation for `setup_file` variables (#333) +* fix link to examples page (#331) +* fix link to "File Descriptor 3" section (#301) + +## [1.2.1] - 2020-07-06 + +### Added + +* JUnit output and extensible formatter rewrite (#246) +* `load` function now reads from absolute and relative paths, and $PATH (#282) +* Beginner-friendly examples in /docs/examples (#243) +* @peshay's `bats-file` fork contributed to `bats-core/bats-file` (#276) + +### Changed + +* Duplicate test names now error (previous behaviour was to issue a warning) (#286) +* Changed default formatter in Docker to pretty by adding `ncurses` to + Dockerfile, override with `--tap` (#239) +* Replace "readlink -f" dependency with Bash solution (#217) + +## [1.2.0] - 2020-04-25 + +Support parallel suite execution and filtering by test name. + +### Added + +* docs/CHANGELOG.md and docs/releasing.md (#122) +* The `-f, --filter` flag to run only the tests matching a regular expression (#126) +* Optimize stack trace capture (#138) +* `--jobs n` flag to support parallel execution of tests with GNU parallel (#172) + +### Changed + +* AppVeyor builds are now semver-compliant (#123) +* Add Bash 5 as test target (#181) +* Always use upper case signal names to avoid locale dependent err… (#215) +* Fix for tests reading from stdin (#227) +* Fix wrong line numbers of errors in bash < 4.4 (#229) +* Remove preprocessed source after test run (#232) + +## [1.1.0] - 2018-07-08 + +This is the first release with new features relative to the original Bats 0.4.0. + +### Added + +* The `-r, --recursive` flag to scan directory arguments recursively for + `*.bats` files (#109) +* The `contrib/rpm/bats.spec` file to build RPMs (#111) + +### Changed + +* Travis exercises latest versions of Bash from 3.2 through 4.4 (#116, #117) +* Error output highlights invalid command line options (#45, #46, #118) +* Replaced `echo` with `printf` (#120) + +### Fixed + +* Fixed `BATS_ERROR_STATUS` getting lost when `bats_error_trap` fired multiple + times under Bash 4.2.x (#110) +* Updated `bin/bats` symlink resolution, handling the case on CentOS where + `/bin` is a symlink to `/usr/bin` (#113, #115) + +## [1.0.2] - 2018-06-18 + +* Fixed sstephenson/bats#240, whereby `skip` messages containing parentheses + were truncated (#48) +* Doc improvements: + * Docker usage (#94) + * Better README badges (#101) + * Better installation instructions (#102, #104) +* Packaging/installation improvements: + * package.json update (#100) + * Moved `libexec/` files to `libexec/bats-core/`, improved `install.sh` (#105) + +## [1.0.1] - 2018-06-09 + +* Fixed a `BATS_CWD` bug introduced in #91 whereby it was set to the parent of + `PWD`, when it should've been set to `PWD` itself (#98). This caused file + names in stack traces to contain the basename of `PWD` as a prefix, when the + names should've been purely relative to `PWD`. +* Ensure the last line of test output prints when it doesn't end with a newline + (#99). This was a quasi-bug introduced by replacing `sed` with `while` in #88. + +## [1.0.0] - 2018-06-08 + +`1.0.0` generally preserves compatibility with `0.4.0`, but with some Bash +compatibility improvements and a massive performance boost. In other words: + +* all existing tests should remain compatible +* tests that might've failed or exhibited unexpected behavior on earlier + versions of Bash should now also pass or behave as expected + +Changes: + +* Added support for Docker. +* Added support for test scripts that have the [unofficial strict + mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) enabled. +* Improved stability on Windows and macOS platforms. +* Massive performance improvements, especially on Windows (#8) +* Workarounds for inconsistent behavior between Bash versions (#82) +* Workaround for preserving stack info after calling an exported function under + Bash < 4.4 (#87) +* Fixed TAP compliance for skipped tests +* Added support for tabs in test names. +* `bin/bats` and `install.sh` now work reliably on Windows (#91) + +## [0.4.0] - 2014-08-13 + +* Improved the display of failing test cases. Bats now shows the source code of + failing test lines, along with full stack traces including function names, + filenames, and line numbers. +* Improved the display of the pretty-printed test summary line to include the + number of skipped tests, if any. +* Improved the speed of the preprocessor, dramatically shortening test and suite + startup times. +* Added support for absolute pathnames to the `load` helper. +* Added support for single-line `@test` definitions. +* Added bats(1) and bats(7) manual pages. +* Modified the `bats` command to default to TAP output when the `$CI` variable + is set, to better support environments such as Travis CI. + +## [0.3.1] - 2013-10-28 + +* Fixed an incompatibility with the pretty formatter in certain environments + such as tmux. +* Fixed a bug where the pretty formatter would crash if the first line of a test + file's output was invalid TAP. + +## [0.3.0] - 2013-10-21 + +* Improved formatting for tests run from a terminal. Failing tests are now + colored in red, and the total number of failing tests is displayed at the end + of the test run. When Bats is not connected to a terminal (e.g. in CI runs), + or when invoked with the `--tap` flag, output is displayed in standard TAP + format. +* Added the ability to skip tests using the `skip` command. +* Added a message to failing test case output indicating the file and line + number of the statement that caused the test to fail. +* Added "ad-hoc" test suite support. You can now invoke `bats` with multiple + filename or directory arguments to run all the specified tests in aggregate. +* Added support for test files with Windows line endings. +* Fixed regular expression warnings from certain versions of Bash. +* Fixed a bug running tests containing lines that begin with `-e`. + +## [0.2.0] - 2012-11-16 + +* Added test suite support. The `bats` command accepts a directory name + containing multiple test files to be run in aggregate. +* Added the ability to count the number of test cases in a file or suite by + passing the `-c` flag to `bats`. +* Preprocessed sources are cached between test case runs in the same file for + better performance. + +## [0.1.0] - 2011-12-30 + +* Initial public release. + +[Unreleased]: https://github.com/bats-core/bats-core/compare/v1.7.0...HEAD +[1.7.0]: https://github.com/bats-core/bats-core/compare/v1.6.1...v1.7.0 +[1.6.1]: https://github.com/bats-core/bats-core/compare/v1.6.0...v1.6.1 +[1.6.0]: https://github.com/bats-core/bats-core/compare/v1.5.0...v1.6.0 +[1.5.0]: https://github.com/bats-core/bats-core/compare/v1.4.1...v1.5.0 +[1.4.1]: https://github.com/bats-core/bats-core/compare/v1.4.0...v1.4.1 +[1.4.0]: https://github.com/bats-core/bats-core/compare/v1.3.0...v1.4.0 +[1.3.0]: https://github.com/bats-core/bats-core/compare/v1.2.1...v1.3.0 +[1.2.1]: https://github.com/bats-core/bats-core/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/bats-core/bats-core/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/bats-core/bats-core/compare/v1.0.2...v1.1.0 +[1.0.2]: https://github.com/bats-core/bats-core/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/bats-core/bats-core/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/bats-core/bats-core/compare/v0.4.0...v1.0.0 +[0.4.0]: https://github.com/bats-core/bats-core/compare/v0.3.1...v0.4.0 +[0.3.1]: https://github.com/bats-core/bats-core/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/bats-core/bats-core/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/bats-core/bats-core/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/bats-core/bats-core/commits/v0.1.0 diff --git a/test/bats/docs/CODEOWNERS b/test/bats/docs/CODEOWNERS new file mode 100644 index 000000000..2eb233305 --- /dev/null +++ b/test/bats/docs/CODEOWNERS @@ -0,0 +1,4 @@ +# This enables automatic code review requests per: +# - https://help.github.com/articles/about-codeowners/ +# - https://help.github.com/articles/enabling-required-reviews-for-pull-requests/ +* @bats-core/bats-core diff --git a/test/bats/docs/CODE_OF_CONDUCT.md b/test/bats/docs/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..d8d6972d5 --- /dev/null +++ b/test/bats/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,92 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting one of the current [project maintainers](#project-maintainers) listed below. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Project Maintainers + +### Current Maintainers + +* [Bianca Tamayo][bt-gh] +* [Mike Bland][mb-gh] +* [Jason Karns][jk-gh] +* [Andrew Martin][am-gh] + +### Past Maintainers + +* Sam Stephenson <> (Original author) + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[bt-gh]: https://github.com/btamayo +[mb-gh]: https://github.com/mbland +[jk-gh]: https://github.com/jasonkarns +[am-gh]: https://github.com/sublimino + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/test/bats/docs/CONTRIBUTING.md b/test/bats/docs/CONTRIBUTING.md new file mode 100644 index 000000000..263e41197 --- /dev/null +++ b/test/bats/docs/CONTRIBUTING.md @@ -0,0 +1,392 @@ +# Contributing Guidelines + +## Welcome! + +Thank you for considering contributing to the development of this project's +development and/or documentation. Just a reminder: if you're new to this project +or to OSS and want to find issues to work on, please check the following labels +on issues: + +- [help wanted][helpwantedlabel] +- [docs][docslabel] +- [good first issue][goodfirstissuelabel] + +[docslabel]: https://github.com/bats-core/bats-core/labels/docs +[helpwantedlabel]: https://github.com/bats-core/bats-core/labels/help%20wanted +[goodfirstissuelabel]: https://github.com/bats-core/bats-core/labels/good%20first%20issue + +To see all labels and their meanings, [check this wiki page][labelswiki]. + +This guide borrows **heavily** from [@mbland's go-script-bash][gsb] (with some +sections directly quoted), which in turn was +drafted with tips from [Wrangling Web Contributions: How to Build +a CONTRIBUTING.md][moz] and with some inspiration from [the Atom project's +CONTRIBUTING.md file][atom]. + +[gsb]: https://github.com/mbland/go-script-bash/blob/master/CONTRIBUTING.md +[moz]: https://mozillascience.github.io/working-open-workshop/contributing/ +[atom]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md + +[labelswiki]: https://github.com/bats-core/bats-core/wiki/GitHub-Issue-Labels + +## Table of contents + +* [Contributing Guidelines](#contributing-guidelines) + * [Welcome!](#welcome) + * [Table of contents](#table-of-contents) + * [Quick links](#quick-links) + * [Contributor License Agreement](#contributor-license-agreement) + * [Code of conduct](#code-of-conduct) + * [Asking questions and reporting issues](#asking-questions-and-reporting-issues) + * [Updating documentation](#updating-documentation) + * [Environment setup](#environment-setup) + * [Workflow](#workflow) + * [Testing](#testing) + * [Coding conventions](#coding-conventions) + * [Formatting](#formatting) + * [Naming](#naming) + * [Function declarations](#function-declarations) + * [Variable and parameter declarations](#variable-and-parameter-declarations) + * [Command substitution](#command-substitution) + * [Process substitution](#process-substitution) + * [Conditionals and loops](#conditionals-and-loops) + * [Generating output](#generating-output) + * [Gotchas](#gotchas) + * [Open Source License](#open-source-license) + * [Credits](#credits) + +## Quick links + +- [Gitter channel →][gitterurl]: Feel free to come chat with us on Gitter +- [README →][README] +- [Code of conduct →][CODE_OF_CONDUCT] +- [License information →][LICENSE] +- [Original repository →][repohome] +- [Issues →][repoissues] +- [Pull requests →][repoprs] +- [Milestones →][repomilestones] +- [Projects →][repoprojects] + +[README]: https://github.com/bats-core/bats-core/blob/master/README.md +[CODE_OF_CONDUCT]: https://github.com/bats-core/bats-core/blob/master/docs/CODE_OF_CONDUCT.md +[LICENSE]: https://github.com/bats-core/bats-core/blob/master/LICENSE.md + +## Contributor License Agreement + +Per the [GitHub Terms of Service][gh-tos], be aware that by making a +contribution to this project, you agree: + +* to license your contribution under the same terms as [this project's + license][osmit], and +* that you have the right to license your contribution under those terms. + +See also: ["Does my project need an additional contributor agreement? Probably + not."][cla-needed] + +[gh-tos]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license +[osmit]: #open-source-license +[cla-needed]: https://opensource.guide/legal/#does-my-project-need-an-additional-contributor-agreement + + +## Code of conduct + +Harassment or rudeness of any kind will not be tolerated, period. For +specifics, see the [CODE_OF_CONDUCT][] file. + +## Asking questions and reporting issues + +### Asking questions + +Please check the [README][] or existing [issues][repoissues] first. + +If you cannot find an answer to your question, please feel free to hop on our +[Gitter][gitterurl]. [![Gitter](https://badges.gitter.im/bats-core/bats-core.svg)](https://gitter.im/bats-core/bats-core) + +### Reporting issues + +Before reporting an issue, please use the search feature on the [issues +page][repoissues] to see if an issue matching the one you've observed has already +been filed. + +### Updating or filing a new issue + +#### Information to include + +Try to be as specific as possible about your environment and the problem you're +observing. At a minimum, include: + +#### Installation issues + +1. State the version of Bash you're using `bash --version` +1. State your operating system and its version +1. If you're installing through homebrew, run `brew doctor`, and attach the +output of `brew info bats-core` + +#### Bugs/usage issues + +1. State the version of Bash you're using `bash --version` +1. State your operating system and its version +1. Command line steps or code snippets that reproduce the issue +1. Any apparently relevant information from the [Bash changelog][bash-changes] + +[bash-changes]: https://tiswww.case.edu/php/chet/bash/CHANGES + +Also consider using: + +- Bash's `time` builtin to collect running times +- a regression test to add to the suite +- memory usage as reported by a tool such as + [memusg](https://gist.github.com/netj/526585) + +### On existing issues + +1. DO NOT add a +1 comment: Use the reactions provided instead +1. DO add information if you're facing a similar issue to someone else, but +within a different context (e.g. different steps needed to reproduce the issue +than previous stated, different version of Bash or BATS, different OS, etc.) +You can read on how to do that here: [Information to include](#information-to-include) +1. DO remember that you can use the *Subscribe* button on the right side of the +page to receive notifications of further conversations or a resolution. + +## Updating documentation + +We love documentation and people who love documentation! + +If you love writing clear, accessible docs, please don't be shy about pull +requests. Remember: docs are just as important as code. + +Also: _no typo is too small to fix!_ Really. Of course, batches of fixes are +preferred, but even one nit is one nit too many. + +## Environment setup + +Make sure you have Bash installed per the [Environment setup in the +README][env-setup]. + +[env-setup]: https://github.com/bats-core/bats-core/blob/master/README.md#environment-setup + +## Workflow + +The basic workflow for submitting changes resembles that of the [GitHub Git +Flow][github-flow] (a.k.a. GitHub Flow), except that you will be working with +your own fork of the repository and issuing pull requests to the original. + +[github-flow]: https://guides.github.com/introduction/flow/ + +1. Fork the repo on GitHub (look for the "Fork" button) +1. Clone your forked repo to your local machine +1. Create your feature branch (`git checkout -b my-new-feature`) +1. Develop _and [test](#testing)_ your changes as necessary. +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin my-new-feature`) +1. Create a new [GitHub pull request][gh-pr] for your feature branch based + against the original repository's `master` branch +1. If your request is accepted, you can [delete your feature branch][rm-branch] + and pull the updated `master` branch from the original repository into your + fork. You may even [delete your fork][rm-fork] if you don't anticipate making + further changes. + +[gh-pr]: https://help.github.com/articles/using-pull-requests/ +[rm-branch]: https://help.github.com/articles/deleting-unused-branches/ +[rm-fork]: https://help.github.com/articles/deleting-a-repository/ + +## Testing + +- Continuous integration status: [![Tests](https://github.com/bats-core/bats-core/workflows/Tests/badge.svg)](https://github.com/bats-core/bats-core/actions?query=workflow%3ATests) + +## Coding conventions + +- [Formatting](#formatting) +- [Naming](#naming) +- [Variable and parameter declarations](#variable-and-parameter-declarations) +- [Command substitution](#command-substitution) +- [Conditions and loops](#conditionals-and-loops) +- [Gotchas](#gotchas) + +### Formatting + +- Keep all files 80 characters wide. +- Indent using two spaces. +- Enclose all variables in double quotes when used to avoid having them + interpreted as glob patterns (unless the variable contains a glob pattern) + and to avoid word splitting when the value contains spaces. Both scenarios + can introduce errors that often prove difficult to diagnose. + - **This is especially important when the variable is used to generate a + glob pattern**, since spaces may appear in a path value. + - If the variable itself contains a glob pattern, make sure to set + `IFS=$'\n'` before using it so that the pattern itself and any matching + file names containing spaces are not split apart. + - Exceptions: Quotes are not required within math contexts, i.e. `(( ))` or + `$(( ))`, and must not be used for variables on the right side of the `=~` + operator. +- Enclose all string literals in single quotes. + - Exception: If the string contains an apostrophe, use double quotes. +- Use quotes around variables and literals even inside of `[[ ]]` conditions. + - This is because strings that contain '[' or ']' characters may fail to + compare equally when they should. + - Exception: Do not quote variables that contain regular expression patterns + appearing on the right side of the `=~` operator. +- _Only_ quote arguments to the right of `=~` if the expression is a literal + match without any metacharacters. + +The following are intended to prevent too-compact code: + +- Declare only one item per `declare`, `local`, `export`, or `readonly` call. + - _Note:_ This also helps avoid subtle bugs, as trying to initialize one + variable using the value of another declared in the same statement will + not do what you may expect. The initialization of the first variable will + not yet be complete when the second variable is declared, so the first + variable will have an empty value. +- Do not use one-line `if`, `for`, `while`, `until`, `case`, or `select` + statements. +- Do not use `&&` or `||` to avoid writing `if` statements. +- Do not write functions entirely on one line. +- For `case` statements: put each pattern on a line by itself; put each command + on a line by itself; put the `;;` terminator on a line by itself. + +### Naming + +- Use `snake_case` for all identifiers. + +### Function declarations + +- Declare functions without the `function` keyword. +- Strive to always use `return`, never `exit`, unless an error condition is + severe enough to warrant it. + - Calling `exit` makes it difficult for the caller to recover from an error, + or to compose new commands from existing ones. + +### Variable and parameter declarations + +- _Gotcha:_ Never initialize an array on the same line as an `export` or + `declare -g` statement. See [the Gotchas section](#gotchas) below for more + details. +- Declare all variables inside functions using `local`. +- Declare temporary file-level variables using `declare`. Use `unset` to remove + them when finished. +- Don't use `local -r`, as a readonly local variable in one scope can cause a + conflict when it calls a function that declares a `local` variable of the same + name. +- Don't use type flags with `declare` or `local`. Assignments to integer + variables in particular may behave differently, and it has no effect on array + variables. +- For most functions, the first lines should use `local` declarations to + assign the original positional parameters to more meaningful names, e.g.: + ```bash + format_summary() { + local cmd_name="$1" + local summary="$2" + local longest_name_len="$3" + ``` + For very short functions, this _may not_ be necessary, e.g.: + ```bash + has_spaces() { + [[ "$1" != "${1//[[:space:]]/}" ]] + } + ``` + +### Command substitution + +- If possible, don't. While this capability is one of Bash's core strengths, + every new process created by Bats makes the framework slower, and speed is + critical to encouraging the practice of automated testing. (This is especially + true on Windows, [where process creation is one or two orders of magnitude + slower][win-slow]. See [bats-core/bats-core#8][pr-8] for an illustration of + the difference avoiding subshells makes.) Bash is quite powerful; see if you + can do what you need in pure Bash first. +- If you need to capture the output from a function, store the output using + `printf -v` instead if possible. `-v` specifies the name of the variable into + which to write the result; the caller can supply this name as a parameter. +- If you must use command substitution, use `$()` instead of backticks, as it's + more robust, more searchable, and can be nested. + +[win-slow]: https://rufflewind.com/2014-08-23/windows-bash-slow +[pr-8]: https://github.com/bats-core/bats-core/pull/8 + +### Process substitution + +- If possible, don't use it. See the advice on avoiding subprocesses and using + `printf -v` in the **Command substitution** section above. +- Use wherever necessary and possible, such as when piping input into a `while` + loop (which avoids having the loop body execute in a subshell) or running a + command taking multiple filename arguments based on output from a function or + pipeline (e.g. `diff`). +- *Warning*: It is impossible to directly determine the exit status of a process + substitution; emitting an exit status as the last line of output is a possible + workaround. + +### Conditionals and loops + +- Always use `[[` and `]]` for evaluating variables. Per the guideline under + **Formatting**, quote variables and strings within the brackets, but not + regular expressions (or variables containing regular expressions) appearing + on the right side of the `=~` operator. + +### Generating output + +- Use `printf` instead of `echo`. Both are Bash builtins, and there's no + perceptible performance difference when running Bats under the `time` builtin. + However, `printf` provides a more consistent experience in general, as `echo` + has limitations to the arguments it accepts, and even the same version of Bash + may produce different results for `echo` based on how the binary was compiled. + See [Stack Overflow: Why is printf better than echo?][printf-vs-echo] for + excruciating details. + +[printf-vs-echo]: https://unix.stackexchange.com/a/65819 + +### Signal names + +Always use upper case signal names (e.g. `trap - INT EXIT`) to avoid locale +dependent errors. In some locales (for example Turkish, see +[Turkish dotless i](https://en.wikipedia.org/wiki/Dotted_and_dotless_I)) lower +case signal names cause Bash to error. An example of the problem: + +```bash +$ echo "tr_TR.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen tr_TR.UTF-8 # Ubuntu derivatives +$ LC_CTYPE=tr_TR.UTF-8 LC_MESSAGES=C bash -c 'trap - int && echo success' +bash: line 0: trap: int: invalid signal specification +$ LC_CTYPE=tr_TR.UTF-8 LC_MESSAGES=C bash -c 'trap - INT && echo success' +success +``` + +### Gotchas + +- If you wish to use command substitution to initialize a `local` variable, and + then check the exit status of the command substitution, you _must_ declare the + variable on one line and perform the substitution on another. If you don't, + the exit status will always indicate success, as it is the status of the + `local` declaration, not the command substitution. +- To work around a bug in some versions of Bash whereby arrays declared with + `declare -g` or `export` and initialized in the same statement eventually go + out of scope, always `export` the array name on one line and initialize it the + next line. See: + - https://lists.gnu.org/archive/html/bug-bash/2012-06/msg00068.html + - ftp://ftp.gnu.org/gnu/bash/bash-4.2-patches/bash42-025 + - http://lists.gnu.org/archive/html/help-bash/2012-03/msg00078.html +- [ShellCheck](https://www.shellcheck.net/) can help to identify many of these issues + + +## Open Source License + +This software is made available under the [MIT License][osmit]. +For the text of the license, see the [LICENSE][] file. + +## Credits + +- This guide was heavily written by BATS-core member [@mbland](https://github.com/mbland) +for [go-script-bash](https://github.com/mbland/go-script-bash), tweaked for [BATS-core][repohome] +- Table of Contents created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) +- The [official bash logo](https://github.com/odb/official-bash-logo) is copyrighted +by the [Free Software Foundation](https://www.fsf.org/), 2016 under the [Free Art License](http://artlibre.org/licence/lal/en/) + + + +[repoprojects]: https://github.com/bats-core/bats-core/projects +[repomilestones]: https://github.com/bats-core/bats-core/milestones +[repoprs]: https://github.com/bats-core/bats-core/pulls +[repoissues]: https://github.com/bats-core/bats-core/issues +[repohome]: https://github.com/bats-core/bats-core + +[osmit]: https://opensource.org/licenses/MIT + +[gitterurl]: https://gitter.im/bats-core/bats-core diff --git a/test/bats/docs/Makefile b/test/bats/docs/Makefile new file mode 100644 index 000000000..d0c3cbf10 --- /dev/null +++ b/test/bats/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/test/bats/docs/PULL_REQUEST_TEMPLATE.md b/test/bats/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..c7c58d58b --- /dev/null +++ b/test/bats/docs/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +- [ ] I have reviewed the [Contributor Guidelines][contributor]. +- [ ] I have reviewed the [Code of Conduct][coc] and agree to abide by it + +[contributor]: https://github.com/bats-core/bats-core/blob/master/docs/CONTRIBUTING.md +[coc]: https://github.com/bats-core/bats-core/blob/master/docs/CODE_OF_CONDUCT.md diff --git a/test/bats/docs/examples/README.md b/test/bats/docs/examples/README.md new file mode 100644 index 000000000..5ef51342f --- /dev/null +++ b/test/bats/docs/examples/README.md @@ -0,0 +1,6 @@ +# Examples + +This directory contains example .bats files. +See the [bats-core wiki][examples] for more details. + +[examples]: https://github.com/bats-core/bats-core/wiki/Examples \ No newline at end of file diff --git a/test/bats/docs/examples/package-tarball b/test/bats/docs/examples/package-tarball new file mode 100644 index 000000000..b51cffd17 --- /dev/null +++ b/test/bats/docs/examples/package-tarball @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# "unofficial" bash strict mode +# See: http://redsymbol.net/articles/unofficial-bash-strict-mode +set -o errexit # Exit when simple command fails 'set -e' +set -o errtrace # Exit on error inside any functions or subshells. +set -o nounset # Trigger error when expanding unset variables 'set -u' +set -o pipefail # Do not hide errors within pipes 'set -o pipefail' +set -o xtrace # Display expanded command and arguments 'set -x' +IFS=$'\n\t' # Split words on \n\t rather than spaces + +main() { + tar -czf "$dst_tarball" -C "$src_dir" . +} + +main "$@" diff --git a/test/bats/docs/examples/package-tarball.bats b/test/bats/docs/examples/package-tarball.bats new file mode 100755 index 000000000..144318ae1 --- /dev/null +++ b/test/bats/docs/examples/package-tarball.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats + +setup() { + export dst_tarball="${BATS_TMPDIR}/dst.tar.gz" + export src_dir="${BATS_TMPDIR}/src_dir" + + rm -rf "${dst_tarball}" "${src_dir}" + mkdir "${src_dir}" + touch "${src_dir}"/{a,b,c} +} + +main() { + bash "${BATS_TEST_DIRNAME}"/package-tarball +} + +@test "fail when \$src_dir and \$dst_tarball are unbound" { + unset src_dir dst_tarball + + run main + [ "${status}" -ne 0 ] +} + +@test "fail when \$src_dir is a non-existent directory" { + # shellcheck disable=SC2030 + src_dir='not-a-dir' + + run main + [ "${status}" -ne 0 ] +} + +# shellcheck disable=SC2016 +@test "pass when \$src_dir directory is empty" { + # shellcheck disable=SC2031,SC2030 + rm -rf "${src_dir:?}/*" + + run main + echo "$output" + [ "${status}" -eq 0 ] +} + +# shellcheck disable=SC2016 +@test "files in \$src_dir are added to tar archive" { + run main + [ "${status}" -eq 0 ] + + run tar tf "$dst_tarball" + [ "${status}" -eq 0 ] + [[ "${output}" =~ a ]] + [[ "${output}" =~ b ]] + [[ "${output}" =~ c ]] +} diff --git a/test/bats/docs/make.bat b/test/bats/docs/make.bat new file mode 100644 index 000000000..9534b0181 --- /dev/null +++ b/test/bats/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/test/bats/docs/releasing.md b/test/bats/docs/releasing.md new file mode 100644 index 000000000..cd0717b7e --- /dev/null +++ b/test/bats/docs/releasing.md @@ -0,0 +1,127 @@ +# Releasing a new Bats version + +These notes reflect the current process. There's a lot more we could do, in +terms of automation and expanding the number of platforms to which we formally +release (see #103). + +## Update docs/CHANGELOG.md + +Create a new entry at the top of `docs/CHANGELOG.md` that enumerates the +significant updates to the new version. + +## Bumping the version number + +Bump the version numbers in the following files: + +- contrib/rpm/bats.spec +- libexec/bats-core/bats +- package.json + +Commit these changes (including the `docs/CHANGELOG.md` changes) in a commit +with the message `Bats `, where `` is the new version number. + +Create a new signed, annotated tag with: + +```bash +$ git tag -a -s +``` + +Include the `docs/CHANGELOG.md` notes corresponding to the new version as the +tag annotation, except the first line should be: `Bats - YYYY-MM-DD` +and any Markdown headings should become plain text, e.g.: + +```md +### Added +``` + +should become: + +```md +Added: +``` + +## Create a GitHub release + +Push the new version commit and tag to GitHub via the following: + +```bash +$ git push --follow-tags +``` + +Then visit https://github.com/bats-core/bats-core/releases, and: + +* Click **Draft a new release**. +* Select the new version tag. +* Name the release: `Bats `. +* Paste the same notes from the version tag annotation as the description, + except change the first line to read: `Released: YYYY-MM-DD`. +* Click **Publish release**. + +For more on `git push --follow-tags`, see: + +* [git push --follow-tags in the online manual][ft-man] +* [Stack Overflow: How to push a tag to a remote repository using Git?][ft-so] + +[ft-man]: https://git-scm.com/docs/git-push#git-push---follow-tags +[ft-so]: https://stackoverflow.com/a/26438076 + +## NPM + +`npm publish`. Pretty easy! + +For the paranoid, use `npm pack` and install the resulting tarball locally with +`npm install` before publishing. + +## Homebrew + +The basic instructions are in the [Submit a new version of an existing +formula][brew] section of the Homebrew docs. + +[brew]: https://github.com/Homebrew/brew/blob/master/docs/How-To-Open-a-Homebrew-Pull-Request.md#submit-a-new-version-of-an-existing-formula + +An example using v1.1.0 (notice that this uses the sha256 sum of the tarball): + +```bash +$ curl -LOv https://github.com/bats-core/bats-core/archive/v1.1.0.tar.gz +$ openssl sha256 v1.1.0.tar.gz +SHA256(v1.1.0.tar.gz)=855d8b8bed466bc505e61123d12885500ef6fcdb317ace1b668087364717ea82 + +# Add the --dry-run flag to see the individual steps without executing. +$ brew bump-formula-pr \ + --url=https://github.com/bats-core/bats-core/archive/v1.1.0.tar.gz \ + --sha256=855d8b8bed466bc505e61123d12885500ef6fcdb317ace1b668087364717ea82 +``` +This resulted in https://github.com/Homebrew/homebrew-core/pull/29864, which was +automatically merged once the build passed. + +## Alpine Linux + +An example using v1.1.0 (notice that this uses the sha512 sum of the Zip file): + +```bash +$ curl -LOv https://github.com/bats-core/bats-core/archive/v1.1.0.zip +$ openssl sha512 v1.1.0.zip +SHA512(v1.1.0.zip)=accd83cfec0025a2be40982b3f9a314c2bbf72f5c85daffa9e9419611904a8d34e376919a5d53e378382e0f3794d2bd781046d810225e2a77812474e427bed9e +``` + +After cloning alpinelinux/aports, I used the above information to create: +https://github.com/alpinelinux/aports/pull/4696 + +**Note:** Currently users must enable the `edge` branch of the `community` repo +by adding/uncommenting the corresponding entry in `/etc/apk/repositories`. + +## Announce + +It's worth making a brief announcement like [the v1.1.0 announcement via +Gitter][gitter]: + +[gitter]: https://gitter.im/bats-core/bats-core?at=5b42c9a57b811a6d63daacb5 + +``` +v1.1.0 is now available via Homebrew and npm: +https://github.com/bats-core/bats-core/releases/tag/v1.1.0 + +It'll eventually be available in Alpine via the edge branch of the community +repo once alpinelinux/aports#4696 gets merged. (Check /etc/apk/repositories to +ensure this repo is enabled.) +``` diff --git a/test/bats/docs/source/_static/.gitkeep b/test/bats/docs/source/_static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/bats/docs/source/_templates/.gitkeep b/test/bats/docs/source/_templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/bats/docs/source/conf.py b/test/bats/docs/source/conf.py new file mode 100644 index 000000000..6deda83d4 --- /dev/null +++ b/test/bats/docs/source/conf.py @@ -0,0 +1,72 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'bats-core' +copyright = '2022, bats-core organization' +author = 'bats-core organization' + +# The full version, including alpha/beta/rc tags +release = '1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'recommonmark', + 'sphinxcontrib.programoutput' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +html_sidebars = { '**': [ + 'about.html', + 'navigation.html', + 'relations.html', + 'searchbox.html', + 'donate.html'] } + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +#html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +#man_pages = [ ('man.1', 'bats', 'bats documentation', ['bats-core Contributors'], 1)] + +def setup(app): + app.add_config_value('recommonmark_config', {'enable_eval_rst': True}, True) + import recommonmark + from recommonmark.transform import AutoStructify + app.add_transform(AutoStructify) diff --git a/test/bats/docs/source/docker-usage.md b/test/bats/docs/source/docker-usage.md new file mode 100644 index 000000000..7318e566c --- /dev/null +++ b/test/bats/docs/source/docker-usage.md @@ -0,0 +1,58 @@ +# Docker Usage Guide + +- [Docker Usage Guide](#docker-usage-guide) + * [Basic Usage](#basic-usage) + * [Docker Gotchas](#docker-gotchas) + * [Extending from the base image](#extending-from-the-base-image) + +## Basic Usage + +To build and run `bats`' own tests: +```bash +$ git clone https://github.com/bats-core/bats-core.git +Cloning into 'bats-core'... +remote: Counting objects: 1222, done. +remote: Compressing objects: 100% (53/53), done. +remote: Total 1222 (delta 34), reused 55 (delta 21), pack-reused 1146 +Receiving objects: 100% (1222/1222), 327.28 KiB | 1.70 MiB/s, done. +Resolving deltas: 100% (661/661), done. + +$ cd bats-core/ +$ docker build --tag bats/bats:latest . +... +$ docker run -it bats/bats:latest --formatter tap /opt/bats/test +``` + +To mount your tests into the container, first build the image as above. Then, for example with `bats`: +```bash +$ docker run -it -v "$PWD:/opt/bats" bats/bats:latest /opt/bats/test +``` +This runs the `test/` directory from the bats-core repository inside the bats Docker container. + +For test suites that are intended to run in isolation from the project (i.e. the tests do not depend on project files outside of the test directory), you can mount the test directory by itself and execute the tests like so: + +```bash +$ docker run -it -v "$PWD:/code" bats/bats:latest /code/test +``` + +## Docker Gotchas + +Relying on functionality provided by your environment (ssh keys or agent, installed binaries, fixtures outside the mounted test directory) will fail when running inside Docker. + +`--interactive`/`-i` attaches an interactive terminal and is useful to kill hanging processes (otherwise has to be done via docker stop command). `--tty`/`-t` simulates a tty (often not used, but most similar to test runs from a Bash prompt). Interactivity is important to a user, but not a build, and TTYs are probably more important to a headless build. Everything's least-surprising to a new Docker use if both are used. + +## Extending from the base image + +Docker operates on a principle of isolation, and bundles all dependencies required into the Docker image. These can be mounted in at runtime (for test files, configuration, etc). For binary dependencies it may be better to extend the base Docker image with further tools and files. + +```dockerfile +FROM bats/bats + +RUN \ + apk \ + --no-cache \ + --update \ + add \ + openssh + +``` diff --git a/test/bats/docs/source/faq.rst b/test/bats/docs/source/faq.rst new file mode 100644 index 000000000..0cecfa7aa --- /dev/null +++ b/test/bats/docs/source/faq.rst @@ -0,0 +1,170 @@ +FAQ +=== + +How do I set the working directory? +----------------------------------- + +The working directory is simply the directory where you started when executing bats. +If you want to enforce a specific directory, you can use `cd` in the `setup_file`/`setup` functions. +However, be aware that code outside any function will run before any of these setup functions and might interfere with bats' internals. + + +How do I see the output of the command under `run` when a test fails? +--------------------------------------------------------------------- + +`run` captures stdout and stderr of its command and stores it in the `$output` and `${lines[@]}` variables. +If you want to see this output, you need to print it yourself, or use functions like `assert_output` that will reproduce it on failure. + +Can I use `--filter` to exclude files/tests? +-------------------------------------------- + +No, not directly. `--filter` uses a regex to match against test names. So you could try to invert the regex. +The filename won't be part of the strings that are tested, so you cannot filter against files. + +How can I exclude a single test from a test run? +------------------------------------------------ + +If you want to exclude only few tests from a run, you can either `skip` them: + +.. code-block:: bash + + @test "Testname" { + # yadayada + } + +becomes + +.. code-block:: bash + + @test "Testname" { + skip 'Optional skip message' + # yadayada + } + +or comment them out, e.g.: + +.. code-block:: bash + + @test "Testname" { + +becomes + +.. code-block:: bash + + disabled() { # @test "Testname" { + +For multiple tests or all tests of a file, this becomes tedious, so read on. + +How can I exclude all tests of a file from a test run? +-------------------------------------------------------- + +If you run your test suite by naming individual files like: + +.. code-block:: bash + + $ bats test/a.bats test/b.bats ... + +you can simply omit your file. When running a folder like + + +.. code-block:: bash + + $ bats test/ + +you can prevent test files from being picked up by changing their extension to something other than `.bats`. + +It is also possible to `skip` in `setup_file`/`setup` which will skip all tests in the file. + +How can I include my own `.sh` files for testing? +------------------------------------------------- + +You can simply `source .sh` files. However, be aware that `source`ing files with errors outside of any function (or inside `setup_file`) will trip up bats +and lead to hard to diagnose errors. +Therefore, it is safest to only `source` inside `setup` or the test functions themselves. + +How can I debug a failing test? +------------------------------- + +Short of using a bash debugger you should make sure to use appropriate asserts for your task instead of raw bash comparisons, e.g.: + +.. code-block:: bash + + @test test { + run echo test failed + assert_output "test" + # instead of + [ "$output" = "test" ] + } + +Because the former will print the output when the test fails while the latter won't. +Similarly, you should use `assert_success`/`assert_failure` instead of `[ "$status" -eq 0 ]` for return code checks. + +Is there a mechanism to add file/test specific functionality to a common setup function? +---------------------------------------------------------------------------------------- + +Often the setup consists of parts that are common between different files of a test suite and parts that are specific to each file. +There is no suite wide setup functionality yet, so you should extract these common setup steps into their own file (e.g. `common-test-setup.sh`) and function (e.g. `commonSetup() {}`), +which can be `source`d or `load`ed and call it in `setup_file` or `setup`. + +How can I use helper libraries like bats-assert? +------------------------------------------------ + +This is a short reproduction of https://github.com/ztombol/bats-docs. + +At first, you should make sure the library is installed. This is usually done in the `test_helper/` folders alongside the `.bats` files, giving you a filesystem layout like this: + +.. code-block:: + + test/ + test.bats + test_helper/ + bats-support/ + bats-assert/ + +Next, you should load those helper libraries: + +.. code-block:: bash + + setup() { + load 'test_helper/bats-support/load' # this is required by bats-assert! + load 'test_helper/bats-assert/load' + } + +Now, you should be able to use the functions from these helpers inside your tests, e.g.: + +.. code-block:: bash + + @test "test" { + run echo test + assert_output "test" + } + +Note that you obviously need to load the library before using it. +If you need the library inside `setup_file` or `teardown_file` you need to load it in `setup_file`. + +How to set a test timeout in bats? +---------------------------------- + +Set the variable `$BATS_TEST_TIMEOUT` before `setup()` starts. This means you can set it either on the command line, +in free code in the test file or in `setup_file()`. + +How can I lint/shell-format my bats tests? +------------------------------------------ + +Due to their custom syntax (`@test`), `.bats` files are not standard bash. This prevents most tools from working with bats. +However, there is an alternative syntax `function_name { # @test` to declare tests in a bash compliant manner. + +- shellcheck support since version 0.7 +- shfmt support since version 3.2.0 (using `-ln bats`) + + +How can I check if a test failed/succeeded during teardown? +----------------------------------------------------------- + +You can check `BATS_TEST_COMPLETED` which will be set to 1 if the test was successful or empty if it was not. +There is also `BATS_TEST_SKIPPED` which will be non-empty (contains the skip message or -1) when `skip` was called. + +How can I setup/cleanup before/after all tests? +----------------------------------------------- + +Currently, this is not supported. Please contribute your usecase to issue `#39 `_. diff --git a/test/bats/docs/source/gotchas.rst b/test/bats/docs/source/gotchas.rst new file mode 100644 index 000000000..485b67503 --- /dev/null +++ b/test/bats/docs/source/gotchas.rst @@ -0,0 +1,132 @@ +Gotchas +======= + +My test fails although I return true? +------------------------------------- + +Using `return 1` to signify `true` for a success as is done often in other languages does not mesh well with Bash's +convention of using return code 0 to signify success and everything non-zero to indicate a failure. + +Please adhere to this idiom while using bats, or you will constantly work against your environment. + +My negated statement (e.g. ! true) does not fail the test, even when it should. +------------------------------------------------------------------------------- + +Bash deliberately excludes negated return values from causing a pipeline to exit (see bash's `-e` option). +Use `run !` on Bats 1.5.0 and above. For older bats versions, use one of `! x || false` or `run` with `[ $status != 0 ]`. + +If the negated command is the final statement in a test, that final statement's (negated) exit status will propagate through to the test's return code as usual. +Negated statements of one of the correct forms mentioned above will explicitly fail the test when the pipeline returns true, regardless of where they occur in the test. + +I cannot register a test multiple times via for loop. +----------------------------------------------------- + +The usual bats tests (`@test`) are preprocessed into functions. +Wrapping them into a for loop only redeclares this function. + +If you are interested in registering multiple calls to the same function, contribute your wishes to issue `#306 `_. + +I cannot pass parameters to test or .bats files. +------------------------------------------------ + +Especially while using bats via shebang: + +.. code-block:: bash + + #!/usr/bin/env bats + + @test "test" { + # ... + } + +You could be tempted to pass parameters to the test invocation like `./test.bats param1 param2`. +However, bats does not support passing parameters to files or tests. +If you need such a feature, please let us know about your usecase. + +As a workaround you can use environment variables to pass parameters. + +Why can't my function return results via a variable when using `run`? +--------------------------------------------------------------------- + +The `run` function executes its command in a subshell which means the changes to variables won't be available in the calling shell. + +If you want to test these functions, you should call them without `run`. + +`run` doesn't fail, although the same command without `run` does. +----------------------------------------------------------------- + +`run` is a wrapper that always succeeds. The wrapped command's exit code is stored in `$status` and the stdout/stderr in `$output`. +If you want to fail the test, you should explicitly check `$status` or omit `run`. See also `when not to use run `_. + +`load` won't load my `.sh` files. +--------------------------------- + +`load` is intended as an internal helper function that always loads `.bash` files (by appending this suffix). +If you want to load an `.sh` file, you can simple `source` it. + +I can't lint/shell-format my bats tests. +---------------------------------------- + +Bats uses a custom syntax for annotating tests (`@test`) that is not bash compliant. +Therefore, standard bash tooling won't be able to interact directly with `.bats` files. +Shellcheck supports bats' native syntax as of version 0.7. + +Additionally, there is bash compatible syntax for tests: + +.. code-block:: bash + + function bash_compliant_function_name_as_test_name { # @test + # your code + } + + +The output (stdout/err) from commands under `run` is not visible in failed tests. +--------------------------------------------------------------------------------- + +By default, `run` only stores stdout/stderr in `$output` (and `${lines[@]}`). +If you want to see this output, you either should use bat-assert's assertions or have to print `$output` before the check that fails. + +My piped command does not work under run. +----------------------------------------- + +Be careful with using pipes and with `run`. While your mind model of `run` might wrap the whole command behind it, bash's parser won't + +.. code-block:: bash + + run echo foo | grep bar + +Won't `run (echo foo | grep bar)` but will `(run echo foo) | grep bar`. If you need to incorporate pipes, you either should do + +.. code-block:: bash + + run bash -c 'echo foo | grep bar' + +or use a function to wrap the pipe in: + +.. code-block:: bash + + fun_with_pipes() { + echo foo | grep bar + } + + run fun_with_pipes + +`[[ ]]` (or `(( ))` did not fail my test +---------------------------------------- + +The `set -e` handling of `[[ ]]` and `(( ))` changed in Bash 4.1. Older versions, like 3.2 on MacOS, +don't abort the test when they fail, unless they are the last command before the (test) function returns, +making their exit code the return code. + +`[ ]` does not suffer from this, but is no replacement for all `[[ ]]` usecases. Appending ` || false` will work in all cases. + +Background tasks prevent the test run from terminating when finished +-------------------------------------------------------------------- + +When running a task in background, it will inherit the opened FDs of the process it was forked from. +This means that the background task forked from a Bats test will hold the FD for the pipe to the formatter that prints to the terminal, +thus keeping it open until the background task finished. +Due to implementation internals of Bats and bash, this pipe might be held in multiple FDs which all have to be closed by the background task. + +You can use `close_non_std_fds from `test/fixtures/bats/issue-205.bats` in the background job to close all FDs except stdin, stdout and stderr, thus solving the problem. +More details about the issue can be found in [#205](https://github.com/bats-core/bats-core/issues/205#issuecomment-973572596). diff --git a/test/bats/docs/source/index.rst b/test/bats/docs/source/index.rst new file mode 100644 index 000000000..b1867d64c --- /dev/null +++ b/test/bats/docs/source/index.rst @@ -0,0 +1,18 @@ +Welcome to bats-core's documentation! +===================================== + +Versions before v1.2.1 are documented over `there `_. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + tutorial + installation + usage + docker-usage + writing-tests + gotchas + faq + warnings/index + support-matrix diff --git a/test/bats/docs/source/installation.rst b/test/bats/docs/source/installation.rst new file mode 100644 index 000000000..43bdc0571 --- /dev/null +++ b/test/bats/docs/source/installation.rst @@ -0,0 +1,138 @@ + +Installation +============ + +Linux: Distribition Package Manager +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Following Linux distributions provide Bats via their package manager: + +* Arch Linux: `community/bash-bats `__ +* Alpine Linux: `bats `__ +* Debian Linux: `shells/bats `__ +* Fedora Linux: `rpms/bats `__ +* Gentoo Linux `dev-util/bats `__ +* OpenSUSE Linux: `bats `__ +* Ubuntu Linux `shells/bats `__ + +**Note**: Bats versions pre 1.0 are from sstephenson's original project. +Consider using one of the other installation methods below to get the latest Bats release. +The test matrix above only applies to the latest Bats version. + +If your favorite distribution is not listed above, +you can try one of the following package managers or install from source. + +MacOS: Homebrew +^^^^^^^^^^^^^^^ + +On macOS, you can install `Homebrew `__ if you haven't already, +then run: + +.. code-block:: bash + + $ brew install bats-core + +Any OS: npm +^^^^^^^^^^^ + +You can install the `Bats npm package `__ via: + +.. code-block:: + + # To install globally: + $ npm install -g bats + + # To install into your project and save it as one of the "devDependencies" in + # your package.json: + $ npm install --save-dev bats + +Any OS: Installing Bats from source +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Check out a copy of the Bats repository. Then, either add the Bats ``bin`` +directory to your ``$PATH``\ , or run the provided ``install.sh`` command with the +location to the prefix in which you want to install Bats. For example, to +install Bats into ``/usr/local``\ , + +.. code-block:: + + $ git clone https://github.com/bats-core/bats-core.git + $ cd bats-core + $ ./install.sh /usr/local + + +**Note:** You may need to run ``install.sh`` with ``sudo`` if you do not have +permission to write to the installation prefix. + +Windows: Installing Bats from source via Git Bash +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Check out a copy of the Bats repository and install it to ``$HOME``. This +will place the ``bats`` executable in ``$HOME/bin``\ , which should already be +in ``$PATH``. + +.. code-block:: + + $ git clone https://github.com/bats-core/bats-core.git + $ cd bats-core + $ ./install.sh $HOME + + +Running Bats in Docker +^^^^^^^^^^^^^^^^^^^^^^ + +There is an official image on the Docker Hub: + +.. code-block:: + + $ docker run -it bats/bats:latest --version + + +Building a Docker image +~~~~~~~~~~~~~~~~~~~~~~~ + +Check out a copy of the Bats repository, then build a container image: + +.. code-block:: + + $ git clone https://github.com/bats-core/bats-core.git + $ cd bats-core + $ docker build --tag bats/bats:latest . + + +This creates a local Docker image called ``bats/bats:latest`` based on `Alpine +Linux `__ +(to push to private registries, tag it with another organisation, e.g. +``my-org/bats:latest``\ ). + +To run Bats' internal test suite (which is in the container image at +``/opt/bats/test``\ ): + +.. code-block:: + + $ docker run -it bats/bats:latest /opt/bats/test + + +To run a test suite from a directory called ``test`` in the current directory of +your local machine, mount in a volume and direct Bats to its path inside the +container: + +.. code-block:: + + $ docker run -it -v "${PWD}:/code" bats/bats:latest test + + +.. + + ``/code`` is the working directory of the Docker image. "${PWD}/test" is the + location of the test directory on the local machine. + + +This is a minimal Docker image. If more tools are required this can be used as a +base image in a Dockerfile using ``FROM ``. In the future there may +be images based on Debian, and/or with more tools installed (\ ``curl`` and ``openssl``\ , +for example). If you require a specific configuration please search and +1 an +issue or `raise a new issue `__. + +Further usage examples are in +`the wiki `__. diff --git a/test/bats/docs/source/requirements.txt b/test/bats/docs/source/requirements.txt new file mode 100644 index 000000000..b61574d1e --- /dev/null +++ b/test/bats/docs/source/requirements.txt @@ -0,0 +1,2 @@ +sphinxcontrib-programoutput +recommonmark \ No newline at end of file diff --git a/test/bats/docs/source/support-matrix.rst b/test/bats/docs/source/support-matrix.rst new file mode 100644 index 000000000..b67be4238 --- /dev/null +++ b/test/bats/docs/source/support-matrix.rst @@ -0,0 +1,26 @@ +Support Matrix +============== + +Supported Bash versions +^^^^^^^^^^^^^^^^^^^^^^^ + +The following is a list of Bash versions that are currently supported by Bats and verified through automated tests: + + * 3.2.57(1) (macOS's highest bundled version) + * 4.0, 4.1, 4.2, 4.3, 4.4 + * 5.0, 5.1, 5.2 + +Supported Operating systems +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following Operating Systems are supported and tested automatically (CI) or manually during development: + + * Linux: Alpine (CI), Alma 8 (CI), Arch Linux (manual), Ubuntu 20.04/22.04 (CI) + * FreeBSD: 11 (CI) + * macOS: 11 (CI), 12 (CI) + * Windows: Server 2019 (CI), 10 (manual) + + * Git for Windows Bash (MSYS2 based) + * Windows Subsystem for Linux + * MSYS2 + * Cygwin diff --git a/test/bats/docs/source/tutorial.rst b/test/bats/docs/source/tutorial.rst new file mode 100644 index 000000000..72a5409d8 --- /dev/null +++ b/test/bats/docs/source/tutorial.rst @@ -0,0 +1,661 @@ +Tutorial +======== + +This tutorial is intended for beginners with bats and possibly bash. +Make sure to also read the list of gotchas and the faq. + +For this tutorial we are assuming you already have a project in a git repository and want to add tests. +Ultimately they should run in the CI environment but will also be started locally during development. + +.. + TODO: link to example repository? + +Quick installation +------------------ + +Since we already have an existing git repository, it is very easy to include bats and its libraries as submodules. +We are aiming for following filesystem structure: + +.. code-block:: + + src/ + project.sh + ... + test/ + bats/ <- submodule + test_helper/ + bats-support/ <- submodule + bats-assert/ <- submodule + test.bats + ... + +So we start from the project root: + +.. code-block:: console + + git submodule add https://github.com/bats-core/bats-core.git test/bats + git submodule add https://github.com/bats-core/bats-support.git test/test_helper/bats-support + git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert + +Your first test +--------------- + +Now we want to add our first test. + +In the tutorial repository, we want to build up our project in a TDD fashion. +Thus, we start with an empty project and our first test is to just run our (nonexistent) shell script. + +We start by creating a new test file `test/test.bats` + +.. code-block:: bash + + @test "can run our script" { + ./project.sh + } + +and run it by + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ can run our script + (in test file test/test.bats, line 2) + `./project.sh' failed with status 127 + /tmp/bats-run-19605/bats.19627.src: line 2: ./project.sh: No such file or directory + + 1 test, 1 failure + +Okay, our test is red. Obviously, the project.sh doesn't exist, so we create the file `src/project.sh`: + +.. code-block:: console + + mkdir src/ + echo '#!/usr/bin/env bash' > src/project.sh + chmod a+x src/project.sh + +A new test run gives us + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ can run our script + (in test file test/test.bats, line 2) + `./project.sh' failed with status 127 + /tmp/bats-run-19605/bats.19627.src: line 2: ./project.sh: No such file or directory + + 1 test, 1 failure + +Oh, we still used the wrong path. No problem, we just need to use the correct path to `project.sh`. +Since we're still in the same directory as when we started `bats`, we can simply do: + +.. code-block:: bash + + @test "can run our script" { + ./src/project.sh + } + +and get: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✓ can run our script + + 1 test, 0 failures + +Yesss! But that victory feels shallow: What if somebody less competent than us starts bats from another directory? + +Let's do some setup +------------------- + +The obvious solution to becoming independent of `$PWD` is using some fixed anchor point in the filesystem. +We can use the path to the test file itself as an anchor and rely on the internal project structure. +Since we are lazy people and want to treat our project's files as first class citizens in the executable world, we will also put them on the `$PATH`. +Our new `test/test.bats` now looks like this: + +.. code-block:: bash + + setup() { + # get the containing directory of this file + # use $BATS_TEST_FILENAME instead of ${BASH_SOURCE[0]} or $0, + # as those will point to the bats executable's location or the preprocessed file respectively + DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + # make executables in src/ visible to PATH + PATH="$DIR/../src:$PATH" + } + + @test "can run our script" { + # notice the missing ./ + # As we added src/ to $PATH, we can omit the relative path to `src/project.sh`. + project.sh + } + +still giving us: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✓ can run our script + + 1 test, 0 failures + +It still works as expected. This is because the newly added `setup` function put the absolute path to `src/` onto `$PATH`. +This setup function is automatically called before each test. +Therefore, our test could execute `project.sh` directly, without using a (relative) path. + +.. important:: + + The `setup` function will be called before each individual test in the file. + Each file can only define one setup function for all tests in the file. + However, the setup functions can differ between different files. + +Dealing with output +------------------- + +Okay, we have a green test but our executable does not do anything useful. +To keep things simple, let us start with an error message. Our new `src/project.sh` now reads: + +.. code-block:: bash + + #!/usr/bin/env bash + + echo "Welcome to our project!" + + echo "NOT IMPLEMENTED!" >&2 + exit 1 + +And gives is this test output: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ can run our script + (in test file test/test.bats, line 11) + `project.sh' failed + Welcome to our project! + NOT IMPLEMENTED! + + 1 test, 1 failure + +Okay, our test failed, because we now exit with 1 instead of 0. +Additionally, we see the stdout and stderr of the failing program. + +Our goal now is to retarget our test and check that we get the welcome message. +bats-assert gives us some help with this, so we should now load it (and its dependency bats-support), +so we change `test/test.bats` to + +.. code-block:: bash + + setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + # ... the remaining setup is unchanged + + # get the containing directory of this file + # use $BATS_TEST_FILENAME instead of ${BASH_SOURCE[0]} or $0, + # as those will point to the bats executable's location or the preprocessed file respectively + DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + # make executables in src/ visible to PATH + PATH="$DIR/../src:$PATH" + } + + @test "can run our script" { + run project.sh # notice `run`! + assert_output 'Welcome to our project!' + } + +which gives us the following test output: + +.. code-block:: console + + $ LANG=C ./test/bats/bin/bats test/test.bats + ✗ can run our script + (from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194, + in test file test/test.bats, line 14) + `assert_output 'Welcome to our project!'' failed + + -- output differs -- + expected (1 lines): + Welcome to our project! + actual (2 lines): + Welcome to our project! + NOT IMPLEMENTED! + -- + + + 1 test, 1 failure + +The first change in this output is the failure description. We now fail on assert_output instead of the call itself. +We prefixed our call to `project.sh` with `run`, which is a function provided by bats that executes the command it gets passed as parameters. +Then, `run` sucks up the stdout and stderr of the command it ran and stores it in `$output`, stores the exit code in `$status` and returns 0. +This means `run` never fails the test and won't generate any context/output in the log of a failed test on its own. + +Marking the test as failed and printing context information is up to the consumers of `$status` and `$output`. +`assert_output` is such a consumer, it compares `$output` to the parameter it got and tells us quite succinctly that it did not match in this case. + +For our current test we don't care about any other output or the error message, so we want it gone. +`grep` is always at our fingertips, so we tape together this ramshackle construct + +.. code-block:: bash + + run project.sh 2>&1 | grep Welcome + +which gives us the following test result: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ can run our script + (in test file test/test.bats, line 13) + `run project.sh | grep Welcome' failed + + 1 test, 1 failure + +Huh, what is going on? Why does it fail the `run` line again? + +This is a common mistake that can happen when our mind parses the file differently than the bash parser. +`run` is just a function, so the pipe won't actually be forwarded into the function. Bash reads this as `(run project.sh) | grep Welcome`, +instead of our intended `run (project.sh | grep Welcome)`. + +Unfortunately, the latter is not valid bash syntax, so we have to work around it, e.g. by using a function: + +.. code-block:: bash + + get_projectsh_welcome_message() { + project.sh 2>&1 | grep Welcome + } + + @test "Check welcome message" { + run get_projectsh_welcome_message + assert_output 'Welcome to our project!' + } + +Now our test passes again but having to write a function each time we want only a partial match does not accommodate our laziness. +Isn't there an app for that? Maybe we should look at the documentation? + + Partial matching can be enabled with the --partial option (-p for short). When used, the assertion fails if the expected substring is not found in $output. + + -- the documentation for `assert_output `_ + +Okay, so maybe we should try that: + +.. code-block:: bash + + @test "Check welcome message" { + run project.sh + assert_output --partial 'Welcome to our project!' + } + +Aaannnd ... the test stays green. Yay! + +There are many other asserts and options but this is not the place for all of them. +Skimming the documentation of `bats-assert `_ will give you a good idea what you can do. +You should also have a look at the other helper libraries `here `_ like `bats-file `_, +to avoid reinventing the wheel. + + +Cleaning up your mess +--------------------- + +Often our setup or tests leave behind some artifacts that clutter our test environment. +You can define a `teardown` function which will be called after each test, regardless whether it failed or not. + +For example, we now want our project.sh to only show the welcome message on the first invocation. +So we change our test to this: + +.. code-block:: bash + + @test "Show welcome message on first invocation" { + run project.sh + assert_output --partial 'Welcome to our project!' + + run project.sh + refute_output --partial 'Welcome to our project!' + } + +This test fails as expected: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ Show welcome message on first invocation + (from function `refute_output' in file test/test_helper/bats-assert/src/refute_output.bash, line 189, + in test file test/test.bats, line 17) + `refute_output --partial 'Welcome to our project!'' failed + + -- output should not contain substring -- + substring (1 lines): + Welcome to our project! + output (2 lines): + Welcome to our project! + NOT IMPLEMENTED! + -- + + + 1 test, 1 failure + +Now, to get the test green again, we want to store the information that we already ran in the file `/tmp/bats-tutorial-project-ran`, +so our `src/project.sh` becomes: + +.. code-block:: bash + + #!/usr/bin/env bash + + FIRST_RUN_FILE=/tmp/bats-tutorial-project-ran + + if [[ ! -e "$FIRST_RUN_FILE" ]]; then + echo "Welcome to our project!" + touch "$FIRST_RUN_FILE" + fi + + echo "NOT IMPLEMENTED!" >&2 + exit 1 + +And our test says: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✓ Show welcome message on first invocation + + 1 test, 0 failures + +Nice, we're done, or are we? Running the test again now gives: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + ✗ Show welcome message on first invocation + (from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 186, + in test file test/test.bats, line 14) + `assert_output --partial 'Welcome to our project!'' failed + + -- output does not contain substring -- + substring : Welcome to our project! + output : NOT IMPLEMENTED! + -- + + + 1 test, 1 failure + +Now the first assert failed, because of the leftover `$FIRST_RUN_FILE` from the last test run. + +Luckily, bats offers the `teardown` function, which can take care of that, we add the following code to `test/test.bats`: + +.. code-block:: bash + + teardown() { + rm -f /tmp/bats-tutorial-project-ran + } + +Now running the test again first give us the same error, as the teardown has not run yet. +On the second try we get a clean `/tmp` folder again and our test passes consistently now. + +It is worth noting that we could do this `rm` in the test code itself but it would get skipped on failures. + +.. important:: + + A test ends at its first failure. None of the subsequent commands in this test will be executed. + The `teardown` function runs after each individual test in a file, regardless of test success or failure. + Similarly to `setup`, each `.bats` file can have its own `teardown` function which will be the same for all tests in the file. + +Test what you can +----------------- + +Sometimes tests rely on the environment to provide infrastructure that is needed for the test. +If not all test environments provide this infrastructure but we still want to test on them, +it would be unhelpful to get errors on parts that are not testable. + +Bats provides you with the `skip` command which can be used in `setup` and `test`. + +.. tip:: + + You should `skip` as early as you know it does not make sense to continue. + +In our example project we rewrite the welcome message test to `skip` instead of doing cleanup: + +.. code-block:: bash + + teardown() { + : # Look Ma! No cleanup! + } + + @test "Show welcome message on first invocation" { + if [[ -e /tmp/bats-tutorial-project-ran ]]; then + skip 'The FIRST_RUN_FILE already exists' + fi + + run project.sh + assert_output --partial 'Welcome to our project!' + + run project.sh + refute_output --partial 'Welcome to our project!' + } + +The first test run still works due to the cleanup from the last round. However, our second run gives us: + +.. code-block:: console + + $ ./test/bats/bin/bats test/test.bats + - Show welcome message on first invocation (skipped: The FIRST_RUN_FILE already exists) + + 1 test, 0 failures, 1 skipped + +.. important:: + + Skipped tests won't fail a test suite and are counted separately. + No test command after `skip` will be executed. If an error occurs before `skip`, the test will fail. + An optional reason can be passed to `skip` and will be printed in the test output. + +Setting up a multifile test suite +--------------------------------- + +With a growing project, putting all tests into one file becomes unwieldy. +For our example project, we will extract functionality into the additional file `src/helper.sh`: + +.. code-block:: bash + + #!/usr/bin/env bash + + _is_first_run() { + local FIRST_RUN_FILE=/tmp/bats-tutorial-project-ran + if [[ ! -e "$FIRST_RUN_FILE" ]]; then + touch "$FIRST_RUN_FILE" + return 0 + fi + return 1 + } + +This allows for testing it separately in a new file `test/helper.bats`: + +.. code-block:: bash + + setup() { + load 'test_helper/common-setup' + _common_setup + + source "$PROJECT_ROOT/src/helper.sh" + } + + teardown() { + rm -f "$NON_EXISTANT_FIRST_RUN_FILE" + rm -f "$EXISTING_FIRST_RUN_FILE" + } + + @test "Check first run" { + NON_EXISTANT_FIRST_RUN_FILE=$(mktemp -u) # only create the name, not the file itself + + assert _is_first_run + refute _is_first_run + refute _is_first_run + + EXISTING_FIRST_RUN_FILE=$(mktemp) + refute _is_first_run + refute _is_first_run + } + +Since the setup function would have duplicated much of the other files', we split that out into the file `test/test_helper/common-setup.bash`: + +.. code-block:: bash + + #!/usr/bin/env bash + + _common_setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + # get the containing directory of this file + # use $BATS_TEST_FILENAME instead of ${BASH_SOURCE[0]} or $0, + # as those will point to the bats executable's location or the preprocessed file respectively + PROJECT_ROOT="$( cd "$( dirname "$BATS_TEST_FILENAME" )/.." >/dev/null 2>&1 && pwd )" + # make executables in src/ visible to PATH + PATH="$PROJECT_ROOT/src:$PATH" + } + +with the following `setup` in `test/test.bats`: + +.. code-block:: bash + + setup() { + load 'test_helper/common-setup' + _common_setup + } + +Please note, that we gave our helper the extension `.bash`, which is automatically appended by `load`. + +.. important:: + + `load` automatically tries to append `.bash` to its argument. + +In our new `test/helper.bats` we can see, that loading `.sh` is simply done via `source`. + +.. tip:: + + Avoid using `load` and `source` outside of any functions. + If there is an error in the test file's "free code", the diagnostics are much worse than for code in `setup` or `@test`. + +With the new changes in place, we can run our tests again. However, our previous run command does not include the new file. +You could add the new file to the parameter list, e.g. by running `./test/bats/bin/bats test/*.bats`. +However, bats also can handle directories: + +.. code-block:: console + + $ ./test/bats/bin/bats test/ + ✓ Check first run + - Show welcome message on first invocation (skipped: The FIRST_RUN_FILE already exists) + + 2 tests, 0 failures, 1 skipped + +In this mode, bats will pick up all `.bats` files in the directory it was given. There is an additional `-r` switch that will recursively search for more `.bats` files. +However, in our project layout this would pick up the test files of bats itself from `test/bats/test`. We don't have test subfolders anyways, so we can do without `-r`. + + +Avoiding costly repeated setups +------------------------------- + +We already have seen the `setup` function in use, which is called before each test. +Sometimes our setup is very costly, such as booting up a service just for testing. +If we can reuse the same setup across multiple tests, we might want to do only one setup before all these tests. + +This usecase is exactly what the `setup_file` function was created for. +It can be defined per file and will run before all tests of the respective file. +Similarly, we have `teardown_file`, which will run after all tests of the file, even when you abort a test run or a test failed. + +As an example, we want to add an echo server capability to our project. First, we add the following `server.bats` to our suite: + +.. code-block:: bash + + setup_file() { + load 'test_helper/common-setup' + _common_setup + PORT=$(project.sh start-echo-server 2>&1 >/dev/null) + export PORT + } + + @test "server is reachable" { + nc -z localhost "$PORT" + } + +Which will obviously fail: + +Note that `export PORT` to make it visible to the test! +Running this gives us: + +.. + TODO: Update this example with fixed test name reporting from setup_file? (instead of "✗ ") + +.. code-block:: console + + $ ./test/bats/bin/bats test/server.bats + ✗ + (from function `setup_file' in test file test/server.bats, line 4) + `PORT=$(project.sh start-echo-server >/dev/null 2>&1)' failed + + 1 test, 1 failure + +Now that we got our red test, we need to get it green again. +Our new `project.sh` now ends with: + +.. code-block:: bash + + case $1 in + start-echo-server) + echo "Starting echo server" + PORT=2000 + ncat -l $PORT -k -c 'xargs -n1 echo' 2>/dev/null & # don't keep open this script's stderr + echo $! > /tmp/project-echo-server.pid + echo "$PORT" >&2 + ;; + *) + echo "NOT IMPLEMENTED!" >&2 + exit 1 + ;; + esac + +and the tests now say + +.. code-block:: console + + $ LANG=C ./test/bats/bin/bats test/server.bats + ✓ server is reachable + + 1 test, 0 failures + +However, running this a second time gives: + +.. code-block:: console + + $ ./test/bats/bin/bats test/server.bats + ✗ server is reachable + (in test file test/server.bats, line 14) + `nc -z -w 2 localhost "$PORT"' failed + 2000 + Ncat: bind to :::2000: Address already in use. QUITTING. + nc: port number invalid: 2000 + Ncat: bind to :::2000: Address already in use. QUITTING. + + 1 test, 1 failure + +Obviously, we did not turn off our server after testing. +This is a task for `teardown_file` in `server.bats`: + +.. code-block:: bash + + teardown_file() { + project.sh stop-echo-server + } + +Our `project.sh` should also get the new command: + +.. code-block:: bash + + stop-echo-server) + kill "$(< "/tmp/project-echo-server.pid")" + rm /tmp/project-echo-server.pid + ;; + +Now starting our tests again will overwrite the .pid file with the new instance's, so we have to do manual cleanup once. +From now on, our test should clean up after itself. + +.. note:: + + `teardown_file` will run regardless of tests failing or succeeding. diff --git a/test/bats/docs/source/usage.md b/test/bats/docs/source/usage.md new file mode 100644 index 000000000..bdbe407d5 --- /dev/null +++ b/test/bats/docs/source/usage.md @@ -0,0 +1,114 @@ +# Usage + +Bats comes with two manual pages. After installation you can view them with `man +1 bats` (usage manual) and `man 7 bats` (writing test files manual). Also, you +can view the available command line options that Bats supports by calling Bats +with the `-h` or `--help` options. These are the options that Bats currently +supports: + +``` eval_rst +.. program-output:: ../../bin/bats --help +``` + +To run your tests, invoke the `bats` interpreter with one or more paths to test +files ending with the `.bats` extension, or paths to directories containing test +files. (`bats` will only execute `.bats` files at the top level of each +directory; it will not recurse unless you specify the `-r` flag.) + +Test cases from each file are run sequentially and in isolation. If all the test +cases pass, `bats` exits with a `0` status code. If there are any failures, +`bats` exits with a `1` status code. + +When you run Bats from a terminal, you'll see output as each test is performed, +with a check-mark next to the test's name if it passes or an "X" if it fails. + +```text +$ bats addition.bats + ✓ addition using bc + ✓ addition using dc + +2 tests, 0 failures +``` + +If Bats is not connected to a terminal—in other words, if you run it from a +continuous integration system, or redirect its output to a file—the results are +displayed in human-readable, machine-parsable [TAP format][tap-format]. + +You can force TAP output from a terminal by invoking Bats with the `--formatter tap` +option. + +```text +$ bats --formatter tap addition.bats +1..2 +ok 1 addition using bc +ok 2 addition using dc +``` + +With `--formatter junit`, it is possible +to output junit-compatible report files. + +```text +$ bats --formatter junit addition.bats +1..2 +ok 1 addition using bc +ok 2 addition using dc +``` + +If you have your own formatter, you can use an absolute path to the executable +to use it: + +```bash +$ bats --formatter /absolute/path/to/my-formatter addition.bats +addition using bc WORKED +addition using dc FAILED +``` + +You can also generate test report files via `--report-formatter` which accepts +the same options as `--formatter`. By default, the file is stored in the current +workdir. However, it may be placed elsewhere by specifying the `--output` flag. + +```text +$ bats --report-formatter junit addition.bats --output /tmp +1..2 +ok 1 addition using bc +ok 2 addition using dc + +$ cat /tmp/report.xml + + + + + + + +``` + +## Parallel Execution + +``` eval_rst +.. versionadded:: 1.0.0 +``` + +By default, Bats will execute your tests serially. However, Bats supports +parallel execution of tests (provided you have [GNU parallel][gnu-parallel] or +a compatible replacement installed) using the `--jobs` parameter. This can +result in your tests completing faster (depending on your tests and the testing +hardware). + +Ordering of parallelised tests is not guaranteed, so this mode may break suites +with dependencies between tests (or tests that write to shared locations). When +enabling `--jobs` for the first time be sure to re-run bats multiple times to +identify any inter-test dependencies or non-deterministic test behaviour. + +When parallelizing, the results of a file only become visible after it has been finished. +You can use `--no-parallelize-across-files` to get immediate output at the cost of reduced +overall parallelity, as parallelization will only happen within files and files will be run +sequentially. + +If you have files where tests within the file would interfere with each other, you can use +`--no-parallelize-within-files` to disable parallelization within all files. +If you want more fine-grained control, you can `export BATS_NO_PARALLELIZE_WITHIN_FILE=true` in `setup_file()` +or outside any function to disable parallelization only within the containing file. + +[tap-format]: https://testanything.org +[gnu-parallel]: https://www.gnu.org/software/parallel/ diff --git a/test/bats/docs/source/warnings/BW01.rst b/test/bats/docs/source/warnings/BW01.rst new file mode 100644 index 000000000..8ae8bb67d --- /dev/null +++ b/test/bats/docs/source/warnings/BW01.rst @@ -0,0 +1,17 @@ +BW01: `run`'s command `` exited with code 127, indicating 'Command not found'. Use run's return code checks, e.g. `run -127`, to fix this message. +=========================================================================================================================================================== + +Due to `run`'s default behavior of always succeeding, errors in the command string can remain hidden from the user, e.g.[here](https://github.com/bats-core/bats-core/issues/578). +As a proxy for this problem, the return code is checked for value 127 ("Command not found"). + +How to fix +---------- + +If your command should actually return code 127, then you can simply use `run -127 ` to state your intent and the message will go away. + +If your command should not return 127, you should fix the problem with the command. +Take a careful look at the command string in the warning message, to see if it contains code that you did not intend to run. + +If your command should sometimes return 127, but never 0, you can use `run ! `. + +If your command can sometimes return 127 and sometimes 0, the please submit an issue. \ No newline at end of file diff --git a/test/bats/docs/source/warnings/BW02.rst b/test/bats/docs/source/warnings/BW02.rst new file mode 100644 index 000000000..33936128e --- /dev/null +++ b/test/bats/docs/source/warnings/BW02.rst @@ -0,0 +1,46 @@ +BW02: requires at least BATS_VERSION=. Use `bats_require_minimum_version ` to fix this message. +=========================================================================================================================== + +Using a feature that is only available starting with a certain version can be a problem when your tests also run on older versions of Bats. +In most cases, running this code in older versions will generate an error due to a missing command. +However, in cases like `run`'s where old version simply take all parameters as command to execute, the failure can be silent. + +How to fix +---------- + +When you encounter this warning, you can simply guard your code with `bats_require_minimum_version ` as the message says. +For example, consider the following code: + +.. code-block:: bash + + @test test { + bats_require_minimum_version 1.5.0 + # pre 1.5.0 the flag --separate-stderr would be interpreted as command to run + run --separate-stderr some-command + [ $output = "blablabla" ] + } + + +The call to `bats_require_minimum_version` can be put anywhere before the warning generating command, even in `setup`, `setup_file`, or even outside any function. +This can be used to give fine control over the version dependencies: + +.. code-block:: bash + + @test test { + bats_require_minimum_version 1.5.0 + # pre 1.5.0 the flag --separate-stderr would be interpreted as command to run + run --separate-stderr some-command + [ $output = "blablabla" ] + } + + @test test2 { + run some-other-command # no problem executing on earlier version + } + + +If the above code is executed on a system with a `BATS_VERSION` pre 1.5.0, the first test will fail on `bats_require_minimum_version 1.5.0`. + +Instances: +---------- + +- run's non command parameters like `--keep-empty-lines` are only available since 1.5.0 \ No newline at end of file diff --git a/test/bats/docs/source/warnings/BW03.rst b/test/bats/docs/source/warnings/BW03.rst new file mode 100644 index 000000000..2a0adc0dc --- /dev/null +++ b/test/bats/docs/source/warnings/BW03.rst @@ -0,0 +1,15 @@ +BW03: `setup_suite` is visible to test file '', but was not executed. It belongs into 'setup_suite.bash' to be picked up automatically. +============================================================================================================================================= + +In contrast to the other setup functions, `setup_suite` must not be defined in `*.bats` files but in `setup_suite.bash`. +When a file is executed and sees `setup_suite` defined but not run before the tests, this warning will be printed. + +How to fix +---------- + +The fix depends on your actual intention. There are basically two cases: + +1. You want a setup before all tests and accidentally put `setup_suite` into a test file instead of `setup_suite.bash`. + Simply move `setup_suite` (and `teardown_suite`!) into `setup_suite.bash`. +2. You did not mean to run a setup before any test but need to defined a function named `setup_suite` in your test file. + In this case, you can silence this warning by assigning `BATS_SETUP_SUITE_COMPLETED='suppress BW03'`. \ No newline at end of file diff --git a/test/bats/docs/source/warnings/index.rst b/test/bats/docs/source/warnings/index.rst new file mode 100644 index 000000000..957fc4305 --- /dev/null +++ b/test/bats/docs/source/warnings/index.rst @@ -0,0 +1,28 @@ +Warnings +======== + +Starting with version 1.7.0 Bats shows warnings about issues it found during the test run. +They are printed on stderr after all other output: + +.. code-block:: bash + + BW01.bats + ✓ Trigger BW01 + + 1 test, 0 failures + + + The following warnings were encountered during tests: + BW01: `run`'s command `=0 actually-intended-command with some args` exited with code 127, indicating 'Command not found'. Use run's return code checks, e.g. `run -127`, to fix this message. + (from function `run' in file lib/bats-core/test_functions.bash, line 299, + in test file test/fixtures/warnings/BW01.bats, line 3) + +A warning will not make a successful run fail but should be investigated and taken seriously, since it hints at a possible error. + +Currently, Bats emits the following warnings: + +.. toctree:: + + BW01 + BW02 + BW03 \ No newline at end of file diff --git a/test/bats/docs/source/writing-tests.md b/test/bats/docs/source/writing-tests.md new file mode 100644 index 000000000..25541aabc --- /dev/null +++ b/test/bats/docs/source/writing-tests.md @@ -0,0 +1,614 @@ +# Writing tests + +Each Bats test file is evaluated _n+1_ times, where _n_ is the number of +test cases in the file. The first run counts the number of test cases, +then iterates over the test cases and executes each one in its own +process. + +For more details about how Bats evaluates test files, see [Bats Evaluation +Process][bats-eval] on the wiki. + +For sample test files, see [examples](https://github.com/bats-core/bats-core/tree/master/docs/examples). + +[bats-eval]: https://github.com/bats-core/bats-core/wiki/Bats-Evaluation-Process + +## Tagging tests + +Starting with version 1.8.0, Bats comes with a tagging system that allows users +to categorize their tests and filter according to those categories. + +Each test has a list of tags attached to it. Without specification, this list is empty. +Tags can be defined in two ways. The first being `# bats test_tags=`: + +```bash +# bats test_tags=tag:1, tag:2, tag:3 +@test "first test" { + # ... +} + +@test "second test" { + # ... +} +``` + +These tags (`tag:1`, `tag:2`, `tag:3`) will be attached to the test `first test`. +The second test will have no tags attached. Values defined in the `# bats test_tags=` +directive will be assigned to the next `@test` that is being encountered in the +file and forgotten after that. Only the value of the last `# bats test_tags=` directive +before a given test will be used. + +Sometimes, we want to give all tests in a file a set of the same tags. This can +be achieved via `# bats file_tags=`. They will be added to all tests in the file +after that directive. An additional `# bats file_tags=` directive will override +the previously defined values: + +```bash +@test "Zeroth test" { + # will have no tags +} + +# bats file_tags=a:b +# bats test_tags=c:d + +@test "First test" { + # will be tagged a:b, c:d +} + +# bats file_tags= + +@test "Second test" { + # will have no tags +} +``` + +Tags are case sensitive and must only consist of alphanumeric characters and `_`, + `-`, or `:`. They must not contain whitespaces! +The colon is intended as a separator for (recursive) namespacing. + +Tag lists must be separated by commas and are allowed to contain whitespace. +They must not contain empty tags like `test_tags=,b` (first tag is empty), +`test_tags=a,,c`, `test_tags=a, ,c` (second tag is only whitespace/empty), +`test_tags=a,b,` (third tag is empty). + +Every tag starting with `bats:` (case insensitive!) is reserved for Bats' +internal use. + +### Special tags + +#### Focusing on tests with `bats:focus` tag + +If a test with the tag `bats:focus` is encountered in a test suite, +all other tests will be filtered out and only those tagged with this tag will be executed. + +In focus mode, the exit code of successful runs will be overriden to 1 to prevent CI from silently running on a subset of tests due to an accidentally commited `bats:focus` tag. +Should you require the true exit code, e.g. for a `git bisect` operation, you can disable this behavior by setting +`BATS_NO_FAIL_FOCUS_RUN=1` when running `bats`, but make sure not to commit this to CI! + +### Filtering execution + +Tags can be used for more finegrained filtering of which tests to run via `--filter-tags`. +This accepts a comma separated list of tags. Only tests that match all of these +tags will be executed. For example, `bats --filter-tags a,b,c` will pick up tests +with tags `a,b,c`, but not tests that miss one or more of those tags. + +Additionally, you can specify negative tags via `bats --filter-tags a,!b,c`, +which now won't match tests with tags `a,b,c`, due to the `b`, but will select `a,c`. +To put it more formally, `--filter-tags` is a boolean conjunction. + +To allow for more complex queries, you can specify multiple `--filter-tags`. +A test will be executed, if it matches at least one of them. +This means multiple `--filter-tags` form a boolean disjunction. + +A query of `--filter-tags a,!b --filter-tags b,c` can be translated to: +Execute only tests that (have tag a, but not tag b) or (have tag b and c). + +An empty tag list matches tests without tags. + +## Comment syntax + +External tools (like `shellcheck`, `shfmt`, and various IDE's) may not support +the standard `.bats` syntax. Because of this, we provide a valid `bash` +alternative: + +```bash +function invoking_foo_without_arguments_prints_usage { #@test + run foo + [ "$status" -eq 1 ] + [ "${lines[0]}" = "usage: foo " ] +} +``` + +When using this syntax, the function name will be the title in the result output +and the value checked when using `--filter`. + +## `run`: Test other commands + +Many Bats tests need to run a command and then make assertions about its exit +status and output. Bats includes a `run` helper that invokes its arguments as a +command, saves the exit status and output into special global variables, and +then returns with a `0` status code so you can continue to make assertions in +your test case. + +For example, let's say you're testing that the `foo` command, when passed a +nonexistent filename, exits with a `1` status code and prints an error message. + +```bash +@test "invoking foo with a nonexistent file prints an error" { + run foo nonexistent_filename + [ "$status" -eq 1 ] + [ "$output" = "foo: no such file 'nonexistent_filename'" ] + [ "$BATS_RUN_COMMAND" = "foo nonexistent_filename" ] + +} +``` + +The `$status` variable contains the status code of the command, the +`$output` variable contains the combined contents of the command's standard +output and standard error streams, and the `$BATS_RUN_COMMAND` string contains the +command and command arguments passed to `run` for execution. + +If invoked with one of the following as the first argument, `run` +will perform an implicit check on the exit status of the invoked command: + +```pre + -N expect exit status N (0-255), fail if otherwise + ! expect nonzero exit status (1-255), fail if command succeeds +``` + +We can then write the above more elegantly as: + +```bash +@test "invoking foo with a nonexistent file prints an error" { + run -1 foo nonexistent_filename + [ "$output" = "foo: no such file 'nonexistent_filename'" ] +} +``` + +A third special variable, the `$lines` array, is available for easily accessing +individual lines of output. For example, if you want to test that invoking `foo` +without any arguments prints usage information on the first line: + +```bash +@test "invoking foo without arguments prints usage" { + run -1 foo + [ "${lines[0]}" = "usage: foo " ] +} +``` + +__Note:__ The `run` helper executes its argument(s) in a subshell, so if +writing tests against environmental side-effects like a variable's value +being changed, these changes will not persist after `run` completes. + +By default `run` leaves out empty lines in `${lines[@]}`. Use +`run --keep-empty-lines` to retain them. + +Additionally, you can use `--separate-stderr` to split stdout and stderr +into `$output`/`$stderr` and `${lines[@]}`/`${stderr_lines[@]}`. + +All additional parameters to run should come before the command. +If you want to run a command that starts with `-`, prefix it with `--` to +prevent `run` from parsing it as an option. + +### When not to use `run` + +In case you only need to check the command succeeded, it is better to not use `run`, since the following code + +```bash +run -0 command args ... +``` + +is equivalent to + +```bash +command args ... +``` + +(because bats sets `set -e` for all tests). + +__Note__: In contrast to the above, testing that a command failed is best done via + +```bash +run ! command args ... +``` + +because + +```bash +! command args ... +``` + +will only fail the test if it is the last command and thereby determines the test function's exit code. +This is due to Bash's decision to (counterintuitively?) not trigger `set -e` on `!` commands. +(See also [the associated gotcha](https://bats-core.readthedocs.io/en/stable/gotchas.html#my-negated-statement-e-g-true-does-not-fail-the-test-even-when-it-should)) + + +### `run` and pipes + +Don't fool yourself with pipes when using `run`. Bash parses the pipe outside of `run`, not internal to its command. Take this example: + +```bash +run command args ... | jq -e '.limit == 42' +``` + +Here, `jq` receives no input (which is captured by `run`), +executes no filters, and always succeeds, so the test does not work as +expected. + +Instead use a Bash subshell: + +```bash +run bash -c "command args ... | jq -e '.limit == 42'" +``` + +This subshell is a fresh Bash environment, and will only inherit variables +and functions that are exported into it. + +```bash +limit() { jq -e '.limit == 42'; } +export -f limit +run bash -c "command args ... | limit" +``` + + +## `load`: Share common code + +You may want to share common code across multiple test files. Bats +includes a convenient `load` command for sourcing a Bash source files +relative to the current test file and from absolute paths. + +For example, if you have a Bats test in `test/foo.bats`, the command + +```bash +load test_helper.bash +``` + +will source the script `test/test_helper.bash` in your test file (limitations +apply, see below). This can be useful for sharing functions to set up your +environment or load fixtures. `load` delegates to Bash's `source` command after +resolving paths. + +If `load` encounters errors - e.g. because the targeted source file +errored - it will print a message with the failing library and Bats +exits. + +To allow to use `load` in conditions `bats_load_safe` has been added. +`bats_load_safe` prints a message and returns `1` if a source file cannot be +loaded instead of exiting Bats. +Aside from that `bats_load_safe` acts exactly like `load`. + +As pointed out by @iatrou in https://www.tldp.org/LDP/abs/html/declareref.html, +using the `declare` builtin restricts scope of a variable. Thus, since actual +`source`-ing is performed in context of the `load` function, `declare`d symbols +will _not_ be made available to callers of `load`. + +### `load` argument resolution + +`load` supports the following arguments: + +- absolute paths +- relative paths (to the current test file) + +> For backwards compatibility `load` first searches for a file ending in +> `.bash` (e.g. `load test_helper` searches for `test_helper.bash` before +> it looks for `test_helper`). This behaviour is deprecated and subject to +> change, please use exact filenames instead. + +If `argument` is an absolute path `load` tries to determine the load +path directly. + +If `argument` is a relative path or a name `load` looks for a matching +path in the directory of the current test. + +## `bats_load_library`: Load system wide libraries + +Some libraries are installed on the system, e.g. by `npm` or `brew`. +These should not be `load`ed, as their path depends on the installation method. +Instead, one should use `bats_load_library` together with setting +`BATS_LIB_PATH`, a `PATH`-like colon-delimited variable. + +`bats_load_library` has two modes of resolving requests: + +1. by relative path from the `BATS_LIB_PATH` to a file in the library +2. by library name, expecting libraries to have a `load.bash` entrypoint + +For example if your `BATS_LIB_PATH` is set to +`~/.bats/libs:/usr/lib/bats`, then `bats_load_library test_helper` +would look for existing files with the following paths: + +- `~/.bats/libs/test_helper` +- `~/.bats/libs/test_helper/load.bash` +- `/usr/lib/bats/test_helper` +- `/usr/lib/bats/test_helper/load.bash` + +The first existing file in this list will be sourced. + +If you want to load only part of a library or the entry point is not named `load.bash`, +you have to include it in the argument: +`bats_load_library library_name/file_to_load` will try + +- `~/.bats/libs/library_name/file_to_load` +- `~/.bats/libs/library_name/file_to_load/load.bash` +- `/usr/lib/bats/library_name/file_to_load` +- `/usr/lib/bats/library_name/file_to_load/load.bash` + +Apart from the changed lookup rules, `bats_load_library` behaves like `load`. + +__Note:__ As seen above `load.bash` is the entry point for libraries and +meant to load more files from its directory or other libraries. + +__Note:__ Obviously, the actual `BATS_LIB_PATH` is highly dependent on the environment. +To maintain a uniform location across systems, (distribution) package maintainers +are encouraged to use `/usr/lib/bats/` as the install path for libraries where possible. +However, if the package manager has another preferred location, like `npm` or `brew`, +you should use this instead. + +## `skip`: Easily skip tests + +Tests can be skipped by using the `skip` command at the point in a test you wish +to skip. + +```bash +@test "A test I don't want to execute for now" { + skip + run foo + [ "$status" -eq 0 ] +} +``` + +Optionally, you may include a reason for skipping: + +```bash +@test "A test I don't want to execute for now" { + skip "This command will return zero soon, but not now" + run foo + [ "$status" -eq 0 ] +} +``` + +Or you can skip conditionally: + +```bash +@test "A test which should run" { + if [ foo != bar ]; then + skip "foo isn't bar" + fi + + run foo + [ "$status" -eq 0 ] +} +``` + +__Note:__ `setup` and `teardown` hooks still run for skipped tests. + +## `setup` and `teardown`: Pre- and post-test hooks + +You can define special `setup` and `teardown` functions, which run before and +after each test case, respectively. Use these to load fixtures, set up your +environment, and clean up when you're done. + +You can also define `setup_file` and `teardown_file`, which will run once before +the first test's `setup` and after the last test's `teardown` for the containing +file. Variables that are exported in `setup_file` will be visible to all following +functions (`setup`, the test itself, `teardown`, `teardown_file`). + +Similarly, there is `setup_suite` (and `teardown_suite`) which run once before (and +after) all tests of the test run. + +__Note:__ As `setup_suite` and `teardown_suite` are intended for all files in a suite, +they must be defined in a separate `setup_suite.bash` file. Automatic discovery works +by searching for `setup_suite.bash` in the folder of the first `*.bats` file of the suite. +If this automatism does not work for your usecase, you can work around by specifying +`--setup-suite-file` on the `bats` command. If you have a `setup_suite.bash`, it must define +`setup_suite`! However, defining `teardown_suite` is optional. + + +
+ Example of setup/{,_file,_suite} (and teardown{,_file,_suite}) call order +For example the following call order would result from two files (file 1 with +tests 1 and 2, and file 2 with test3) with a corresponding `setup_suite.bash` file being tested: + +```text +setup_suite # from setup_suite.bash + setup_file # from file 1, on entering file 1 + setup + test1 + teardown + setup + test2 + teardown + teardown_file # from file 1, on leaving file 1 + setup_file # from file 2, on enter file 2 + setup + test3 + teardown + teardown_file # from file 2, on leaving file 2 +teardown_suite # from setup_suite.bash +``` + +
+ + +Note that the `teardown*` functions can fail a test, if their return code is nonzero. +This means, using `return 1` or having the last command in teardown fail, will +fail the teardown. Unlike `@test`, failing commands within `teardown` won't +trigger failure as ERREXIT is disabled. + + +
+ Example of different teardown failure modes + +```bash +teardown() { + false # this will fail the test, as it determines the return code +} + +teardown() { + false # this won't fail the test ... + echo some more code # ... and this will be executed too! +} + +teardown() { + return 1 # this will fail the test, but the rest won't be executed + echo some more code +} + +teardown() { + if true; then + false # this will also fail the test, as it is the last command in this function + else + true + fi +} +``` + +
+ + +## `bats_require_minimum_version ` + +Added in [v1.7.0](https://github.com/bats-core/bats-core/releases/tag/v1.7.0) + +Code for newer versions of Bats can be incompatible with older versions. +In the best case this will lead to an error message and a failed test suite. +In the worst case, the tests will pass erroneously, potentially masking a failure. + +Use `bats_require_minimum_version ` to avoid this. +It communicates in a concise manner, that you intend the following code to be run +under the given Bats version or higher. + +Additionally, this function will communicate the current Bats version floor to +subsequent code, allowing e.g. Bats' internal warning to give more informed warnings. + +__Note__: By default, calling `bats_require_minimum_version` with versions before +Bats 1.7.0 will fail regardless of the required version as the function is not +available. However, you can use the +[bats-backports plugin](https://github.com/bats-core/bats-backports) to make +your code usable with older versions, e.g. during migration while your CI system +is not yet upgraded. + +## Code outside of test cases + +In general you should avoid code outside tests, because each test file will be evaluated many times. +However, there are situations in which this might be useful, e.g. when you want to check for dependencies +and fail immediately if they're not present. + +In general, you should avoid printing outside of `@test`, `setup*` or `teardown*` functions. +Have a look at section [printing to the terminal](#printing-to-the-terminal) for more details. +## File descriptor 3 (read this if Bats hangs) + +Bats makes a separation between output from the code under test and output that +forms the TAP stream (which is produced by Bats internals). This is done in +order to produce TAP-compliant output. In the [Printing to the +terminal](#printing-to-the-terminal) section, there are details on how to use +file descriptor 3 to print custom text properly. + +A side effect of using file descriptor 3 is that, under some circumstances, it +can cause Bats to block and execution to seem dead without reason. This can +happen if a child process is spawned in the background from a test. In this +case, the child process will inherit file descriptor 3. Bats, as the parent +process, will wait for the file descriptor to be closed by the child process +before continuing execution. If the child process takes a lot of time to +complete (eg if the child process is a `sleep 100` command or a background +service that will run indefinitely), Bats will be similarly blocked for the same +amount of time. + +**To prevent this from happening, close FD 3 explicitly when running any command +that may launch long-running child processes**, e.g. `command_name 3>&-` . + +## Printing to the terminal + +Bats produces output compliant with [version 12 of the TAP protocol](https://testanything.org/tap-specification.html). The +produced TAP stream is by default piped to a pretty formatter for human +consumption, but if Bats is called with the `-t` flag, then the TAP stream is +directly printed to the console. + +This has implications if you try to print custom text to the terminal. As +mentioned in [File descriptor 3](#file-descriptor-3-read-this-if-bats-hangs), +bats provides a special file descriptor, `&3`, that you should use to print +your custom text. Here are some detailed guidelines to refer to: + +- Printing **from within a test function**: + - First you should consider if you want the text to be always visible or only + when the test fails. Text that is output directly to stdout or stderr (file + descriptor 1 or 2), ie `echo 'text'` is considered part of the test function + output and is printed only on test failures for diagnostic purposes, + regardless of the formatter used (TAP or pretty). + - To have text printed unconditionally from within a test function you need to + redirect the output to file descriptor 3, eg `echo 'text' >&3`. This output + will become part of the TAP stream. You are encouraged to prepend text printed + this way with a hash (eg `echo '# text' >&3`) in order to produce 100% TAP compliant + output. Otherwise, depending on the 3rd-party tools you use to analyze the + TAP stream, you can encounter unexpected behavior or errors. + +- Printing **from within the `setup*` or `teardown*` functions**: The same hold + true as for printing with test functions. + +- Printing **outside test or `setup*`/`teardown*` functions**: + - You should avoid printing in free code: Due to the multiple executions + contexts (`setup_file`, multiple `@test`s) of test files, output + will be printed more than once. + + - Regardless of where text is redirected to (stdout, stderr or file descriptor 3) + text is immediately visible in the terminal, as it is not piped into the formatter. + + - Text printed to stdout may interfere with formatters as it can + make output non-compliant with the TAP spec. The reason for this is that + such output will be produced before the [_plan line_][tap-plan] is printed, + contrary to the spec that requires the _plan line_ to be either the first or + the last line of the output. + + - Due to internal pipes/redirects, output to stderr is always printed first. + +[tap-plan]: https://testanything.org/tap-specification.html#the-plan + +## Special variables + +There are several global variables you can use to introspect on Bats tests: + +- `$BATS_RUN_COMMAND` is the run command used in your test case. +- `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test file. +- `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is located. +- `$BATS_TEST_NAMES` is an array of function names for each test case. +- `$BATS_TEST_NAME` is the name of the function containing the current test case. +- `BATS_TEST_NAME_PREFIX` will be prepended to the description of each test on + stdout and in reports. +- `$BATS_TEST_DESCRIPTION` is the description of the current test case. +- `BATS_TEST_RETRIES` is the maximum number of additional attempts that will be + made on a failed test before it is finally considered failed. + The default of 0 means the test must pass on the first attempt. +- `BATS_TEST_TIMEOUT` is the number of seconds after which a test (including setup) + will be aborted and marked as failed. Updates to this value in `setup()` or `@test` + cannot change the running timeout countdown, so the latest useful update location + is `setup_file()`. +- `$BATS_TEST_NUMBER` is the (1-based) index of the current test case in the test file. +- `$BATS_SUITE_TEST_NUMBER` is the (1-based) index of the current test case in the test suite (over all files). +- `$BATS_TEST_TAGS` the tags of the current test. +- `$BATS_TMPDIR` is the base temporary directory used by bats to create its + temporary files / directories. + (default: `$TMPDIR`. If `$TMPDIR` is not set, `/tmp` is used.) +- `$BATS_RUN_TMPDIR` is the location to the temporary directory used by bats to + store all its internal temporary files during the tests. + (default: `$BATS_TMPDIR/bats-run-$BATS_ROOT_PID-XXXXXX`) +- `$BATS_FILE_EXTENSION` (default: `bats`) specifies the extension of test files that should be found when running a suite (via `bats [-r] suite_folder/`) +- `$BATS_SUITE_TMPDIR` is a temporary directory common to all tests of a suite. + Could be used to create files required by multiple tests. +- `$BATS_FILE_TMPDIR` is a temporary directory common to all tests of a test file. + Could be used to create files required by multiple tests in the same test file. +- `$BATS_TEST_TMPDIR` is a temporary directory unique for each test. + Could be used to create files required only for specific tests. +- `$BATS_VERSION` is the version of Bats running the test. + +## Libraries and Add-ons + +Bats supports loading external assertion libraries and helpers. Those under `bats-core` are officially supported libraries (integration tests welcome!): + +- - common assertions for Bats +- - supporting library for Bats test helpers +- - common filesystem assertions for Bats +- - e2e tests of applications in K8s environments + +and some external libraries, supported on a "best-effort" basis: + +- (still relevant? Requires review) +- (as per #147) +- (how is this different from grayhemp/bats-mock?) diff --git a/test/bats/docs/versions.md b/test/bats/docs/versions.md new file mode 100644 index 000000000..5113077c4 --- /dev/null +++ b/test/bats/docs/versions.md @@ -0,0 +1,9 @@ +Here are the docs of following versions: + +* [v1.2.0](../../v1.2.0/README.md) +* [v1.1.0](../../v1.1.0/README.md) +* [v1.0.2](../../v1.0.2/README.md) +* [v0.4.0](../../v0.4.0/README.md) +* [v0.3.1](../../v0.3.1/README.md) +* [v0.2.0](../../v0.2.0/README.md) +* [v0.1.0](../../v0.1.0/README.md) diff --git a/test/bats/install.sh b/test/bats/install.sh new file mode 100755 index 000000000..39ca662e9 --- /dev/null +++ b/test/bats/install.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e + +BATS_ROOT="${0%/*}" +PREFIX="$1" +LIBDIR="${2:-lib}" + +if [[ -z "$PREFIX" ]]; then + printf '%s\n' \ + "usage: $0 " \ + " e.g. $0 /usr/local" >&2 + exit 1 +fi + +install -d -m 755 "$PREFIX"/{bin,libexec/bats-core,"${LIBDIR}"/bats-core,share/man/man{1,7}} +install -m 755 "$BATS_ROOT/bin"/* "$PREFIX/bin" +install -m 755 "$BATS_ROOT/libexec/bats-core"/* "$PREFIX/libexec/bats-core" +install -m 755 "$BATS_ROOT/lib/bats-core"/* "$PREFIX/${LIBDIR}/bats-core" +install -m 644 "$BATS_ROOT/man/bats.1" "$PREFIX/share/man/man1" +install -m 644 "$BATS_ROOT/man/bats.7" "$PREFIX/share/man/man7" + +echo "Installed Bats to $PREFIX/bin/bats" diff --git a/test/bats/lib/bats-core/common.bash b/test/bats/lib/bats-core/common.bash new file mode 100644 index 000000000..a27f1ac8e --- /dev/null +++ b/test/bats/lib/bats-core/common.bash @@ -0,0 +1,249 @@ +#!/usr/bin/env bash + +bats_prefix_lines_for_tap_output() { + while IFS= read -r line; do + printf '# %s\n' "$line" || break # avoid feedback loop when errors are redirected into BATS_OUT (see #353) + done + if [[ -n "$line" ]]; then + printf '# %s\n' "$line" + fi +} + +function bats_replace_filename() { + local line + while read -r line; do + printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}" + done + if [[ -n "$line" ]]; then + printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}" + fi +} + +bats_quote_code() { # + printf -v "$1" -- "%s%s%s" "$BATS_BEGIN_CODE_QUOTE" "$2" "$BATS_END_CODE_QUOTE" +} + +bats_check_valid_version() { + if [[ ! $1 =~ [0-9]+.[0-9]+.[0-9]+ ]]; then + printf "ERROR: version '%s' must be of format ..!\n" "$1" >&2 + exit 1 + fi +} + +# compares two versions. Return 0 when version1 < version2 +bats_version_lt() { # + bats_check_valid_version "$1" + bats_check_valid_version "$2" + + local -a version1_parts version2_parts + IFS=. read -ra version1_parts <<<"$1" + IFS=. read -ra version2_parts <<<"$2" + + for i in {0..2}; do + if ((version1_parts[i] < version2_parts[i])); then + return 0 + elif ((version1_parts[i] > version2_parts[i])); then + return 1 + fi + done + # if we made it this far, they are equal -> also not less then + return 2 # use other failing return code to distinguish equal from gt +} + +# ensure a minimum version of bats is running or exit with failure +bats_require_minimum_version() { # + local required_minimum_version=$1 + + if bats_version_lt "$BATS_VERSION" "$required_minimum_version"; then + printf "BATS_VERSION=%s does not meet required minimum %s\n" "$BATS_VERSION" "$required_minimum_version" + exit 1 + fi + + if bats_version_lt "$BATS_GUARANTEED_MINIMUM_VERSION" "$required_minimum_version"; then + BATS_GUARANTEED_MINIMUM_VERSION="$required_minimum_version" + fi +} + +bats_binary_search() { # + if [[ $# -ne 2 ]]; then + printf "ERROR: bats_binary_search requires exactly 2 arguments: \n" >&2 + return 2 + fi + + local -r search_value=$1 array_name=$2 + + # we'd like to test if array is set but we cannot distinguish unset from empty arrays, so we need to skip that + + local start=0 mid end mid_value + # start is inclusive, end is exclusive ... + eval "end=\${#${array_name}[@]}" + + # so start == end means empty search space + while ((start < end)); do + mid=$(((start + end) / 2)) + eval "mid_value=\${${array_name}[$mid]}" + if [[ "$mid_value" == "$search_value" ]]; then + return 0 + elif [[ "$mid_value" < "$search_value" ]]; then + # This branch excludes equality -> +1 to skip the mid element. + # This +1 also avoids endless recursion on odd sized search ranges. + start=$((mid + 1)) + else + end=$mid + fi + done + + # did not find it -> its not there + return 1 +} + +# store the values in ascending (string!) order in result array +# Intended for short lists! (uses insertion sort) +bats_sort() { # + local -r result_name=$1 + shift + + if (($# == 0)); then + eval "$result_name=()" + return 0 + fi + + local -a sorted_array=() + local -i i + while (( $# > 0 )); do # loop over input values + local current_value="$1" + shift + for ((i = ${#sorted_array[@]}; i >= 0; --i)); do # loop over output array from end + if (( i == 0 )) || [[ ${sorted_array[i - 1]} < $current_value ]]; then + # shift bigger elements one position to the end + sorted_array[i]=$current_value + break + else + # insert new element at (freed) desired location + sorted_array[i]=${sorted_array[i - 1]} + fi + done + done + + eval "$result_name=(\"\${sorted_array[@]}\")" +} + +# check if all search values (must be sorted!) are in the (sorted!) array +# Intended for short lists/arrays! +bats_all_in() { # + local -r haystack_array=$1 + shift + + local -i haystack_length # just to appease shellcheck + eval "local -r haystack_length=\${#${haystack_array}[@]}" + + local -i haystack_index=0 # initialize only here to continue from last search position + local search_value haystack_value # just to appease shellcheck + for ((i = 1; i <= $#; ++i)); do + eval "local search_value=${!i}" + for (( ; haystack_index < haystack_length; ++haystack_index)); do + eval "local haystack_value=\${${haystack_array}[$haystack_index]}" + if [[ $haystack_value > "$search_value" ]]; then + # we passed the location this value would have been at -> not found + return 1 + elif [[ $haystack_value == "$search_value" ]]; then + continue 2 # search value found -> try the next one + fi + done + return 1 # we ran of the end of the haystack without finding the value! + done + + # did not return from loop above -> all search values were found + return 0 +} + +# check if any search value (must be sorted!) is in the (sorted!) array +# intended for short lists/arrays +bats_any_in() { # + local -r haystack_array=$1 + shift + + local -i haystack_length # just to appease shellcheck + eval "local -r haystack_length=\${#${haystack_array}[@]}" + + local -i haystack_index=0 # initialize only here to continue from last search position + local search_value haystack_value # just to appease shellcheck + for ((i = 1; i <= $#; ++i)); do + eval "local search_value=${!i}" + for (( ; haystack_index < haystack_length; ++haystack_index)); do + eval "local haystack_value=\${${haystack_array}[$haystack_index]}" + if [[ $haystack_value > "$search_value" ]]; then + continue 2 # search value not in array! -> try next + elif [[ $haystack_value == "$search_value" ]]; then + return 0 # search value found + fi + done + done + + # did not return from loop above -> no search value was found + return 1 +} + +bats_trim() { # + local -r bats_trim_ltrimmed=${2#"${2%%[![:space:]]*}"} # cut off leading whitespace + # shellcheck disable=SC2034 # used in eval! + local -r bats_trim_trimmed=${bats_trim_ltrimmed%"${bats_trim_ltrimmed##*[![:space:]]}"} # cut off trailing whitespace + eval "$1=\$bats_trim_trimmed" +} + +# a helper function to work around unbound variable errors with ${arr[@]} on Bash 3 +bats_append_arrays_as_args() { # -- + local -a trailing_args=() + while (($# > 0)) && [[ $1 != -- ]]; do + local array=$1 + shift + + if eval "(( \${#${array}[@]} > 0 ))"; then + eval "trailing_args+=(\"\${${array}[@]}\")" + fi + done + shift # remove -- separator + + if (($# == 0)); then + printf "Error: append_arrays_as_args is missing a command or -- separator\n" >&2 + return 1 + fi + + if ((${#trailing_args[@]} > 0)); then + "$@" "${trailing_args[@]}" + else + "$@" + fi +} + +bats_format_file_line_reference() { # + # shellcheck disable=SC2034 # will be used in subimplementation + local output="${1?}" + shift + "bats_format_file_line_reference_${BATS_LINE_REFERENCE_FORMAT?}" "$@" +} + +bats_format_file_line_reference_comma_line() { + printf -v "$output" "%s, line %d" "$@" +} + +bats_format_file_line_reference_colon() { + printf -v "$output" "%s:%d" "$@" +} + +# approximate realpath without subshell +bats_approx_realpath() { # + local output=$1 path=$2 + if [[ $path != /* ]]; then + path="$PWD/$path" + fi + # x/./y -> x/y + path=${path//\/.\//\/} + printf -v "$output" "%s" "$path" +} + +bats_format_file_line_reference_uri() { + local filename=${1?} line=${2?} + bats_approx_realpath filename "$filename" + printf -v "$output" "file://%s:%d" "$filename" "$line" +} diff --git a/test/bats/lib/bats-core/formatter.bash b/test/bats/lib/bats-core/formatter.bash new file mode 100644 index 000000000..b774e1673 --- /dev/null +++ b/test/bats/lib/bats-core/formatter.bash @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# reads (extended) bats tap streams from stdin and calls callback functions for each line +# +# Segmenting functions +# ==================== +# bats_tap_stream_plan -> when the test plan is encountered +# bats_tap_stream_suite -> when a new file is begun WARNING: extended only +# bats_tap_stream_begin -> when a new test is begun WARNING: extended only +# +# Test result functions +# ===================== +# If timing was enabled, BATS_FORMATTER_TEST_DURATION will be set to their duration in milliseconds +# bats_tap_stream_ok -> when a test was successful +# bats_tap_stream_not_ok -> when a test has failed. If the failure was due to a timeout, +# BATS_FORMATTER_TEST_TIMEOUT is set to the timeout duration in seconds +# bats_tap_stream_skipped -> when a test was skipped +# +# Context functions +# ================= +# bats_tap_stream_comment -> when a comment line was encountered, +# scope tells the last encountered of plan, begin, ok, not_ok, skipped, suite +# bats_tap_stream_unknown -> when a line is encountered that does not match the previous entries, +# scope @see bats_tap_stream_comment +# forwards all input as is, when there is no TAP test plan header +function bats_parse_internal_extended_tap() { + local header_pattern='[0-9]+\.\.[0-9]+' + IFS= read -r header + + if [[ "$header" =~ $header_pattern ]]; then + bats_tap_stream_plan "${header:3}" + else + # If the first line isn't a TAP plan, print it and pass the rest through + printf '%s\n' "$header" + exec cat + fi + + ok_line_regexpr="ok ([0-9]+) (.*)" + skip_line_regexpr="ok ([0-9]+) (.*) # skip( (.*))?$" + timeout_line_regexpr="not ok ([0-9]+) (.*) # timeout after ([0-9]+)s$" + not_ok_line_regexpr="not ok ([0-9]+) (.*)" + + timing_expr="in ([0-9]+)ms$" + local test_name begin_index ok_index not_ok_index index scope + begin_index=0 + index=0 + scope=plan + while IFS= read -r line; do + unset BATS_FORMATTER_TEST_DURATION BATS_FORMATTER_TEST_TIMEOUT + case "$line" in + 'begin '*) # this might only be called in extended tap output + ((++begin_index)) + scope=begin + test_name="${line#* "$begin_index" }" + bats_tap_stream_begin "$begin_index" "$test_name" + ;; + 'ok '*) + ((++index)) + if [[ "$line" =~ $ok_line_regexpr ]]; then + ok_index="${BASH_REMATCH[1]}" + test_name="${BASH_REMATCH[2]}" + if [[ "$line" =~ $skip_line_regexpr ]]; then + scope=skipped + test_name="${BASH_REMATCH[2]}" # cut off name before "# skip" + local skip_reason="${BASH_REMATCH[4]}" + if [[ "$test_name" =~ $timing_expr ]]; then + local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}" + test_name="${test_name% in "${BATS_FORMATTER_TEST_DURATION}"ms}" + bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason" + else + bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason" + fi + else + scope=ok + if [[ "$line" =~ $timing_expr ]]; then + local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}" + bats_tap_stream_ok "$ok_index" "${test_name% in "${BASH_REMATCH[1]}"ms}" + else + bats_tap_stream_ok "$ok_index" "$test_name" + fi + fi + else + printf "ERROR: could not match ok line: %s" "$line" >&2 + exit 1 + fi + ;; + 'not ok '*) + ((++index)) + scope=not_ok + if [[ "$line" =~ $not_ok_line_regexpr ]]; then + not_ok_index="${BASH_REMATCH[1]}" + test_name="${BASH_REMATCH[2]}" + if [[ "$line" =~ $timeout_line_regexpr ]]; then + not_ok_index="${BASH_REMATCH[1]}" + test_name="${BASH_REMATCH[2]}" + # shellcheck disable=SC2034 # used in bats_tap_stream_ok + local BATS_FORMATTER_TEST_TIMEOUT="${BASH_REMATCH[3]}" + fi + if [[ "$test_name" =~ $timing_expr ]]; then + # shellcheck disable=SC2034 # used in bats_tap_stream_ok + local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}" + test_name="${test_name% in "${BASH_REMATCH[1]}"ms}" + fi + bats_tap_stream_not_ok "$not_ok_index" "$test_name" + else + printf "ERROR: could not match not ok line: %s" "$line" >&2 + exit 1 + fi + ;; + '# '*) + bats_tap_stream_comment "${line:2}" "$scope" + ;; + '#') + bats_tap_stream_comment "" "$scope" + ;; + 'suite '*) + scope=suite + # pass on the + bats_tap_stream_suite "${line:6}" + ;; + *) + bats_tap_stream_unknown "$line" "$scope" + ;; + esac + done +} + +normalize_base_path() { # + # the relative path root to use for reporting filenames + # this is mainly intended for suite mode, where this will be the suite root folder + local base_path="$2" + # use the containing directory when --base-path is a file + if [[ ! -d "$base_path" ]]; then + base_path="$(dirname "$base_path")" + fi + # get the absolute path + base_path="$(cd "$base_path" && pwd)" + # ensure the path ends with / to strip that later on + if [[ "${base_path}" != *"/" ]]; then + base_path="$base_path/" + fi + printf -v "$1" "%s" "$base_path" +} diff --git a/test/bats/lib/bats-core/preprocessing.bash b/test/bats/lib/bats-core/preprocessing.bash new file mode 100644 index 000000000..5d9a7652c --- /dev/null +++ b/test/bats/lib/bats-core/preprocessing.bash @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +BATS_TMPNAME="$BATS_RUN_TMPDIR/bats.$$" +BATS_PARENT_TMPNAME="$BATS_RUN_TMPDIR/bats.$PPID" +# shellcheck disable=SC2034 +BATS_OUT="${BATS_TMPNAME}.out" # used in bats-exec-file + +bats_preprocess_source() { + # export to make it visible to bats_evaluate_preprocessed_source + # since the latter runs in bats-exec-test's bash while this runs in bats-exec-file's + export BATS_TEST_SOURCE="${BATS_TMPNAME}.src" + CHECK_BATS_COMMENT_COMMANDS=1 bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE" +} + +bats_evaluate_preprocessed_source() { + if [[ -z "${BATS_TEST_SOURCE:-}" ]]; then + BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" + fi + # Dynamically loaded user files provided outside of Bats. + # shellcheck disable=SC1090 + source "$BATS_TEST_SOURCE" +} diff --git a/test/bats/lib/bats-core/semaphore.bash b/test/bats/lib/bats-core/semaphore.bash new file mode 100644 index 000000000..a196ac82d --- /dev/null +++ b/test/bats/lib/bats-core/semaphore.bash @@ -0,0 +1,113 @@ +#!/usr/bin/env bash + +bats_run_under_flock() { + flock "$BATS_SEMAPHORE_DIR" "$@" +} + +bats_run_under_shlock() { + local lockfile="$BATS_SEMAPHORE_DIR/shlock.lock" + while ! shlock -p $$ -f "$lockfile"; do + sleep 1 + done + # we got the lock now, execute the command + "$@" + local status=$? + # free the lock + rm -f "$lockfile" + return $status + } + +# setup the semaphore environment for the loading file +bats_semaphore_setup() { + export -f bats_semaphore_get_free_slot_count + export -f bats_semaphore_acquire_while_locked + export BATS_SEMAPHORE_DIR="$BATS_RUN_TMPDIR/semaphores" + + if command -v flock >/dev/null; then + BATS_LOCKING_IMPLEMENTATION=flock + elif command -v shlock >/dev/null; then + BATS_LOCKING_IMPLEMENTATION=shlock + else + printf "ERROR: flock/shlock is required for parallelization within files!\n" >&2 + exit 1 + fi +} + +# $1 - output directory for stdout/stderr +# $@ - command to run +# run the given command in a semaphore +# block when there is no free slot for the semaphore +# when there is a free slot, run the command in background +# gather the output of the command in files in the given directory +bats_semaphore_run() { + local output_dir=$1 + shift + local semaphore_slot + semaphore_slot=$(bats_semaphore_acquire_slot) + bats_semaphore_release_wrapper "$output_dir" "$semaphore_slot" "$@" & + printf "%d\n" "$!" +} + +# $1 - output directory for stdout/stderr +# $@ - command to run +# this wraps the actual function call to install some traps on exiting +bats_semaphore_release_wrapper() { + local output_dir="$1" + local semaphore_name="$2" + shift 2 # all other parameters will be use for the command to execute + + # shellcheck disable=SC2064 # we want to expand the semaphore_name right now! + trap "status=$?; bats_semaphore_release_slot '$semaphore_name'; exit $status" EXIT + + mkdir -p "$output_dir" + "$@" 2>"$output_dir/stderr" >"$output_dir/stdout" + local status=$? + + # bash bug: the exit trap is not called for the background process + bats_semaphore_release_slot "$semaphore_name" + trap - EXIT # avoid calling release twice + return $status +} + +bats_semaphore_acquire_while_locked() { + if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then + local slot=0 + while [[ -e "$BATS_SEMAPHORE_DIR/slot-$slot" ]]; do + ((++slot)) + done + if [[ $slot -lt $BATS_SEMAPHORE_NUMBER_OF_SLOTS ]]; then + touch "$BATS_SEMAPHORE_DIR/slot-$slot" && printf "%d\n" "$slot" && return 0 + fi + fi + return 1 +} + +# block until a semaphore slot becomes free +# prints the number of the slot that it received +bats_semaphore_acquire_slot() { + mkdir -p "$BATS_SEMAPHORE_DIR" + # wait for a slot to become free + # TODO: avoid busy waiting by using signals -> this opens op prioritizing possibilities as well + while true; do + # don't lock for reading, we are fine with spuriously getting no free slot + if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then + bats_run_under_"$BATS_LOCKING_IMPLEMENTATION" \ + bash -c bats_semaphore_acquire_while_locked \ + && break + fi + sleep 1 + done +} + +bats_semaphore_release_slot() { + # we don't need to lock this, since only our process owns this file + # and freeing a semaphore cannot lead to conflicts with others + rm "$BATS_SEMAPHORE_DIR/slot-$1" # this will fail if we had not acquired a semaphore! +} + +bats_semaphore_get_free_slot_count() { + # find might error out without returning something useful when a file is deleted, + # while the directory is traversed -> only continue when there was no error + until used_slots=$(find "$BATS_SEMAPHORE_DIR" -name 'slot-*' 2>/dev/null | wc -l); do :; done + echo $((BATS_SEMAPHORE_NUMBER_OF_SLOTS - used_slots)) +} diff --git a/test/bats/lib/bats-core/test_functions.bash b/test/bats/lib/bats-core/test_functions.bash new file mode 100644 index 000000000..3db539e59 --- /dev/null +++ b/test/bats/lib/bats-core/test_functions.bash @@ -0,0 +1,371 @@ +#!/usr/bin/env bash + +BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}" +BATS_TEST_NAMES=() + +# shellcheck source=lib/bats-core/warnings.bash +source "$BATS_ROOT/lib/bats-core/warnings.bash" + +# find_in_bats_lib_path echoes the first recognized load path to +# a library in BATS_LIB_PATH or relative to BATS_TEST_DIRNAME. +# +# Libraries relative to BATS_TEST_DIRNAME take precedence over +# BATS_LIB_PATH. +# +# Library load paths are recognized using find_library_load_path. +# +# If no library is found find_in_bats_lib_path returns 1. +find_in_bats_lib_path() { # + local return_var="${1:?}" + local library_name="${2:?}" + + local -a bats_lib_paths + IFS=: read -ra bats_lib_paths <<<"$BATS_LIB_PATH" + + for path in "${bats_lib_paths[@]}"; do + if [[ -f "$path/$library_name" ]]; then + printf -v "$return_var" "%s" "$path/$library_name" + # A library load path was found, return + return 0 + elif [[ -f "$path/$library_name/load.bash" ]]; then + printf -v "$return_var" "%s" "$path/$library_name/load.bash" + # A library load path was found, return + return 0 + fi + done + + return 1 +} + +# bats_internal_load expects an absolute path that is a library load path. +# +# If the library load path points to a file (a library loader) it is +# sourced. +# +# If it points to a directory all files ending in .bash inside of the +# directory are sourced. +# +# If the sourcing of the library loader or of a file in a library +# directory fails bats_internal_load prints an error message and returns 1. +# +# If the passed library load path is not absolute or is not a valid file +# or directory bats_internal_load prints an error message and returns 1. +bats_internal_load() { + local library_load_path="${1:?}" + + if [[ "${library_load_path:0:1}" != / ]]; then + printf "Passed library load path is not an absolute path: %s\n" "$library_load_path" >&2 + return 1 + fi + + # library_load_path is a library loader + if [[ -f "$library_load_path" ]]; then + # shellcheck disable=SC1090 + if ! source "$library_load_path"; then + printf "Error while sourcing library loader at '%s'\n" "$library_load_path" >&2 + return 1 + fi + return 0 + fi + + printf "Passed library load path is neither a library loader nor library directory: %s\n" "$library_load_path" >&2 + return 1 +} + +# bats_load_safe accepts an argument called 'slug' and attempts to find and +# source a library based on the slug. +# +# A slug can be an absolute path, a library name or a relative path. +# +# If the slug is an absolute path bats_load_safe attempts to find the library +# load path using find_library_load_path. +# What is considered a library load path is documented in the +# documentation for find_library_load_path. +# +# If the slug is not an absolute path it is considered a library name or +# relative path. bats_load_safe attempts to find the library load path using +# find_in_bats_lib_path. +# +# If bats_load_safe can find a library load path it is passed to bats_internal_load. +# If bats_internal_load fails bats_load_safe returns 1. +# +# If no library load path can be found bats_load_safe prints an error message +# and returns 1. +bats_load_safe() { + local slug="${1:?}" + if [[ ${slug:0:1} != / ]]; then # relative paths are relative to BATS_TEST_DIRNAME + slug="$BATS_TEST_DIRNAME/$slug" + fi + + if [[ -f "$slug.bash" ]]; then + bats_internal_load "$slug.bash" + return $? + elif [[ -f "$slug" ]]; then + bats_internal_load "$slug" + return $? + fi + + # loading from PATH (retained for backwards compatibility) + if [[ ! -f "$1" ]] && type -P "$1" >/dev/null; then + # shellcheck disable=SC1090 + source "$1" + return $? + fi + + # No library load path can be found + printf "bats_load_safe: Could not find '%s'[.bash]\n" "$slug" >&2 + return 1 +} + +bats_load_library_safe() { # + local slug="${1:?}" library_path + + # Check for library load paths in BATS_TEST_DIRNAME and BATS_LIB_PATH + if [[ ${slug:0:1} != / ]]; then + if ! find_in_bats_lib_path library_path "$slug"; then + printf "Could not find library '%s' relative to test file or in BATS_LIB_PATH\n" "$slug" >&2 + return 1 + fi + else + # absolute paths are taken as is + library_path="$slug" + if [[ ! -f "$library_path" ]]; then + printf "Could not find library on absolute path '%s'\n" "$library_path" >&2 + return 1 + fi + fi + + bats_internal_load "$library_path" + return $? +} + +# immediately exit on error, use bats_load_library_safe to catch and handle errors +bats_load_library() { # + if ! bats_load_library_safe "$@"; then + exit 1 + fi +} + +# load acts like bats_load_safe but exits the shell instead of returning 1. +load() { + if ! bats_load_safe "$@"; then + exit 1 + fi +} + +bats_redirect_stderr_into_file() { + "$@" 2>>"$bats_run_separate_stderr_file" # use >> to see collisions' content +} + +bats_merge_stdout_and_stderr() { + "$@" 2>&1 +} + +# write separate lines from into +bats_separate_lines() { # + local -r output_array_name="$1" + local -r input_var_name="$2" + local input="${!input_var_name}" + if [[ $keep_empty_lines ]]; then + local bats_separate_lines_lines=() + if [[ -n "$input" ]]; then # avoid getting an empty line for empty input + # remove one trailing \n if it exists to compensate its addition by <<< + input=${input%$'\n'} + while IFS= read -r line; do + bats_separate_lines_lines+=("$line") + done <<<"${input}" + fi + eval "${output_array_name}=(\"\${bats_separate_lines_lines[@]}\")" + else + # shellcheck disable=SC2034,SC2206 + IFS=$'\n' read -d '' -r -a "$output_array_name" <<<"${!input_var_name}" || true # don't fail due to EOF + fi +} + +run() { # [!|-N] [--keep-empty-lines] [--separate-stderr] [--] + # This has to be restored on exit from this function to avoid leaking our trap INT into surrounding code. + # Non zero exits won't restore under the assumption that they will fail the test before it can be aborted, + # which allows us to avoid duplicating the restore code on every exit path + trap bats_interrupt_trap_in_run INT + local expected_rc= + local keep_empty_lines= + local output_case=merged + local has_flags= + # parse options starting with - + while [[ $# -gt 0 ]] && [[ $1 == -* || $1 == '!' ]]; do + has_flags=1 + case "$1" in + '!') + expected_rc=-1 + ;; + -[0-9]*) + expected_rc=${1#-} + if [[ $expected_rc =~ [^0-9] ]]; then + printf "Usage error: run: '-NNN' requires numeric NNN (got: %s)\n" "$expected_rc" >&2 + return 1 + elif [[ $expected_rc -gt 255 ]]; then + printf "Usage error: run: '-NNN': NNN must be <= 255 (got: %d)\n" "$expected_rc" >&2 + return 1 + fi + ;; + --keep-empty-lines) + keep_empty_lines=1 + ;; + --separate-stderr) + output_case="separate" + ;; + --) + shift # eat the -- before breaking away + break + ;; + *) + printf "Usage error: unknown flag '%s'" "$1" >&2 + return 1 + ;; + esac + shift + done + + if [[ -n $has_flags ]]; then + bats_warn_minimum_guaranteed_version "Using flags on \`run\`" 1.5.0 + fi + + local pre_command= + + case "$output_case" in + merged) # redirects stderr into stdout and fills only $output/$lines + pre_command=bats_merge_stdout_and_stderr + ;; + separate) # splits stderr into own file and fills $stderr/$stderr_lines too + local bats_run_separate_stderr_file + bats_run_separate_stderr_file="$(mktemp "${BATS_TEST_TMPDIR}/separate-stderr-XXXXXX")" + pre_command=bats_redirect_stderr_into_file + ;; + esac + + local origFlags="$-" + set +eET + if [[ $keep_empty_lines ]]; then + # 'output', 'status', 'lines' are global variables available to tests. + # preserve trailing newlines by appending . and removing it later + # shellcheck disable=SC2034 + output="$( + "$pre_command" "$@" + status=$? + printf . + exit $status + )" && status=0 || status=$? + output="${output%.}" + else + # 'output', 'status', 'lines' are global variables available to tests. + # shellcheck disable=SC2034 + output="$("$pre_command" "$@")" && status=0 || status=$? + fi + + bats_separate_lines lines output + + if [[ "$output_case" == separate ]]; then + # shellcheck disable=SC2034 + read -d '' -r stderr <"$bats_run_separate_stderr_file" || true + bats_separate_lines stderr_lines stderr + fi + + # shellcheck disable=SC2034 + BATS_RUN_COMMAND="${*}" + set "-$origFlags" + + bats_run_print_output() { + if [[ -n "$output" ]]; then + printf "%s\n" "$output" + fi + if [[ "$output_case" == separate && -n "$stderr" ]]; then + printf "stderr:\n%s\n" "$stderr" + fi + } + + if [[ -n "$expected_rc" ]]; then + if [[ "$expected_rc" = "-1" ]]; then + if [[ "$status" -eq 0 ]]; then + BATS_ERROR_SUFFIX=", expected nonzero exit code!" + bats_run_print_output + return 1 + fi + elif [ "$status" -ne "$expected_rc" ]; then + # shellcheck disable=SC2034 + BATS_ERROR_SUFFIX=", expected exit code $expected_rc, got $status" + bats_run_print_output + return 1 + fi + elif [[ "$status" -eq 127 ]]; then # "command not found" + bats_generate_warning 1 "$BATS_RUN_COMMAND" + fi + + if [[ ${BATS_VERBOSE_RUN:-} ]]; then + bats_run_print_output + fi + + # don't leak our trap into surrounding code + trap bats_interrupt_trap INT +} + +setup() { + return 0 +} + +teardown() { + return 0 +} + +skip() { + # if this is a skip in teardown ... + if [[ -n "${BATS_TEARDOWN_STARTED-}" ]]; then + # ... we want to skip the rest of teardown. + # communicate to bats_exit_trap that the teardown was completed without error + # shellcheck disable=SC2034 + BATS_TEARDOWN_COMPLETED=1 + # if we are already in the exit trap (e.g. due to previous skip) ... + if [[ "$BATS_TEARDOWN_STARTED" == as-exit-trap ]]; then + # ... we need to do the rest of the tear_down_trap that would otherwise be skipped after the next call to exit + bats_exit_trap + # and then do the exit (at the end of this function) + fi + # if we aren't in exit trap, the normal exit handling should suffice + else + # ... this is either skip in test or skip in setup. + # Following variables are used in bats-exec-test which sources this file + # shellcheck disable=SC2034 + BATS_TEST_SKIPPED="${1:-1}" + # shellcheck disable=SC2034 + BATS_TEST_COMPLETED=1 + fi + exit 0 +} + +bats_test_begin() { + BATS_TEST_DESCRIPTION="$1" + if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then + printf 'begin %d %s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}$BATS_TEST_DESCRIPTION" >&3 + fi + setup +} + +bats_test_function() { + local tags=() + if [[ "$1" == --tags ]]; then + IFS=',' read -ra tags <<<"$2" + shift 2 + fi + local test_name="$1" + BATS_TEST_NAMES+=("$test_name") + if [[ "$test_name" == "$BATS_TEST_NAME" ]]; then + # shellcheck disable=SC2034 + BATS_TEST_TAGS=("${tags[@]+${tags[@]}}") + fi +} + +# decides whether a failed test should be run again +bats_should_retry_test() { + # test try number starts at 1 + # 0 retries means run only first try + ((BATS_TEST_TRY_NUMBER <= BATS_TEST_RETRIES)) +} diff --git a/test/bats/lib/bats-core/tracing.bash b/test/bats/lib/bats-core/tracing.bash new file mode 100644 index 000000000..0e94ec30d --- /dev/null +++ b/test/bats/lib/bats-core/tracing.bash @@ -0,0 +1,399 @@ +#!/usr/bin/env bash + +# shellcheck source=lib/bats-core/common.bash +source "$BATS_ROOT/lib/bats-core/common.bash" + +bats_capture_stack_trace() { + local test_file + local funcname + local i + + BATS_DEBUG_LAST_STACK_TRACE=() + + for ((i = 2; i != ${#FUNCNAME[@]}; ++i)); do + # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby + # calling an exported function erases the test file's BASH_SOURCE entry. + test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}" + funcname="${FUNCNAME[$i]}" + BATS_DEBUG_LAST_STACK_TRACE+=("${BASH_LINENO[$((i - 1))]} $funcname $test_file") + case "$funcname" in + "${BATS_TEST_NAME-}" | setup | teardown | setup_file | teardown_file | setup_suite | teardown_suite) + break + ;; + esac + if [[ "${BASH_SOURCE[$i + 1]:-}" == *"bats-exec-file" ]] && [[ "$funcname" == 'source' ]]; then + break + fi + done +} + +bats_get_failure_stack_trace() { + local stack_trace_var + # See bats_debug_trap for details. + if [[ -n "${BATS_DEBUG_LAST_STACK_TRACE_IS_VALID:-}" ]]; then + stack_trace_var=BATS_DEBUG_LAST_STACK_TRACE + else + stack_trace_var=BATS_DEBUG_LASTLAST_STACK_TRACE + fi + # shellcheck disable=SC2016 + eval "$(printf \ + '%s=(${%s[@]+"${%s[@]}"})' \ + "${1}" \ + "${stack_trace_var}" \ + "${stack_trace_var}")" +} + +bats_print_stack_trace() { + local frame + local index=1 + local count="${#@}" + local filename + local lineno + + for frame in "$@"; do + bats_frame_filename "$frame" 'filename' + bats_trim_filename "$filename" 'filename' + bats_frame_lineno "$frame" 'lineno' + + printf '%s' "${BATS_STACK_TRACE_PREFIX-# }" + if [[ $index -eq 1 ]]; then + printf '(' + else + printf ' ' + fi + + local fn + bats_frame_function "$frame" 'fn' + if [[ "$fn" != "${BATS_TEST_NAME-}" ]] && + # don't print "from function `source'"", + # when failing in free code during `source $test_file` from bats-exec-file + ! [[ "$fn" == 'source' && $index -eq $count ]]; then + local quoted_fn + bats_quote_code quoted_fn "$fn" + printf "from function %s " "$quoted_fn" + fi + + local reference + bats_format_file_line_reference reference "$filename" "$lineno" + if [[ $index -eq $count ]]; then + printf 'in test file %s)\n' "$reference" + else + printf 'in file %s,\n' "$reference" + fi + + ((++index)) + done +} + +bats_print_failed_command() { + local stack_trace=("${@}") + if [[ ${#stack_trace[@]} -eq 0 ]]; then + return 0 + fi + local frame="${stack_trace[${#stack_trace[@]} - 1]}" + local filename + local lineno + local failed_line + local failed_command + + bats_frame_filename "$frame" 'filename' + bats_frame_lineno "$frame" 'lineno' + bats_extract_line "$filename" "$lineno" 'failed_line' + bats_strip_string "$failed_line" 'failed_command' + local quoted_failed_command + bats_quote_code quoted_failed_command "$failed_command" + printf '# %s ' "${quoted_failed_command}" + + if [[ "${BATS_TIMED_OUT-NOTSET}" != NOTSET ]]; then + # the other values can be safely overwritten here, + # as the timeout is the primary reason for failure + BATS_ERROR_SUFFIX=" due to timeout" + fi + + if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then + printf 'failed%s\n' "$BATS_ERROR_SUFFIX" + else + printf 'failed with status %d%s\n' "$BATS_ERROR_STATUS" "$BATS_ERROR_SUFFIX" + fi +} + +bats_frame_lineno() { + printf -v "$2" '%s' "${1%% *}" +} + +bats_frame_function() { + local __bff_function="${1#* }" + printf -v "$2" '%s' "${__bff_function%% *}" +} + +bats_frame_filename() { + local __bff_filename="${1#* }" + __bff_filename="${__bff_filename#* }" + + if [[ "$__bff_filename" == "${BATS_TEST_SOURCE-}" ]]; then + __bff_filename="$BATS_TEST_FILENAME" + fi + printf -v "$2" '%s' "$__bff_filename" +} + +bats_extract_line() { + local __bats_extract_line_line + local __bats_extract_line_index=0 + + while IFS= read -r __bats_extract_line_line; do + if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then + printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}" + break + fi + done <"$1" +} + +bats_strip_string() { + [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]] + printf -v "$2" '%s' "${BASH_REMATCH[1]}" +} + +bats_trim_filename() { + printf -v "$2" '%s' "${1#"$BATS_CWD"/}" +} + +# normalize a windows path from e.g. C:/directory to /c/directory +# The path must point to an existing/accessable directory, not a file! +bats_normalize_windows_dir_path() { # + local output_var="$1" path="$2" + if [[ "$output_var" != NORMALIZED_INPUT ]]; then + local NORMALIZED_INPUT + fi + if [[ $path == ?:* ]]; then + NORMALIZED_INPUT="$( + cd "$path" || exit 1 + pwd + )" + else + NORMALIZED_INPUT="$path" + fi + printf -v "$output_var" "%s" "$NORMALIZED_INPUT" +} + +bats_emit_trace() { + if [[ $BATS_TRACE_LEVEL -gt 0 ]]; then + local line=${BASH_LINENO[1]} + # shellcheck disable=SC2016 + if [[ $BASH_COMMAND != '"$BATS_TEST_NAME" >> "$BATS_OUT" 2>&1 4>&1' && $BASH_COMMAND != "bats_test_begin "* ]] && # don't emit these internal calls + [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || $line != "$BATS_LAST_BASH_LINENO" ]] && + # avoid printing a function twice (at call site and at definition site) + [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || ${BASH_LINENO[2]} != "$BATS_LAST_BASH_LINENO" || ${BASH_SOURCE[3]} != "$BATS_LAST_BASH_SOURCE" ]]; then + local file="${BASH_SOURCE[2]}" # index 2: skip over bats_emit_trace and bats_debug_trap + if [[ $file == "${BATS_TEST_SOURCE}" ]]; then + file="$BATS_TEST_FILENAME" + fi + local padding='$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' + if ((BATS_LAST_STACK_DEPTH != ${#BASH_LINENO[@]})); then + local reference + bats_format_file_line_reference reference "${file##*/}" "$line" + printf '%s [%s]\n' "${padding::${#BASH_LINENO[@]}-4}" "$reference" >&4 + fi + printf '%s %s\n' "${padding::${#BASH_LINENO[@]}-4}" "$BASH_COMMAND" >&4 + BATS_LAST_BASH_COMMAND="$BASH_COMMAND" + BATS_LAST_BASH_LINENO="$line" + BATS_LAST_BASH_SOURCE="${BASH_SOURCE[2]}" + BATS_LAST_STACK_DEPTH="${#BASH_LINENO[@]}" + fi + fi +} + +# bats_debug_trap tracks the last line of code executed within a test. This is +# necessary because $BASH_LINENO is often incorrect inside of ERR and EXIT +# trap handlers. +# +# Below are tables describing different command failure scenarios and the +# reliability of $BASH_LINENO within different the executed DEBUG, ERR, and EXIT +# trap handlers. Naturally, the behaviors change between versions of Bash. +# +# Table rows should be read left to right. For example, on bash version +# 4.0.44(2)-release, if a test executes `false` (or any other failing external +# command), bash will do the following in order: +# 1. Call the DEBUG trap handler (bats_debug_trap) with $BASH_LINENO referring +# to the source line containing the `false` command, then +# 2. Call the DEBUG trap handler again, but with an incorrect $BASH_LINENO, then +# 3. Call the ERR trap handler, but with a (possibly-different) incorrect +# $BASH_LINENO, then +# 4. Call the DEBUG trap handler again, but with $BASH_LINENO set to 1, then +# 5. Call the EXIT trap handler, with $BASH_LINENO set to 1. +# +# bash version 4.4.20(1)-release +# command | first DEBUG | second DEBUG | ERR | third DEBUG | EXIT +# -------------+-------------+--------------+---------+-------------+-------- +# false | OK | OK | OK | BAD[1] | BAD[1] +# [[ 1 = 2 ]] | OK | BAD[2] | BAD[2] | BAD[1] | BAD[1] +# (( 1 = 2 )) | OK | BAD[2] | BAD[2] | BAD[1] | BAD[1] +# ! true | OK | --- | BAD[4] | --- | BAD[1] +# $var_dne | OK | --- | --- | BAD[1] | BAD[1] +# source /dne | OK | --- | --- | BAD[1] | BAD[1] +# +# bash version 4.0.44(2)-release +# command | first DEBUG | second DEBUG | ERR | third DEBUG | EXIT +# -------------+-------------+--------------+---------+-------------+-------- +# false | OK | BAD[3] | BAD[3] | BAD[1] | BAD[1] +# [[ 1 = 2 ]] | OK | --- | BAD[3] | --- | BAD[1] +# (( 1 = 2 )) | OK | --- | BAD[3] | --- | BAD[1] +# ! true | OK | --- | BAD[3] | --- | BAD[1] +# $var_dne | OK | --- | --- | BAD[1] | BAD[1] +# source /dne | OK | --- | --- | BAD[1] | BAD[1] +# +# [1] The reported line number is always 1. +# [2] The reported source location is that of the beginning of the function +# calling the command. +# [3] The reported line is that of the last command executed in the DEBUG trap +# handler. +# [4] The reported source location is that of the call to the function calling +# the command. +bats_debug_trap() { + # on windows we sometimes get a mix of paths (when install via nmp install -g) + # which have C:/... or /c/... comparing them is going to be problematic. + # We need to normalize them to a common format! + local NORMALIZED_INPUT + bats_normalize_windows_dir_path NORMALIZED_INPUT "${1%/*}" + local file_excluded='' path + for path in "${BATS_DEBUG_EXCLUDE_PATHS[@]}"; do + if [[ "$NORMALIZED_INPUT" == "$path"* ]]; then + file_excluded=1 + break + fi + done + + # don't update the trace within library functions or we get backtraces from inside traps + # also don't record new stack traces while handling interruptions, to avoid overriding the interrupted command + if [[ -z "$file_excluded" && + "${BATS_INTERRUPTED-NOTSET}" == NOTSET && + "${BATS_TIMED_OUT-NOTSET}" == NOTSET ]]; then + BATS_DEBUG_LASTLAST_STACK_TRACE=( + ${BATS_DEBUG_LAST_STACK_TRACE[@]+"${BATS_DEBUG_LAST_STACK_TRACE[@]}"} + ) + + BATS_DEBUG_LAST_LINENO=(${BASH_LINENO[@]+"${BASH_LINENO[@]}"}) + BATS_DEBUG_LAST_SOURCE=(${BASH_SOURCE[@]+"${BASH_SOURCE[@]}"}) + bats_capture_stack_trace + bats_emit_trace + fi +} + +# For some versions of Bash, the `ERR` trap may not always fire for every +# command failure, but the `EXIT` trap will. Also, some command failures may not +# set `$?` properly. See #72 and #81 for details. +# +# For this reason, we call `bats_check_status_from_trap` at the very beginning +# of `bats_teardown_trap` and check the value of `$BATS_TEST_COMPLETED` before +# taking other actions. We also adjust the exit status value if needed. +# +# See `bats_exit_trap` for an additional EXIT error handling case when `$?` +# isn't set properly during `teardown()` errors. +bats_check_status_from_trap() { + local status="$?" + if [[ -z "${BATS_TEST_COMPLETED:-}" ]]; then + BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}" + if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then + BATS_ERROR_STATUS=1 + fi + trap - DEBUG + fi +} + +bats_add_debug_exclude_path() { # + if [[ -z "$1" ]]; then # don't exclude everything + printf "bats_add_debug_exclude_path: Exclude path must not be empty!\n" >&2 + return 1 + fi + if [[ "$OSTYPE" == cygwin || "$OSTYPE" == msys ]]; then + local normalized_dir + bats_normalize_windows_dir_path normalized_dir "$1" + BATS_DEBUG_EXCLUDE_PATHS+=("$normalized_dir") + else + BATS_DEBUG_EXCLUDE_PATHS+=("$1") + fi +} + +bats_setup_tracing() { + # Variables for capturing accurate stack traces. See bats_debug_trap for + # details. + # + # BATS_DEBUG_LAST_LINENO, BATS_DEBUG_LAST_SOURCE, and + # BATS_DEBUG_LAST_STACK_TRACE hold data from the most recent call to + # bats_debug_trap. + # + # BATS_DEBUG_LASTLAST_STACK_TRACE holds data from two bats_debug_trap calls + # ago. + # + # BATS_DEBUG_LAST_STACK_TRACE_IS_VALID indicates that + # BATS_DEBUG_LAST_STACK_TRACE contains the stack trace of the test's error. If + # unset, BATS_DEBUG_LAST_STACK_TRACE is unreliable and + # BATS_DEBUG_LASTLAST_STACK_TRACE should be used instead. + BATS_DEBUG_LASTLAST_STACK_TRACE=() + BATS_DEBUG_LAST_LINENO=() + BATS_DEBUG_LAST_SOURCE=() + BATS_DEBUG_LAST_STACK_TRACE=() + BATS_DEBUG_LAST_STACK_TRACE_IS_VALID= + BATS_ERROR_SUFFIX= + BATS_DEBUG_EXCLUDE_PATHS=() + # exclude some paths by default + bats_add_debug_exclude_path "$BATS_ROOT/lib/" + bats_add_debug_exclude_path "$BATS_ROOT/libexec/" + + exec 4<&1 # used for tracing + if [[ "${BATS_TRACE_LEVEL:-0}" -gt 0 ]]; then + # avoid undefined variable errors + BATS_LAST_BASH_COMMAND= + BATS_LAST_BASH_LINENO= + BATS_LAST_BASH_SOURCE= + BATS_LAST_STACK_DEPTH= + # try to exclude helper libraries if found, this is only relevant for tracing + while read -r path; do + bats_add_debug_exclude_path "$path" + done < <(find "$PWD" -type d -name bats-assert -o -name bats-support) + fi + + local exclude_paths path + # exclude user defined libraries + IFS=':' read -r exclude_paths <<<"${BATS_DEBUG_EXCLUDE_PATHS:-}" + for path in "${exclude_paths[@]}"; do + if [[ -n "$path" ]]; then + bats_add_debug_exclude_path "$path" + fi + done + + # turn on traps after setting excludes to avoid tracing the exclude setup + trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG + trap 'bats_error_trap' ERR +} + +bats_error_trap() { + bats_check_status_from_trap + + # If necessary, undo the most recent stack trace captured by bats_debug_trap. + # See bats_debug_trap for details. + if [[ "${BASH_LINENO[*]}" = "${BATS_DEBUG_LAST_LINENO[*]:-}" && + "${BASH_SOURCE[*]}" = "${BATS_DEBUG_LAST_SOURCE[*]:-}" && + -z "$BATS_DEBUG_LAST_STACK_TRACE_IS_VALID" ]]; then + BATS_DEBUG_LAST_STACK_TRACE=( + ${BATS_DEBUG_LASTLAST_STACK_TRACE[@]+"${BATS_DEBUG_LASTLAST_STACK_TRACE[@]}"} + ) + fi + BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1 +} + +bats_interrupt_trap() { + # mark the interruption, to handle during exit + BATS_INTERRUPTED=true + BATS_ERROR_STATUS=130 + # debug trap fires before interrupt trap but gets wrong linenumber (line 1) + # -> use last stack trace instead of BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=true +} + +# this is used inside run() +bats_interrupt_trap_in_run() { + # mark the interruption, to handle during exit + BATS_INTERRUPTED=true + BATS_ERROR_STATUS=130 + BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=true + exit 130 +} diff --git a/test/bats/lib/bats-core/validator.bash b/test/bats/lib/bats-core/validator.bash new file mode 100644 index 000000000..59fc2c1e6 --- /dev/null +++ b/test/bats/lib/bats-core/validator.bash @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +bats_test_count_validator() { + trap '' INT # continue forwarding + header_pattern='[0-9]+\.\.[0-9]+' + IFS= read -r header + # repeat the header + printf "%s\n" "$header" + + # if we detect a TAP plan + if [[ "$header" =~ $header_pattern ]]; then + # extract the number of tests ... + local expected_number_of_tests="${header:3}" + # ... count the actual number of [not ] oks... + local actual_number_of_tests=0 + while IFS= read -r line; do + # forward line + printf "%s\n" "$line" + case "$line" in + 'ok '*) + ((++actual_number_of_tests)) + ;; + 'not ok'*) + ((++actual_number_of_tests)) + ;; + esac + done + # ... and error if they are not the same + if [[ "${actual_number_of_tests}" != "${expected_number_of_tests}" ]]; then + printf '# bats warning: Executed %s instead of expected %s tests\n' "$actual_number_of_tests" "$expected_number_of_tests" + return 1 + fi + else + # forward output unchanged + cat + fi +} diff --git a/test/bats/lib/bats-core/warnings.bash b/test/bats/lib/bats-core/warnings.bash new file mode 100644 index 000000000..fbb5186a4 --- /dev/null +++ b/test/bats/lib/bats-core/warnings.bash @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# shellcheck source=lib/bats-core/tracing.bash +source "$BATS_ROOT/lib/bats-core/tracing.bash" + +# generate a warning report for the parent call's call site +bats_generate_warning() { # [--no-stacktrace] [...] + local warning_number="${1-}" padding="00" + shift + local no_stacktrace= + if [[ ${1-} == --no-stacktrace ]]; then + no_stacktrace=1 + shift + fi + if [[ $warning_number =~ [0-9]+ ]] && ((warning_number < ${#BATS_WARNING_SHORT_DESCS[@]})); then + { + printf "BW%s: ${BATS_WARNING_SHORT_DESCS[$warning_number]}\n" "${padding:${#warning_number}}${warning_number}" "$@" + if [[ -z "$no_stacktrace" ]]; then + bats_capture_stack_trace + BATS_STACK_TRACE_PREFIX=' ' bats_print_stack_trace "${BATS_DEBUG_LAST_STACK_TRACE[@]}" + fi + } >>"$BATS_WARNING_FILE" 2>&3 + else + printf "Invalid Bats warning number '%s'. It must be an integer between 1 and %d." "$warning_number" "$((${#BATS_WARNING_SHORT_DESCS[@]} - 1))" >&2 + exit 1 + fi +} + +# generate a warning if the BATS_GUARANTEED_MINIMUM_VERSION is not high enough +bats_warn_minimum_guaranteed_version() { # + if bats_version_lt "$BATS_GUARANTEED_MINIMUM_VERSION" "$2"; then + bats_generate_warning 2 "$1" "$2" "$2" + fi +} + +# put after functions to avoid line changes in tests when new ones get added +BATS_WARNING_SHORT_DESCS=( + # to start with 1 + 'PADDING' + # see issue #578 for context + "\`run\`'s command \`%s\` exited with code 127, indicating 'Command not found'. Use run's return code checks, e.g. \`run -127\`, to fix this message." + "%s requires at least BATS_VERSION=%s. Use \`bats_require_minimum_version %s\` to fix this message." + "\`setup_suite\` is visible to test file '%s', but was not executed. It belongs into 'setup_suite.bash' to be picked up automatically." +) diff --git a/test/bats/libexec/bats-core/bats b/test/bats/libexec/bats-core/bats new file mode 100755 index 000000000..9b5f0c435 --- /dev/null +++ b/test/bats/libexec/bats-core/bats @@ -0,0 +1,504 @@ +#!/usr/bin/env bash +set -e + +export BATS_VERSION='1.9.0' +VALID_FORMATTERS="pretty, junit, tap, tap13" + +version() { + printf 'Bats %s\n' "$BATS_VERSION" +} + +abort() { + local print_usage=1 + if [[ ${1:-} == --no-print-usage ]]; then + print_usage= + shift + fi + printf 'Error: %s\n' "$1" >&2 + if [[ -n $print_usage ]]; then + usage >&2 + fi + exit 1 +} + +usage() { + local cmd="${0##*/}" + local line + + cat < + ${cmd} [-h | -v] + +HELP_TEXT_HEADER + + cat <<'HELP_TEXT_BODY' + is the path to a Bats test file, or the path to a directory + containing Bats test files (ending with ".bats") + + -c, --count Count test cases without running any tests + --code-quote-style