diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 219ff06..8147083 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,40 +13,43 @@ jobs: - name: Run Shellcheck uses: ludeeus/action-shellcheck@2.0.0 - test: - name: test + test-ubuntu: + name: test (ubuntu) runs-on: ubuntu-latest - strategy: - matrix: - container: - - debian # uses debian:buster-20200327-slim which is debian 10.3 - - ubuntu16 # uses ubuntu:xenial-20200212 which is ubuntu 16.04 - - container: kdabir/has-test-containers:${{ matrix.container }} - steps: - name: Clone Repo uses: actions/checkout@v4 - - - name: test - run: make test + - name: Setup Bats and bats libs + id: setup-bats + uses: bats-core/bats-action@3.0.0 + - name: Run Unit Tests + run: make unit-test + shell: bash + - name: Run Integration Tests + run: make intg-test shell: bash - test_all: - name: test_all - runs-on: ubuntu-latest - strategy: - matrix: - container: - - ubuntu # uses ubuntu:bionic-20200311 which is ubuntu 18.04 - - alpine # uses bash:5.0.16 which is alpine 3.11 - - container: kdabir/has-test-containers:${{ matrix.container }} - + test-macos: + name: test (macos) + runs-on: macos-latest steps: - name: Clone Repo uses: actions/checkout@v4 - - - name: test_all - run: bats -t ./tests/test_all_packages.bats + - name: Install bats-core + run: brew install bats-core + - name: Run Unit Tests + run: make unit-test shell: bash + - name: Run Integration Tests + run: make intg-test + shell: bash + + # intg-test: + # name: intg-test + # runs-on: ubuntu-latest + # steps: + # - name: Clone Repo + # uses: actions/checkout@v4 + # - name: Run Integration Tests in Docker + # run: make docker-test + # shell: bash diff --git a/.hasrc b/.hasrc index eb2d487..a59a696 100644 --- a/.hasrc +++ b/.hasrc @@ -1,6 +1,12 @@ -# make to install/test the project +# required to install/test the project make curl git -# bats for testing +# bats for testing shell script bats +# for running integration tests +# their outputs will be tested +sed +awk +jq +node \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4361808 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing to has + +Thank you for your interest in contributing! Please follow these guidelines to get started. + +## How to run locally + +1. **Clone the repository:** + ```bash + git clone https://github.com/kdabir/has.git + cd has + chmod +x ./has + ``` +2. **Run the main script:** + ```bash + ./has + ``` + Example: + ```bash + ./has git curl node + ``` +3. Install Prequisites for development + + ```bash + ./has + ``` + + running `has` from root of the project tells you about the pre-requisites required (because of `.hasrc` file) + +## How to run tests locally + +1. **Install dependencies:** + - On Ubuntu: + ```bash + sudo apt-get update && sudo apt-get install -y bats make curl git + ``` + - On Mac (using Homebrew): + ```bash + brew install bats-core make curl git + ``` +2. **Run the unit test suite:** + ```bash + make test + # or + bats tests/unit/unit-tests.bats + ``` + +## Adding more tools + +The current list of supported packages can be viewed with `make list` + +If the command you wish to include supports any of `-v`, `--version`, `-version`, `version`, `-V` then you can find +corresponding function which can be called to check presence and extract version. + +- commands that use `--version` flag -> `__dynamic_detect--version()` +- commands that use `-version` flag -> `__dynamic_detect-version()` +- commands that use `-v` flag -> `__dynamic_detect-v()` +- commands that use `-V` flag -> `__dynamic_detect-V()` + + +However, for many tools version extraction may not work and you will need to add custom parsing of command's output. The `has` script is commented to guide developers about what needs to be done to add more tools. + +## Adding features + +- If you are contributing a feature, please open an issue first to dicuss the idea. +- When implmenting, ensure to check current tests. Add test cases for your feature. +- Tests are executed using the excellent [bats](https://github.com/bats-core/bats-core) testing framework. +- Add tests and run `make test` + +Raise the PR and **make sure the tests pass** on [GitHub Actions](https://github.com/kdabir/has/actions). + + +## Looking for support to maintain docker based tests +I'm not expert at docker (or shell scripts for that matter). I need someone to look at `tests/to-fix` dir to help me +with integration tests for all tools installed in docker containers. + +- `test_all_packages.bats` will test every package has supports. This includes newly added commands so please add new packages to +- `alpine.Dockerfile` and `ubuntu.Dockerfile` to install the tool OR +- `packages_alpine_skip.txt` and `packages_ubuntu_skip.txt` to exclude the package from the tests diff --git a/LICENSE b/LICENSE index da44372..ca03649 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2018 Kunal Dabir, Saager Mhatre and various contributors +Copyright (c) 2014-2025 Kunal Dabir, Saager Mhatre and various 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 diff --git a/Makefile b/Makefile index 186f6f1..68f66b3 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,15 @@ ifeq ($(PREFIX),) PREFIX := /usr/local endif -test : has - bats .hastest.bats + +test: unit-test intg-test + +unit-test: + bats tests/unit/unit-tests.bats + bats tests/unit/with-mocks.bats + +intg-test: + bats -t tests/intg/intg-tests.bats has : # ensure 'has' in repo @@ -38,3 +45,24 @@ uninstall : .PHONY: test install uninstall update +CONTAINERS = ubuntu alpine + +.PHONY: docker-test + +docker-test: + @for c in $(CONTAINERS); do \ + $(MAKE) docker-test-$$c; \ + done + +.PHONY: docker-test-% +docker-test-%: + docker build -t test-image:$* -f tests/to-fix/containers/$*.Dockerfile . + docker run --rm \ + -v $(PWD):/workspace \ + -w /workspace \ + test-image:$* \ + bash -c "make test || bats -t ./tests/to-fix/test_all_packages.bats || true" + + +list: + @grep -o "^ \\+[a-zA-Z0-9_|-]\\+)" has | grep -o "[a-zA-Z0-9_|-]\\+" | tr "|" "\\n" | sort -f | sed '1,3d' \ No newline at end of file diff --git a/README.md b/README.md index 6197b96..3158f76 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ `has` checks presence of various command line tools on the PATH and reports their installed version. [![Build Status](https://github.com/kdabir/has/actions/workflows/main.yml/badge.svg)](https://github.com/kdabir/has/actions/workflows/main.yml) -[![Open Source Helpers](https://www.codetriage.com/kdabir/has/badges/users.svg)](https://www.codetriage.com/kdabir/has) [![demo](demo.svg)](demo.svg) @@ -13,19 +12,19 @@ Just [install](#installing) the `has` script, (there is no dependency apart from ```console $ has node npm java git gradle -✔ node 8.2.1 -✔ npm 5.3.0 -✔ java 1.8.0 -✔ git 2.14.1 -✔ gradle 4.0.1 +✓ node 22.17.0 +✓ npm 11.5.1 +✓ java 21.0.7 +✓ git 2.50.1 +✓ gradle 8.14.2 ``` If everything is good `has` exits with status code `0`. The exit status code reflects number of commands **not found** on your path. ```console $ has node go javac -✔ node 8.2.1 -✔ go 1.8.3 +✓ node 22.17.0 +✓ go 1.24.5 ✘ javac ``` @@ -104,9 +103,9 @@ If you are lazy, you can run `has` directly off the Internet as well: ```console curl -sL https://git.io/_has | bash -s git node npm -✔ git 2.17.1 -✔ node 11.11.0 -✔ npm 6.7.0 +✓ git 2.50.1 +✓ node 22.17.0 +✓ npm 11.5.1 ``` **ProTip**: if that's too much typing every time, setup an alias in your `.bashrc`/`.zshrc` file: @@ -119,7 +118,7 @@ And use it ```console $ has git -✔ git 2.17.1 +✓ git 2.50.1 $ type has has is aliased to `curl -sL https://git.io/_has | bash -s' ``` @@ -153,10 +152,10 @@ When `has` is run in directory containing this file, it produces: ```console $ has -✔ git 2.19.1 -✔ curl 7.54.0 -✔ ruby 2.3.1 -✔ node 10.7.0 +✓ git 2.50.1 +✓ curl 8.5.0 +✓ ruby 3.4.1 +✓ node 22.17.0 ``` Also, CLI arguments passed to `has` are additive to `.hasrc` file. For example, in the same dir, if the following command is fired, @@ -164,11 +163,11 @@ Also, CLI arguments passed to `has` are additive to `.hasrc` file. For example, ```bash $ has java -✔ java 11.0.1 -✔ git 2.19.1 -✔ curl 7.54.0 -✔ ruby 2.3.1 -✔ node 10.7.0 +✓ java 21.0.7 +✓ git 2.50.1 +✓ curl 8.5.0 +✓ ruby 3.4.1 +✓ node 22.17.0 ``` **Pro Tip**: commit `.hasrc` file in root of your project. This can work as a quick check for confirming presence all command @@ -180,31 +179,13 @@ On machines that don't even have `has` installed, your project's `.hasrc` is hon > take a look at [.hasrc](https://github.com/kdabir/has/blob/master/.hasrc) file for this repo. -## Contributing +## Supporting -1. Star the repo, tweet about it, spread the word -2. Update the documentation (i.e. the README file) -3. Adding support for more commands -4. Adding more features to `has` +here are some ways you can help: -## Adding more tools +1. Star the repo, write about it, spread the word +2. Update the documentation (including this README) +3. Adding support for more commands and features - see [CONTRIBUTING.md](CONTRIBUTING.md) -The current list of supported packages can be viewed with `bash tests/packages_all.sh` - -If the command you wish to include supports any of `-v`, `--version`, `-version`, `version`, `-V` then you can find -corresponding function which can be called to check presence and extract version. However, for many tools version -extraction may not work and you will need to add custom parsing of command's output. The `has` script is commented -to guide developers about what needs to be done to add more tools. - -`/tests/test_all_packages.bats` will test every package has supports. This includes newly added commands so please add new packages to - - `alpine.Dockerfile` and `ubuntu.Dockerfile` to install the tool OR - - `packages_alpine_skip.txt` and `packages_ubuntu_skip.txt` to exclude the package from the tests - -## Adding Features - -If you are contributing a feature, please ensure to check current tests. Add test cases for your feature. Tests are -executed using the excellent [bats](https://github.com/bats-core/bats-core) testing framework. Add tests and run `make test` - -Raise the PR and **make sure the tests pass** on [GitHub Actions](https://github.com/kdabir/has/actions). ### ♥ diff --git a/has b/has index e5f0de1..8a481dc 100755 --- a/has +++ b/has @@ -66,7 +66,7 @@ Has checks the presence of various command line tools on the PATH and reports th Options: -q Silent mode -h, --help Display this help text and quit - -V, --version Show version number and quit + -v, --version Show version number and quit Examples: ${BINARY_NAME} git curl node EOF diff --git a/tests/intg/intg-tests.bats b/tests/intg/intg-tests.bats new file mode 100644 index 0000000..2342e05 --- /dev/null +++ b/tests/intg/intg-tests.bats @@ -0,0 +1,100 @@ +#!/usr/bin/env bats + +## should work on mac and ubuntu +## tests just the core features with minimum set of tools + +INSTALL_DIR= +BATS_TMPDIR="${BATS_TMPDIR:-/tmp}" +fancyx='✗' +checkmark='✓' + +## We need to create a new directory so that .hasrc file in the root does not get read by the `has` instance under test +setup() { + export HAS_TMPDIR="${BATS_TMPDIR}/tmp-for-test" + mkdir -p "${HAS_TMPDIR}" + cp -f "${BATS_TEST_DIRNAME}"/../../has "${HAS_TMPDIR}" + cd "${HAS_TMPDIR}" || return + export has="${HAS_TMPDIR}/has" +} + +teardown() { + if [[ -d "${HAS_TMPDIR}" ]]; then + rm -rf "${HAS_TMPDIR}" + fi +} + +@test "invoking 'has' without arguments prints usage" { + run $has + + [ "$status" -eq 0 ] + [ "${lines[0]}" = 'Usage: has [OPTION] ...' ] + [ "${lines[1]}" = 'Has checks the presence of various command line tools on the PATH and reports their installed version.' ] +} + +@test "git version output is non-empty and debug output is shown" { + run $has git + echo "Output: $output" + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep "git")" ] + version="${lines[0]##* }" + [ "${#version}" -ge 2 ] +} + +@test "check multiple real tools from .hasrc (make, curl, jq, node)" { + run $has make curl jq node + echo "Output: $output" + [ "$status" -eq 0 ] + for tool in make curl jq node; do + line=$(echo "$output" | grep "$tool") + [ -n "$line" ] + version="${line##* }" + [ "${#version}" -ge 1 ] + done +} + +@test "loads commands from .hasrc file and excludes comments (real tools, output debug)" { + printf "bash\n#comment\nmake\n" > .hasrc + run $has + echo "Output: $output" + [ "$status" -eq 0 ] + for tool in bash make; do + line=$(echo "$output" | grep "$tool") + [ -n "$line" ] + version="${line##* }" + [ "${#version}" -ge 1 ] + done +} + +@test "'has' warns about tools not configured" { + run $has foobar + + [ "$status" -eq 1 ] + [ "$(echo "${output}" | grep ${fancyx} | grep "foobar not understood")" ] +} + +@test "env var 'HAS_ALLOW_UNSAFE' overrides safety check" { + HAS_ALLOW_UNSAFE=y run $has foobar + + [ "$status" -eq 1 ] + [ "$(echo "${output}" | grep ${fancyx} | grep "foobar")" ] +} + +@test "status code reflects number of failed commands" { + HAS_ALLOW_UNSAFE=y run $has foobar git barbaz + + [ "$status" -eq 2 ] + [ "$(echo "${output}" | grep ${fancyx} | grep "foobar")" ] + [ "$(echo "${output}" | grep ${fancyx} | grep "barbaz")" ] +} + + +@test "loads commands from .hasrc file and honors CLI args as well" { + printf "bash\nmake\ngit" >> .hasrc + run $has git jq + + [ "$status" -eq 0 ] + [ "$(echo "${output}" | grep ${checkmark} | grep "bash")" ] + [ "$(echo "${output}" | grep ${checkmark} | grep "make")" ] + [ "$(echo "${output}" | grep ${checkmark} | grep "git")" ] + [ "$(echo "${output}" | grep ${checkmark} | grep "jq")" ] +} diff --git a/tests/containers/README.md b/tests/to-fix/containers/README.md similarity index 100% rename from tests/containers/README.md rename to tests/to-fix/containers/README.md diff --git a/tests/containers/alpine.Dockerfile b/tests/to-fix/containers/alpine.Dockerfile similarity index 100% rename from tests/containers/alpine.Dockerfile rename to tests/to-fix/containers/alpine.Dockerfile diff --git a/tests/containers/debian.Dockerfile b/tests/to-fix/containers/debian.Dockerfile similarity index 100% rename from tests/containers/debian.Dockerfile rename to tests/to-fix/containers/debian.Dockerfile diff --git a/tests/containers/ubuntu.Dockerfile b/tests/to-fix/containers/ubuntu.Dockerfile similarity index 100% rename from tests/containers/ubuntu.Dockerfile rename to tests/to-fix/containers/ubuntu.Dockerfile diff --git a/tests/containers/ubuntu16.Dockerfile b/tests/to-fix/containers/ubuntu16.Dockerfile similarity index 100% rename from tests/containers/ubuntu16.Dockerfile rename to tests/to-fix/containers/ubuntu16.Dockerfile diff --git a/tests/packages_all.sh b/tests/to-fix/packages_all.sh similarity index 60% rename from tests/packages_all.sh rename to tests/to-fix/packages_all.sh index a312ed7..6180163 100644 --- a/tests/packages_all.sh +++ b/tests/to-fix/packages_all.sh @@ -4,6 +4,6 @@ set -euo pipefail pushd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null -grep -o "^ \\+[a-zA-Z0-9_|-]\\+)" ../has | grep -o "[a-zA-Z0-9_|-]\\+" | tr "|" "\\n" | sort -f +grep -o "^ \\+[a-zA-Z0-9_|-]\\+)" ../../has | grep -o "[a-zA-Z0-9_|-]\\+" | tr "|" "\\n" | sort -f | sed '1,3d' popd >/dev/null diff --git a/tests/packages_alpine_skip.txt b/tests/to-fix/packages_alpine_skip.txt similarity index 100% rename from tests/packages_alpine_skip.txt rename to tests/to-fix/packages_alpine_skip.txt diff --git a/tests/packages_ubuntu_skip.txt b/tests/to-fix/packages_ubuntu_skip.txt similarity index 100% rename from tests/packages_ubuntu_skip.txt rename to tests/to-fix/packages_ubuntu_skip.txt diff --git a/tests/test_all_packages.bats b/tests/to-fix/test_all_packages.bats similarity index 99% rename from tests/test_all_packages.bats rename to tests/to-fix/test_all_packages.bats index 19d47aa..e029a2d 100644 --- a/tests/test_all_packages.bats +++ b/tests/to-fix/test_all_packages.bats @@ -58,3 +58,4 @@ expected_version() { [ "$status" -eq 0 ] echo "#" >&3 } + diff --git a/tests/test_package.bats b/tests/to-fix/test_package.bats similarity index 99% rename from tests/test_package.bats rename to tests/to-fix/test_package.bats index 6a4da96..56af481 100644 --- a/tests/test_package.bats +++ b/tests/to-fix/test_package.bats @@ -28,3 +28,4 @@ get_version_from_has() { # We grep instead of direct compare because has adds ansi colors with tput [ "$(echo "${actual_ver}" | grep "$expected_ver")" ] } + diff --git a/tests/unit/mocks/bash b/tests/unit/mocks/bash new file mode 100644 index 0000000..301f1e3 --- /dev/null +++ b/tests/unit/mocks/bash @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Simulate only the `--version` flag, as has relies on it for bash +if [[ "$1" == "--version" ]]; then + echo "bash, version ${BASH_VERSION:-5.2.21}(1)-release (x86_64-apple-darwin)" # Default mock version +else + echo "$* is not implemented in mocks/bash" # fallback for other commands +fi + diff --git a/tests/unit/mocks/composer b/tests/unit/mocks/composer new file mode 100755 index 0000000..8fa5e76 --- /dev/null +++ b/tests/unit/mocks/composer @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Simulate only the `-V` flag, as has relies on it for composer +if [[ "$1" == "-V" ]]; then + # Output only the version number, matching what has expects + echo "Composer version ${COMPOSER_VERSION:-2.7.2}" +else + echo "$* is not implemented in mocks/composer" # fallback for other commands +fi diff --git a/tests/unit/mocks/git b/tests/unit/mocks/git new file mode 100755 index 0000000..9b7ee15 --- /dev/null +++ b/tests/unit/mocks/git @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Simulate only the `--version` flag, as has relies on it for git +if [[ "$1" == "--version" ]]; then + echo "git version ${GIT_VERSION:-2.39.5}" # Default mock version +else + echo "$* is not implemented in mocks/git" # fallback for other commands +fi diff --git a/tests/unit/mocks/go b/tests/unit/mocks/go new file mode 100755 index 0000000..13d3f3a --- /dev/null +++ b/tests/unit/mocks/go @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Simulate only the `version` +if [ "$1" == "version" ]; then + echo "go version go${GO_VERSION:-1.23.6}" # Default mock version +else + echo "$* is not implemented in mocks/go" # fallback for other commands +fi diff --git a/tests/unit/mocks/java b/tests/unit/mocks/java new file mode 100755 index 0000000..35280fd --- /dev/null +++ b/tests/unit/mocks/java @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Simulate only the `-version` +if [ "$1" == "-version" ]; then + echo "java version ${JAVA_VERSION:-24.0.2}" # Default mock version +else + echo "$* is not implemented in mocks/java" # fallback for other commands +fi diff --git a/tests/unit/mocks/jq b/tests/unit/mocks/jq new file mode 100644 index 0000000..3c4a0af --- /dev/null +++ b/tests/unit/mocks/jq @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Simulate only the `--version` flag, as has relies on it for jq +if [[ "$1" == "--version" ]]; then + echo "jq-${JQ_VERSION:-1.7.1}" # Default mock version +else + echo "$* is not implemented in mocks/jq" # fallback for other commands +fi + diff --git a/tests/unit/mocks/make b/tests/unit/mocks/make new file mode 100644 index 0000000..720fb01 --- /dev/null +++ b/tests/unit/mocks/make @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Simulate only the `--version` flag, as has relies on it for make +if [[ "$1" == "--version" ]]; then + echo "GNU Make ${MAKE_VERSION:-4.4.1}" # Default mock version +else + echo "$* is not implemented in mocks/make" # fallback for other commands +fi + diff --git a/tests/unit/mocks/unzip b/tests/unit/mocks/unzip new file mode 100755 index 0000000..3c3bcaa --- /dev/null +++ b/tests/unit/mocks/unzip @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Simulate only the `-v` flag, as has relies on it for unzip +if [[ "$1" == "-v" ]]; then + echo "UnZip ${UNZIP_VERSION:-6.00} of 20 April 2009, by Info-ZIP. Maintainer: Info-ZIP." +else + echo "$* is not implemented in mocks/unzip" # fallback for other commands +fi diff --git a/.hastest.bats b/tests/unit/unit-tests.bats similarity index 65% rename from .hastest.bats rename to tests/unit/unit-tests.bats index c79ded3..702b416 100644 --- a/.hastest.bats +++ b/tests/unit/unit-tests.bats @@ -1,14 +1,18 @@ #!/usr/bin/env bats +## should work on mac and ubuntu +## tests just the core features with minimum set of tools + INSTALL_DIR= BATS_TMPDIR="${BATS_TMPDIR:-/tmp}" fancyx='✗' checkmark='✓' ## We need to create a new directory so that .hasrc file in the root does not get read by the `has` instance under test setup() { + export PATH="$BATS_TEST_DIRNAME/mocks:$PATH" export HAS_TMPDIR="${BATS_TMPDIR}/tmp-for-test" mkdir -p "${HAS_TMPDIR}" - cp -f "${BATS_TEST_DIRNAME}"/has "${HAS_TMPDIR}" + cp -f "${BATS_TEST_DIRNAME}"/../../has "${HAS_TMPDIR}" cd "${HAS_TMPDIR}" || return export has="${HAS_TMPDIR}/has" } @@ -28,67 +32,71 @@ teardown() { [ "${lines[2]}" = 'Options:' ] [ "${lines[3]}" = ' -q Silent mode' ] [ "${lines[4]}" = ' -h, --help Display this help text and quit' ] - [ "${lines[5]}" = ' -V, --version Show version number and quit' ] + [ "${lines[5]}" = ' -v, --version Show version number and quit' ] [ "${lines[6]}" = 'Examples: has git curl node' ] } -@test "make install creates a valid installation" { - INSTALL_DIR="${HAS_TMPDIR}/.local" - cd "${BATS_TEST_DIRNAME}" - run make PREFIX="${INSTALL_DIR}" install - [ "$status" -eq 0 ] - [ -x "${INSTALL_DIR}/bin/has" ] - - # has reads .hasrc from $PWD, so change anywhere else. - cd "${INSTALL_DIR}" - run "${INSTALL_DIR}/bin/has" - - [ "$status" -eq 0 ] - [ "${lines[0]}" = 'Usage: has [OPTION] ...' ] - [ "${lines[1]}" = 'Has checks the presence of various command line tools on the PATH and reports their installed version.' ] - [ "${lines[2]}" = 'Options:' ] - [ "${lines[3]}" = ' -q Silent mode' ] - [ "${lines[4]}" = ' -h, --help Display this help text and quit' ] - [ "${lines[5]}" = ' -V, --version Show version number and quit' ] - [ "${lines[6]}" = 'Examples: has git curl node' ] -} - -@test "..even if 'has' is missing from directory" { - if [[ -n $GITHUB_ACTION ]] || [[ -n $GITHUB_ACTIONS ]]; then - if grep -iq "ubuntu" /etc/issue; then - skip "todo: this test fails on ubuntu in CI" - fi - fi - - INSTALL_DIR="${HAS_TMPDIR}/system_local" - cd "${BATS_TEST_DIRNAME}" - mv has has-been - run make PREFIX="${INSTALL_DIR}" install - [ "$status" -eq 0 ] - [ -x "${INSTALL_DIR}/bin/has" ] - cd "${BATS_TEST_DIRNAME}" - mv has-been has -} - -@test "make update runs git fetch" { - cd "${BATS_TEST_DIRNAME}" - if [[ -z $GITHUB_ACTION ]] && [[ -z $GITHUB_ACTIONS ]]; then - skip "make update overwrites my git working tree" - elif grep -iq "ubuntu" /etc/issue; then - skip "todo: this test fails on ubuntu in CI" - fi - - run make update - - [ "$status" -eq 0 ] - [ "$(echo "${output}" | grep "git fetch --verbose")" ] -} +# @test "make install creates a valid installation" { +# INSTALL_DIR="${HAS_TMPDIR}/.local" +# ## has is two levels up +# cd "${BATS_TEST_DIRNAME}/../.." +# run make PREFIX="${INSTALL_DIR}" install +# [ "$status" -eq 0 ] +# [ -x "${INSTALL_DIR}/bin/has" ] +# +# # has reads .hasrc from $PWD, so change anywhere else. +# cd "${INSTALL_DIR}" +# run "${INSTALL_DIR}/bin/has" +# +# [ "$status" -eq 0 ] +# [ "${lines[0]}" = 'Usage: has [OPTION] ...' ] +# [ "${lines[1]}" = 'Has checks the presence of various command line tools on the PATH and reports their installed version.' ] +# [ "${lines[2]}" = 'Options:' ] +# [ "${lines[3]}" = ' -q Silent mode' ] +# [ "${lines[4]}" = ' -h, --help Display this help text and quit' ] +# [ "${lines[5]}" = ' -v, --version Show version number and quit' ] +# [ "${lines[6]}" = 'Examples: has git curl node' ] +# } +# +# @test "..even if 'has' is missing from directory" { +# if [[ -n $GITHUB_ACTION ]] || [[ -n $GITHUB_ACTIONS ]]; then +# if grep -iq "ubuntu" /etc/issue; then +# skip "todo: this test fails on ubuntu in CI" +# fi +# fi +# +# INSTALL_DIR="${HAS_TMPDIR}/system_local" +# cd "${BATS_TEST_DIRNAME}" +# +# run make PREFIX="${INSTALL_DIR}" install +# [ "$status" -eq 0 ] +# [ -x "${INSTALL_DIR}/bin/has" ] +# cd "${BATS_TEST_DIRNAME}" +# +# } + +#@test "make update runs git fetch" { +# cd "${BATS_TEST_DIRNAME}" +# if [[ -z $GITHUB_ACTION ]] && [[ -z $GITHUB_ACTIONS ]]; then +# skip "make update overwrites my git working tree" +# elif grep -iq "ubuntu" /etc/issue; then +# skip "todo: this test fails on ubuntu in CI" +# fi +# +# run make update +# +# [ "$status" -eq 0 ] +# [ "$(echo "${output}" | grep "git fetch --verbose")" ] +#} @test "works with single command check" { + local GIT_VERSION="2.39.5" + export GIT_VERSION run $has git [ "$status" -eq 0 ] [ "$(echo "${lines[0]}" | grep "git")" ] + [ "$(echo "${lines[0]}" | grep "${GIT_VERSION}")" ] } @test "'has' warns about tools not configured" { @@ -106,7 +114,7 @@ teardown() { } @test "status code reflects number of failed commands" { - HAS_ALLOW_UNSAFE=y run $has foobar bc git barbaz + HAS_ALLOW_UNSAFE=y run $has foobar git barbaz [ "$status" -eq 2 ] [ "$(echo "${output}" | grep ${fancyx} | grep "foobar")" ] @@ -131,13 +139,13 @@ teardown() { @test "loads commands from .hasrc file and honors CLI args as well" { printf "bash\nmake\ngit" >> .hasrc - HAS_ALLOW_UNSAFE=y run $has git bc + run $has git jq [ "$status" -eq 0 ] [ "$(echo "${output}" | grep ${checkmark} | grep "bash")" ] [ "$(echo "${output}" | grep ${checkmark} | grep "make")" ] [ "$(echo "${output}" | grep ${checkmark} | grep "git")" ] - [ "$(echo "${output}" | grep ${checkmark} | grep "bc")" ] + [ "$(echo "${output}" | grep ${checkmark} | grep "jq")" ] } @test "testing PASS output with unicode" { @@ -163,16 +171,13 @@ teardown() { } @test "testing archiving commands" { - run $has tar unzip gzip xz unar pv zip + run $has tar unzip gzip zip [ "$status" -eq 0 ] [ "$(echo "${lines[0]}" | grep "tar")" ] [ "$(echo "${lines[1]}" | grep "unzip")" ] [ "$(echo "${lines[2]}" | grep "gzip")" ] - [ "$(echo "${lines[3]}" | grep "xz")" ] - [ "$(echo "${lines[4]}" | grep "unar")" ] - [ "$(echo "${lines[5]}" | grep "pv")" ] - [ "$(echo "${lines[6]}" | grep "zip")" ] + [ "$(echo "${lines[3]}" | grep "zip")" ] } @test "testing coreutils commands" { @@ -212,7 +217,7 @@ teardown() { [ "${lines[2]}" = 'Options:' ] [ "${lines[3]}" = ' -q Silent mode' ] [ "${lines[4]}" = ' -h, --help Display this help text and quit' ] - [ "${lines[5]}" = ' -V, --version Show version number and quit' ] + [ "${lines[5]}" = ' -v, --version Show version number and quit' ] [ "${lines[6]}" = 'Examples: has git curl node' ] } diff --git a/tests/unit/with-mocks.bats b/tests/unit/with-mocks.bats new file mode 100644 index 0000000..6eb1ee8 --- /dev/null +++ b/tests/unit/with-mocks.bats @@ -0,0 +1,106 @@ +#!/usr/bin/env bats + +## should work on mac and ubuntu +## tests just the core features with minimum set of tools + +INSTALL_DIR= +BATS_TMPDIR="${BATS_TMPDIR:-/tmp}" +fancyx='✗' +checkmark='✓' +## We need to create a new directory so that .hasrc file in the root does not get read by the `has` instance under test +setup() { + export PATH="$BATS_TEST_DIRNAME/mocks:$PATH" + export HAS_TMPDIR="${BATS_TMPDIR}/tmp-for-test" + mkdir -p "${HAS_TMPDIR}" + cp -f "${BATS_TEST_DIRNAME}"/../../has "${HAS_TMPDIR}" + cd "${HAS_TMPDIR}" || return + export has="${HAS_TMPDIR}/has" +} + +teardown() { + if [[ -d "${HAS_TMPDIR}" ]]; then + rm -rf "${HAS_TMPDIR}" + fi +} + + +@test "works with commands that support --version (git)" { + local GIT_VERSION="2.39.5" + export GIT_VERSION + + run $has git + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep ${checkmark} )" ] + [ "$(echo "${lines[0]}" | grep "git")" ] + [ "$(echo "${lines[0]}" | grep "${GIT_VERSION}")" ] +} + +@test "works with commands that support -v (unzip)" { + local UNZIP_VERSION="6.00" + export UNZIP_VERSION + + run $has unzip + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep ${checkmark} )" ] + [ "$(echo "${lines[0]}" | grep "unzip")" ] + [ "$(echo "${lines[0]}" | grep "${UNZIP_VERSION}")" ] +} + + +@test "works with commands that support -version (java)" { + local JAVA_VERSION="24.0.2" + export JAVA_VERSION + + run $has java + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep ${checkmark} )" ] + [ "$(echo "${lines[0]}" | grep "java")" ] + [ "$(echo "${lines[0]}" | grep "${JAVA_VERSION}")" ] +} + +@test "works with commands that support -V (composer)" { + local COMPOSER_VERSION="2.7.2" + export COMPOSER_VERSION + + run $has composer + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep ${checkmark})" ] + [ "$(echo "${lines[0]}" | grep "composer")" ] + [ "$(echo "${lines[0]}" | grep "${COMPOSER_VERSION}")" ] +} + +@test "works with commands that support version as argument (go)" { + local GO_VERSION="1.23.6" + export GO_VERSION + + run $has go + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 0 ] + [ "$(echo "${lines[0]}" | grep ${checkmark} )" ] + [ "$(echo "${lines[0]}" | grep "go")" ] + [ "$(echo "${lines[0]}" | grep "${GO_VERSION}")" ] +} + +@test "fails gracefully for unknown command" { + run $has notarealcmd + + echo "OUTPUT: ${lines[0]}" >&3 + + [ "$status" -eq 1 ] + [ "$(echo "${lines[0]}" | grep ${fancyx} )" ] + [ "$(echo "${lines[0]}" | grep "notarealcmd")" ] +}