diff --git a/.idea/runConfigurations/Test.xml b/.idea/runConfigurations/Test.xml new file mode 100644 index 0000000..700aee0 --- /dev/null +++ b/.idea/runConfigurations/Test.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 9ae19b0..d17c78f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,35 +16,30 @@ matrix: - sourceline: "ppa:fish-shell/release-3" packages: - fish - - os: osx - env: FISH=2 - addons: - homebrew: - packages: - - 'test/Homebrew-Formula-fish-2.7.1.rb' - update: true # TODO: build should be green without, but isn't - os: osx env: FISH=3 addons: homebrew: packages: - - fish # --> latest, i.e. >=3.0.2 + - fish # --> latest, i.e. >=3.1.2 update: true # TODO: build should be green without, but isn't before_install: - curl -s "https://get.sdkman.io" | bash - - bash test/prepare_tests.sh + - bundle install --gemfile=test/Gemfile --no-cache + - |- + uname -a; + fish --version; + { source ~/.bash_profile || source ~/.bashrc; } && sdk version; + ruby --version; + echo "cucumber $(cucumber --version)"; install: - - mkdir -p "${HOME}"/.config/fish/{completions,conf.d,functions} - - cp completions/* "${HOME}"/.config/fish/completions/ - - cp conf.d/* "${HOME}"/.config/fish/conf.d/ - - cp functions/* "${HOME}"/.config/fish/functions/ - - uname -a; fish --version + - |- + mkdir -p "${HOME}"/.config/fish/{completions,conf.d,functions} + cp completions/* "${HOME}"/.config/fish/completions/ + cp conf.d/* "${HOME}"/.config/fish/conf.d/ + cp functions/* "${HOME}"/.config/fish/functions/ script: - - ruby test/completion.rb - - fish test/wrapper.fish - - fish test/reinitialize.fish - - fish -c "sdk install crash 1.3.0; and sdk uninstall crash 1.3.0" > /dev/null && fish test/check_for_path_zombies.fish - - bash test/remove_sdkman.sh > /dev/null && fish -c "echo 'y' | sdk" > /dev/null && fish -c "sdk version" + - (cd test && cucumber) diff --git a/README.md b/README.md index d31e03e..a638597 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ [![Build Status][travis-badge]][travis-link] -Makes command `sdk` from [SDKMAN!] usable from fish, including auto-completion. +Makes command `sdk` from [SDKMAN!] usable from [fish], including auto-completion. Also adds binaries from installed SDKs to the PATH. Version 1.4.0 tested with - - fish 2.7.1 and 3.0.2, and - - SDKMAN! 5.7.4, on - - Ubuntu 18.04 LTS and macOS 10.13. + - fish 2.7.1 and 3.1.2, and + - SDKMAN! 5.8.2, on + - Ubuntu 18.04 LTS and macOS 10.13 ## Install @@ -29,6 +29,38 @@ _Note:_ It's all in the background; you should be able to run `sdk` and binaries installed with `sdk` as you would expect. +## Contribute + +When you make changes, +please run the tests at least on one platform before creating a pull request. + +As the tests may mess up your own setup +-- you have been warned! -- +the recommended way is to run the tests in a Docker container: + +```fish +docker build -t sdkman-for-fish-tests -f test/Dockerfile . +docker run --rm sdkman-for-fish-tests +``` + +A run configuration for Jetbrains IDEs is included. + +It is a also possible to run individual features, for instance: + +```fish +docker run --rm sdkman-for-fish-tests features/completions.feature +``` + +As a corollary, this is the fastest way to run all tests +(if you do not care about the report): + +```fish +for f in features/*.feature + docker run --rm sdkman-for-fish-tests "$f" & +done +wait +``` + ## Acknowledgements * Completion originally by [Ted Wise](https://github.com/ctwise); see his @@ -37,6 +69,7 @@ with `sdk` as you would expect. see [his comment on sdkman/sdkman-cli#294](https://github.com/sdkman/sdkman-cli/issues/294#issuecomment-318252058). [SDKMAN!]: https://github.com/sdkman/sdkman-cli +[fish]: https://fishshell.com/ [fisher]: https://github.com/jorgebucaran/fisher [travis-link]: https://travis-ci.org/reitzig/sdkman-for-fish [travis-badge]: https://travis-ci.org/reitzig/sdkman-for-fish.svg?branch=master diff --git a/completions/sdk.fish b/completions/sdk.fish index 17fcb7e..054c203 100644 --- a/completions/sdk.fish +++ b/completions/sdk.fish @@ -88,14 +88,14 @@ complete -c sdk -f -n '__fish_sdkman_no_command' \ -d 'Install new version' complete -c sdk -f -n '__fish_sdkman_using_command i install' \ -a "(__fish_sdkman_candidates)" +# TODO complete available versions --> issue #4 complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ - # TODO complete available versions --> #4 -a 'a.b.c' \ -d "version list unavailable" complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ -a 'x.y.z' \ - -d "Add your own; specify path!" - # Implicit: complete files as fourth parameter + -d "Specify path to install custom version." +# Implicit: complete files as fourth parameter complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 3 i install' # block diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..4044d2b --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,41 @@ +FROM ruby:2.5.8-slim-buster + +# Install dependencies +RUN apt-get update \ + && apt-get -y install \ + fish \ + curl \ + unzip \ + zip \ + && apt-get clean + +WORKDIR app +COPY test/Gemfile ./ +RUN bundle install \ + && rm Gemfile + +# Switch to non-root user for test context +ARG TEST_HOME="/home/test" +RUN groupadd -r test \ + && useradd --no-log-init -r -g test -m -d $TEST_HOME test +USER test +RUN curl -s "https://get.sdkman.io" | bash + +# To speed up tests, uncomment this shared setup: +#SHELL ["/bin/bash", "-c"] +#RUN source "$TEST_HOME/.sdkman/bin/sdkman-init.sh" \ +# && sdk install ant 1.9.9 \ +# && sdk install ant 1.10.1 \ +# && sdk install crash + +# "Install" sdkman-for-fish +RUN mkdir -p $TEST_HOME/.config/fish/ +COPY --chown=test:test completions $TEST_HOME/.config/fish/completions/ +COPY --chown=test:test conf.d $TEST_HOME/.config/fish/conf.d/ +COPY --chown=test:test functions $TEST_HOME/.config/fish/functions/ +RUN ls -R $TEST_HOME/.config/fish/ + +# Run tests +COPY test ./ +ENTRYPOINT ["cucumber"] +CMD [] diff --git a/test/Gemfile b/test/Gemfile new file mode 100644 index 0000000..70d69fe --- /dev/null +++ b/test/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +group :test do + gem 'cucumber', '~> 3.1.0' + gem 'rspec', '~> 3.7.0' +end diff --git a/test/Homebrew-Formula-fish-2.7.1.rb b/test/Homebrew-Formula-fish-2.7.1.rb deleted file mode 100644 index 3a463db..0000000 --- a/test/Homebrew-Formula-fish-2.7.1.rb +++ /dev/null @@ -1,60 +0,0 @@ -class HomebrewFormulaFish271 < Formula - desc "User-friendly command-line shell for UNIX-like operating systems" - homepage "https://fishshell.com" - url "https://github.com/fish-shell/fish-shell/releases/download/2.7.1/fish-2.7.1.tar.gz" - mirror "https://fishshell.com/files/2.7.1/fish-2.7.1.tar.gz" - sha256 "e42bb19c7586356905a58578190be792df960fa81de35effb1ca5a5a981f0c5a" - - bottle do - sha256 "affac16a396410a500241266dbbbd8752562c9b800d9e4f2ef8de279c6fdb6aa" => :high_sierra - sha256 "335538a7ea7f9613474f7321af0a1d519b61b0fc4be97a1744a7ea7bef7ff7e3" => :sierra - sha256 "463cfa8edc0603761f25e0ba4e49524f69a1d856263d370d1de5fb8698dd5ccf" => :el_capitan - end - - head do - url "https://github.com/fish-shell/fish-shell.git", :shallow => false - - depends_on "autoconf" => :build - depends_on "automake" => :build - depends_on "doxygen" => :build - end - - depends_on "pcre2" - - def install - system "autoreconf", "--no-recursive" if build.head? - - # In Homebrew's 'superenv' sed's path will be incompatible, so - # the correct path is passed into configure here. - args = %W[ - --prefix=#{prefix} - --with-extra-functionsdir=#{HOMEBREW_PREFIX}/share/fish/vendor_functions.d - --with-extra-completionsdir=#{HOMEBREW_PREFIX}/share/fish/vendor_completions.d - --with-extra-confdir=#{HOMEBREW_PREFIX}/share/fish/vendor_conf.d - SED=/usr/bin/sed - ] - system "./configure", *args - system "make", "install" - end - - def caveats; <<~EOS - You will need to add: - #{HOMEBREW_PREFIX}/bin/fish - to /etc/shells. - - Then run: - chsh -s #{HOMEBREW_PREFIX}/bin/fish - to make fish your default shell. - EOS - end - - def post_install - (pkgshare/"vendor_functions.d").mkpath - (pkgshare/"vendor_completions.d").mkpath - (pkgshare/"vendor_conf.d").mkpath - end - - test do - system "#{bin}/fish", "-c", "echo" - end -end diff --git a/test/check_for_path_zombies.fish b/test/check_for_path_zombies.fish deleted file mode 100644 index ff928a6..0000000 --- a/test/check_for_path_zombies.fish +++ /dev/null @@ -1,9 +0,0 @@ -switch "$PATH" -case "*sdkman/candidates/crash/*" - echo "Uninstalled candidate in PATH" - sdk list crash | head -10 - echo $PATH - exit 1 -case "*" - echo "OKAY" -end diff --git a/test/completion.rb b/test/completion.rb deleted file mode 100644 index 8ddf73d..0000000 --- a/test/completion.rb +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env ruby - -# Includes completion examples with the intent to cover -# all sdk commands -# Beware, pasta follows. - -test_cases = { - # Basic commands (in the order of `sdk help`): - "i" => ["i", "install"], - "u" => ["u", "ug", "uninstall", "update", "upgrade", "use"], - "r" => ["rm"], - "l" => ["list", "ls"], - "d" => ["d", "default"], - "c" => ["c", "current"], - "v" => ["v", "version"], - "b" => ["b", "broadcast"], - "h" => ["h", "help"], - "o" => ["offline"], - "s" => ["selfupdate"], - "f" => ["flush"], - # Currently uncovered; include to catch new upstream commands: - "a" => [], - "e" => [], - "g" => [], - "j" => [], - "k" => [], - "m" => [], - "n" => [], - "p" => [], - "q" => [], - "t" => [], - "w" => [], - "x" => [], - "y" => [], - "z" => [], - - # Second parameters complete correctly - "install an" => ["ant"], - "install xyz" => [], - "install 1." => [], - - "uninstall " => ["ant"], - "uninstall a" => ["ant"], - "uninstall j" => [], - "uninstall 1." => [], - - "list an" => ["ant"], - "list xyz" => [], - "list 1." => [], - - "use " => ["ant"], - "use an" => ["ant"], - "use j" => [], - "use 1." => [], - - "default " => ["ant"], - "default an" => ["ant"], - "default j" => [], - "default 1." => [], - - "current an" => ["ant"], - "current xyz" => [], - "current 1." => [], - - "upgrade " => ["ant"], - "upgrade an" => ["ant"], - "upgrade j" => [], - "upgrade 1." => [], - - "version " => [], - "version a" => [], - - "broadcast " => [], - "broadcast a" => [], - - "help " => [], - "help a" => [], - - "offline " => ["enable", "disable"], - "offline a" => [], - - "selfupdate " => ["force"], - "selfupdate a" => [], - - "update " => [], - "update a" => [], - - "flush " => ["broadcast", "archives", "temp"], - "flush x" => [], - - # Third parameters complete correctly - #"install ant 1.10." => ["1.10.0", "1.10.1"], # TODO: issue #4 - "uninstall ant 1.10." => ["1.10.1"], - "list ant " => [], - "use ant " => ["1.9.9", "1.10.1"], - "default ant " => ["1.9.9", "1.10.1"], - "current ant " => [], - "upgrade ant " => [], - "offline ant " => [], - "selfupdate ant " => [], - "flush ant " => [], - - # Fourth parameters complete correctly - "install ant 1.10.2-mine /tm" => ["/tmp/"], - "uninstall ant 1.10.1 " => [], - "use ant 1.10.1 " => [], - "default ant 1.10.1 " => [], - - # Fifth parameters complete correctly - "install ant 1.10.2-mine /tmp " => [], - - # Lists of all candidates work - "install gr" => ["gradle", "grails", "groovy", "groovyserv"], - "install grad" => ["gradle"], - "install gradk" => [], - - # Lists of installed candidates work - "uninstall an" => ["ant"], - "uninstall gr" => [], - "uninstall xyz" => [], - - # List of all versions work - # TODO - - # List of installed versions work - "uninstall ant 1" => ["1.9.9", "1.10.1"], - "uninstall ant 2" => [], - "uninstall vertx " => [], - "uninstall an " => [] -} - -def fish_command(prompt) - # Fish errors out if we don't set terminal dimensions - "fish -c 'stty rows 80 columns 80; complete -C\"sdk #{prompt}\"'" -end - -def extract(completion_output) - completion_output.split("\n").map { |line| - line.split(/\s+/)[0].strip - } -end - -puts "Testing sdk completions" -failures = 0 -test_cases.each { |prompt, results| - results.sort! - - print " Test: 'sdk #{prompt}'" - completions = extract(`#{fish_command(prompt)}`).sort - if completions != results - puts " -- bad!" - puts " - Expected: #{results}" - puts " - Actual: #{completions}" - failures += 1 - else - puts " -- ok!" - end -} - -puts "Ran #{test_cases.size} checks each." -puts "#{failures}/#{test_cases.size} checks failed." -exit failures > 0 ? 1 : 0 \ No newline at end of file diff --git a/test/features/completions.feature b/test/features/completions.feature new file mode 100644 index 0000000..d2a2710 --- /dev/null +++ b/test/features/completions.feature @@ -0,0 +1,199 @@ +Feature: Shell Completion + We want to get the correct completion on the CLI. + + Background: + Given SDKMAN! candidate list is up to date + And candidate ant is installed at version 1.9.9 + And candidate ant is installed at version 1.10.1 + And candidate crash is installed + + Scenario: Command list correct + When the user enters " " into the prompt + Then completion should propose "b, broadcast, c, current, d, default, flush, h, help, i, install, list, ls, offline, rm, selfupdate, u, ug, uninstall, update, upgrade, use, v, version" + + Scenario Outline: Commands complete + When the user enters "" into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | b | b, broadcast | /^[^b]+$/ | + | c | c, current | /^[^c]+$/ | + | d | d, default | /^[^d]+$/ | + | f | flush | /^[^f]+$/ | + | h | h, help | /^[^h]+$/ | + | i | i, install | /^[^i]+$/ | + | in | install | | + | l | list, ls | /^[^l]+$/ | + | o | offline | /^[^o]+$/ | + | r | rm | /^[^r]+$/ | + | s | selfupdate | /^[^s]+$/ | + | u | u, ug, uninstall, update, upgrade, use | /^[^u]+$/ | + | un | uninstall | | + | up | update, upgrade | | + | us | use | | + | v | v, version | /^[^v]+$/ | + # Currently uncovered (except by fuzzy matches); + # include negatives to prevent accidents: + | a | | /^a/ | + | e | | /^e/ | + | g | | /^g/ | + | j | | /^j/ | + | k | | /^k/ | + | m | | /^m/ | + | n | | /^n/ | + | p | | /^p/ | + | q | | /^q/ | + | t | | /^t/ | + | w | | /^w/ | + | x | | /^x/ | + | y | | /^y/ | + | z | | /^z/ | + + Scenario Outline: Completion for 'install' + When the user enters "install " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | an | ant | gradle | + | xyz | | /.*/ | + | 1. | | /.*/ | + | gra | gradle, grails | ant | + | grad | gradle | ant, grails | + | gradk | | /.*/ | +# | ant 1.10. | 1.10.0, 1.10.1 | | # TODO: list installable versions --> issue #4 + | ant 1.10.2-mine /tm | /tmp/ | /bin | + | 'ant 1.10.2-mine /tmp ' | | /.*/ | + # NB: Excluding wildcard pattern /.*/ expresses "do not offer any completions" + + Scenario Outline: Completion for 'uninstall' + When the user enters "uninstall " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | ant, crash | gradle | + | a | ant | gradle | + | j | | /.*/ | + | 1. | | /.*/ | + | an | ant | gradle, crash | # some installed + | gr | | /.*/ | # none installed + | xyz | | /.*/ | # no such candidate + | 'an ' | | /.*/ | # no such candidate installed + | 'ant 1' | 1.10.1, 1.9.9 | /^\w+$/ | + | 'ant 1.10.' | 1.10.1 | 1.9.9 | + | 'ant 2' | | /.*/ | + | 'ant 1.10.1 ' | | /.*/ | # only one version at a time + + Scenario Outline: Completion for 'list' + When the user enters "list " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | an | ant | crash | + | xyz | | /.*/ | + | 1. | | /.*/ | + | 'ant ' | | /.*/ | + + Scenario Outline: Completion for 'use' + When the user enters "use " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | ant, crash | gradle | + | an | ant | crash, gradle | + | j | | /.*/ | + | 1. | | /.*/ | + | 'ant ' | 1.10.1, 1.9.9 | /^\w+$/ | + | 'ant 1.10.1 ' | | /.*/ | + + Scenario Outline: Completion for 'default' + When the user enters "default " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | ant, crash | gradle | + | an | ant | crash, gradle | + | j | | /.*/ | + | 1. | | /.*/ | + | 'ant ' | 1.10.1, 1.9.9 | /^\w+$/ | + | 'ant 1.10.1 ' | | /.*/ | + + Scenario Outline: Completion for 'current' + When the user enters "current " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | an | ant | gradle | # --> installed version + | gr | gradle | ant | # --> not installed + | xyz | | /.*/ | + | 1. | | /.*/ | + | 'ant ' | | /.*/ | + + Scenario Outline: Completion for 'upgrade' + When the user enters "upgrade " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | ant, crash | gradle | + | an | ant | crash, gradle | + | j | | /.*/ | + | 1. | | /.*/ | + | 'ant ' | | /^\w+$/ | + + Scenario Outline: Completion for 'offline' + When the user enters "offline " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | disable, enable | /^(?!disable\|enable).*$/ | # NB: \| escaped to get it past Gherkin's parser + | en | enable | /^(?!enable).*$/ | + | di | disable | /^(?!disable).*$/ | + | an | | /.*/ | + | 'enable ' | | /.*/ | + + Scenario Outline: Completion for 'selfupdate' + When the user enters "selfupdate " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | force | /^(?!force).*$/ | + | f | force | /^(?!force).*$/ | + | a | | /.*/ | + | 'force ' | | /.*/ | + + Scenario Outline: Completion for 'flush' + When the user enters "flush " into the prompt + Then completion should propose "" + But completion should not propose + Examples: + | cmd | completions | exclusions | + | | archives, broadcast, temp | /^(?!archives\|broadcast\|temp).*$/ | + | b | broadcast | /^(?!broadcast).*$/ | + | a | archives | /^(?!archives\|broadcast).*$/ | + | t | temp | /^(?!temp\|broadcast).*$/ | + | x | | /.*/ | + | 'temp ' | | /.*/ | + + + Scenario Outline: Completion for commands without parameters + When the user enters "" into the prompt + Then completion should not propose /.*/ + Examples: + | cmd | + | 'version ' | + | 'version a' | + | 'broadcast ' | + | 'broadcast a' | + | 'help ' | + | 'help a' | + | 'update ' | + | 'update a' | diff --git a/test/features/corner_cases.feature b/test/features/corner_cases.feature new file mode 100644 index 0000000..a2c211f --- /dev/null +++ b/test/features/corner_cases.feature @@ -0,0 +1,18 @@ +Feature: Corner Cases + + Scenario: sdk not initialized in this shell + Given environment variable SDKMAN_DIR is not set + When a new Fish shell is launched + Then environment variable SDKMAN_DIR has the original value + + Scenario: sdk initialized for another user in this shell + Given environment variable SDKMAN_DIR is set to "/" + When a new Fish shell is launched + Then environment variable SDKMAN_DIR has the original value + + # TODO: add test that fails if `test` in conf.d/sdk.fish:80 errors (cf issue #28 et al.) + + Scenario: PATH should contain only valid paths + Given candidate crash is installed + When candidate crash is uninstalled + Then environment variable PATH cannot contain "sdkman/candidates/crash/" diff --git a/test/features/installer.feature b/test/features/installer.feature new file mode 100644 index 0000000..aacdefa --- /dev/null +++ b/test/features/installer.feature @@ -0,0 +1,11 @@ +Feature: Install SDKMAN! if necessary + + Scenario: + Given SDKMAN! is not installed + When sdk is called and user answers "n" + Then SDKMAN! is absent + + Scenario: + Given SDKMAN! is not installed + When sdk is called and user answers "y" + Then SDKMAN! is present diff --git a/test/features/step_definitions/completion.rb b/test/features/step_definitions/completion.rb new file mode 100644 index 0000000..474038b --- /dev/null +++ b/test/features/step_definitions/completion.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'open3' + +module CompletionHelper + def complete(cmd) + completions = run_fish_command("complete -C\"sdk #{cmd}\"")[:stdout] + + completions.map { |line| line.split(/\s+/)[0].strip } + # TODO: Why do we get duplicates in the Docker container? + end +end +World CompletionHelper + +When('the user enters {string} into the prompt') do |cmd| + @response = complete(cmd.gsub(/["']/, '')) +end + +Then('completion should propose {string}') do |completions| + completions = completions.split(',').map(&:strip) + expect(@response).to include(*completions) +end + +Then('completion should not propose {patterns}') do |exclusions_patterns| + exclusions_patterns.each do |p| + @response.each do |r| + expect(r).not_to match(p) + end + end +end diff --git a/test/features/step_definitions/corner_cases.rb b/test/features/step_definitions/corner_cases.rb new file mode 100644 index 0000000..198cae4 --- /dev/null +++ b/test/features/step_definitions/corner_cases.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +Given('environment variable {env_name} is not set') do |name| + @name = name + @expect_intermediate_value = false + @command = <<~BASH + ( \\ + env | grep -E "^#{name}="; \\ + export -n #{name}; \\ + env | grep -E "^#{name}="; \\ + BASH +end + +Given('environment variable {env_name} is set to {string}') do |name, new_value| + @name = name + @expect_intermediate_value = true + @command = <<~BASH + ( \\ + env | grep -E "^#{name}="; \\ + export #{name}=#{new_value}; \\ + env | grep -E "^#{name}="; \\ + BASH +end + +When('a new Fish shell is launched') do + @command += <<~BASH + fish -lc "env | grep -E \\"^#{@name}=\\"" \\ + ) \\ + BASH + + @response = run_bash_command(@command) +end + +Then('environment variable {env_name} has the original value') do |name| + expect(name).to eq(@name) # otherwise the test doesn't make sense + + if @expect_intermediate_value + expect(@response[:stdout].count).to eq(3) + expect(@response[:stdout][0]).to_not eq(@response[:stdout][1]) # destruction effective + else + expect(@response[:stdout].count).to eq(2) + end + + expect(@response[:stdout][-1]).to eq(@response[:stdout][0]) # reinitialization effective +end + +Then('environment variable {env_name} cannot contain {string}') do |name, value| + env = run_fish_command('echo noop')[:env] + expect(env[name]).to_not match(/#{Regexp.escape(value)}/) +end diff --git a/test/features/step_definitions/installer.rb b/test/features/step_definitions/installer.rb new file mode 100644 index 0000000..2ed427e --- /dev/null +++ b/test/features/step_definitions/installer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'fileutils' + +Given(/^SDKMAN! is not installed$/) do + FileUtils.rm_rf("#{ENV['HOME']}/.sdkman") +end + +When('sdk is called and user answers {string}') do |answer| + run_fish_command("echo '#{answer}' | sdk version") +end + +Then(/^SDKMAN! is absent$/) do + expect(Dir["#{ENV['HOME']}/.sdkman/*"].count).to eq(0) + response = run_bash_command("sdk version") + expect(response[:status]).to_not eq(0) +end + +Then('SDKMAN! is present') do + expect(Dir["#{ENV['HOME']}/.sdkman/*"].count).to be > 1 + response = run_bash_command("sdk version") + expect(response[:status]).to eq(0) +end diff --git a/test/features/step_definitions/setup.rb b/test/features/step_definitions/setup.rb new file mode 100644 index 0000000..69fdd04 --- /dev/null +++ b/test/features/step_definitions/setup.rb @@ -0,0 +1,52 @@ +$index_updated = false # TODO: Hack since Cucumber doesn't have Feature-level hooks +Given(/^SDKMAN! candidate list is up to date$/) do + unless $index_updated + run_bash_command('sdk update') + $index_updated = true + end +end + +Given(/^candidate (\w+) is installed at version (\d+(?:\.\d+)*)$/) do |candidate, version| + # TODO: Can we mock-install instead? + # Something like + # + # mkdir -p ${SDKMAN_CANDIDATES_DIR}/${candidate}/{version}/bin \ + # && touch ${SDKMAN_CANDIDATES_DIR}/${candidate}/${version}/bin/${candidate} && + # ln -s ${SDKMAN_CANDIDATES_DIR}/${candidate}/current ${SDKMAN_CANDIDATES_DIR}/${candidate}/${version} + # + # should be quite enough to trick sdk as far as we need it to trick. + # Or is it? + run_bash_command("sdk install #{candidate} #{version}") unless installed?(candidate, version) +end + +Given(/^candidate (\w+) is installed$/) do |candidate| + run_bash_command("sdk install #{candidate}") unless installed?(candidate) +end + +def _uninstall_candidate_version(candidate_dir) + %r{/([^/]+)/([^/]+)$}.match(candidate_dir) do |match| + candidate = match[1] + version = match[2] + run_bash_command("sdk rm #{candidate} #{version}") unless version == 'current' + end +end + +When(/^candidate (\w+) is uninstalled$/) do |candidate| + puts `ls ~/.sdkman/candidates/#{candidate}` + Dir["#{ENV['HOME']}/.sdkman/candidates/#{candidate}/*"].each do |candidate_dir| + _uninstall_candidate_version(candidate_dir) + end + puts `ls ~/.sdkman/candidates/#{candidate}` +end + +# Uninstall all SDKMAN! candidates +# TODO: Run after every scenario, this makes tests very slow. +# Currently, Cucumber doesn't have Feature-level hooks, so we have to work around: +# --> install only if not already installed; +# if the test needs a candidate to _not_ be there, make it explicit. +# --> clean up after _all_ features at least +at_exit do + Dir["#{ENV['HOME']}/.sdkman/candidates/*/*"].each do |candidate_dir| + _uninstall_candidate_version(candidate_dir) + end +end diff --git a/test/features/step_definitions/wrapper.rb b/test/features/step_definitions/wrapper.rb new file mode 100644 index 0000000..8869d50 --- /dev/null +++ b/test/features/step_definitions/wrapper.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module WrapperHelper + def reject_then_select(lines, exclude, select) + lines.select do |e| + (exclude.nil? || e !~ exclude) && e =~ select + end.sort + end + + def compare_env(exclude, include) + env_bash = reject_then_select(@response_bash[:env], exclude, include) + env_fish = reject_then_select(@response_fish[:env], exclude, include) + expect(env_fish).to eq(env_bash) + end +end +World WrapperHelper + +When('we run {string} in Bash and Fish') do |command| + @response_bash = run_bash_command(command) + @response_fish = run_fish_command(command) +end + +Then('the exit code is the same') do + expect(@response_fish[:status]).to eq(@response_bash[:status]) +end + +Then('the output is the same') do + %i[stdout stderr].each do |out| + expect(@response_fish[out]).to eq(@response_bash[out]) + end +end + +Then('environment variable(s) {env_glob} is/are the same') do |pattern| + compare_env(nil, pattern) +end + +Then('environment variable(s) {env_glob} is/are the same except for {env_glob}') do |pattern, exclude_pattern| + compare_env(exclude_pattern, pattern) +end diff --git a/test/features/support/helpers.rb b/test/features/support/helpers.rb new file mode 100644 index 0000000..4ea155c --- /dev/null +++ b/test/features/support/helpers.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' + +def list_installed_candidates + candidates = {} + + Dir["#{ENV['HOME']}/.sdkman/candidates/*/*"].each do |candidate_dir| + %r{/([^/]+)/([^/]+)$}.match(candidate_dir) do |match| + candidate = match[1] + version = match[2] + candidates[candidate] = [] unless candidates.key?(candidate) + candidates[candidate].push version unless version == 'current' + end + end + + candidates +end + +def installed?(candidate, version = nil) + candidates = list_installed_candidates + candidates.key?(candidate) \ + && (version.nil? || candidates[candidate].include?(version)) +end + +def run_bash_command(cmd) + Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| + files = %i[status stdout stderr env].map do |s| + [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] + end.to_h + + out, status = Open3.capture2e(<<~BASH + bash -c 'source "#{ENV['HOME']}/.sdkman/bin/sdkman-init.sh" && \ + #{cmd} > #{files[:stdout]} 2> #{files[:stderr]}; \ + echo "$?" > #{files[:status]}; \ + env > #{files[:env]}; \ + ' + BASH + ) + + unless status.success? + warn(out) + raise "Bash command failed: #{out}" + end + + { + status: File.read(files[:status]).to_i, + stdout: File.readlines(files[:stdout]), + stderr: File.readlines(files[:stderr]), + env: File.readlines(files[:env]).map do |l| + l.strip.split('=', 2) \ + if l.include?('=') # NB: on macOS, weird stuff is printed by env + end.compact \ + .to_h + } + end +end + +def run_fish_command(cmd) + Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| + files = %i[status stdout stderr env].map do |s| + [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] + end.to_h + + out, status = Open3.capture2e(<<~FISH + fish -c '#{cmd} > #{files[:stdout]} ^ #{files[:stderr]}; \ + echo $status > #{files[:status]}; \ + env > #{files[:env]}; \ + ' + FISH + ) + + unless status.success? + warn(out) + raise "Fish command failed: #{out}" + end + + { + status: File.read(files[:status]).to_i, + stdout: File.readlines(files[:stdout]), + stderr: File.readlines(files[:stderr]), + env: File.readlines(files[:env]).map do |l| + l.strip.split('=', 2) \ + if l.include?('=') # NB: on macOS, weird stuff is printed by env + end.compact \ + .to_h + } + end +end diff --git a/test/features/support/parameter_types.rb b/test/features/support/parameter_types.rb new file mode 100644 index 0000000..492bdde --- /dev/null +++ b/test/features/support/parameter_types.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +ParameterType( + name: 'patterns', + regexp: %r{([^\s]*|'[^']*'|/[^/]*/)(,\s*([^\s]*|'[^']*'|/[^/]*/))*}, + type: Array, + transformer: lambda do |*patterns| + patterns \ + .map(&:strip) \ + .map do |s| + s = if %r{^/(.*)/$} =~ s + Regexp.last_match(1) + elsif %r{^'(.*)'$} =~ s + "^#{Regexp.escape(Regexp.last_match(1))}$" + else + "^#{Regexp.escape(s)}$" + end + Regexp.compile(s) + end + end +) + +ParameterType( + name: 'env_name', + regexp: /[A-Z_]+/, + type: String, + transformer: ->(s) { s } +) + +ParameterType( + name: 'env_glob', + regexp: /[A-Z_*]+/, + type: Regexp, + transformer: lambda do |glob| + /^#{glob.gsub('*', '[A-Z_]*')}=/ + end +) diff --git a/test/features/wrapper.feature b/test/features/wrapper.feature new file mode 100644 index 0000000..529c61f --- /dev/null +++ b/test/features/wrapper.feature @@ -0,0 +1,30 @@ +Feature: Wrapping of Bash + All calls to sdk are performed through Bash; + we need to wrap those calls in such a way that + the effect sdk has on the Bash environment carries + over the current Fish session. + + We verifiy equality of (standard) output, exit code, and environment variables. + + Background: + Given SDKMAN! candidate list is up to date + And candidate ant is installed at version 1.9.9 + And candidate ant is installed at version 1.10.1 + + Scenario Outline: + When we run "" in Bash and Fish + Then the exit code is the same + And the output is the same + And environment variable PATH is the same + And environment variables *_HOME are the same + And environment variables SDKMAN_* are the same except for SDKMAN_OFFLINE_MODE + # NB: SDKMAN_OFFLINE_MODE is not an environment variable in bash, so ignore it here. + Examples: + | command | + | sdk | + | sdk version | + | sdk list java | + | sdk update | + | sdk use ant 1.9.9 | + | sdk offline enable > /dev/null; sdk install ant foo | + | sdk use ant 1.9.9 > /dev/null; sdk broadcast | diff --git a/test/prepare_tests.sh b/test/prepare_tests.sh deleted file mode 100644 index 981d4e0..0000000 --- a/test/prepare_tests.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -source "${HOME}"/.sdkman/bin/sdkman-init.sh - -# Set up an SDK with two installed versions -# --> test of `sdk use` in wrapper.fish -# --> tests in completion.rb -sdk install ant 1.9.9 -echo "y" | sdk install ant 1.10.1 -sdk default ant 1.10.1 \ No newline at end of file diff --git a/test/reinitialize.fish b/test/reinitialize.fish deleted file mode 100755 index 564e74b..0000000 --- a/test/reinitialize.fish +++ /dev/null @@ -1,27 +0,0 @@ -# If either of -# - $SDKMAN_DIR is unset -# - $SDKMAN_DIR points to a directory not owned by the current user -# is true, sdkman-for-fish should run sdkman's init script. - -# Assumes sdkman-for-fish is installed -set proper_value "$SDKMAN_DIR" - -begin - set -e SDKMAN_DIR - set in_new_shell (fish -lc 'echo $SDKMAN_DIR') - if [ "$in_new_shell" != "$proper_value" ] - echo "SDKMAN_DIR initialized to $in_new_shell instead of $proper_value" - exit 1 - end -end - -begin - set -x SDKMAN_DIR "/" # belongs to root, who hopefully doesn't run this - set in_new_shell (fish -lc 'echo $SDKMAN_DIR') - if [ "$in_new_shell" != "$proper_value" ] - echo "SDKMAN_DIR reinitialized to $in_new_shell instead of $proper_value" - exit 1 - end -end - -# TODO: add test that fails if `test` in conf.d/sdk.fish:80 errors (cf issue #28 et al.) diff --git a/test/remove_sdkman.sh b/test/remove_sdkman.sh deleted file mode 100644 index f8674cb..0000000 --- a/test/remove_sdkman.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - - rm -rf "${HOME}/.sdkman" \ -&& sed -i'.bak' -E -e 's/^.*(sdkman|SDKMAN).*$//g' "${HOME}/.bashrc" \ -&& echo 'SDKMAN! uninstalled' diff --git a/test/wrapper.fish b/test/wrapper.fish deleted file mode 100644 index 3103723..0000000 --- a/test/wrapper.fish +++ /dev/null @@ -1,64 +0,0 @@ -# Test that a couple of commands have the same effect when run through -# the fish wrapper and directly. -# Verifies equality of (standard) output, exit code, and PATH. - -set test_commands \ - "sdk" \ - "sdk version" \ - "sdk list java" \ - "sdk update" \ - "sdk use ant 1.9.9" \ - "sdk offline enable > /dev/null; sdk install ant foo" \ - "sdk use ant 1.9.9 > /dev/null; sdk broadcast" -set test_count (count $test_commands) -set check_count (math "3 * $test_count") - -set sdk_init "$HOME/.sdkman/bin/sdkman-init.sh" - -if [ (uname) = "Linux" ] - function checksum -a file - sha256sum $file | cut -d " " -f 1 - end -else # assume macOS - function checksum -a file - shasum -a 256 $file | cut -d " " -f 1 - end -end - -echo "Testing the sdk wrapper" -set failures 0 -for sdk_cmd in $test_commands - echo " Testing '$sdk_cmd'" - bash -c "source \"$sdk_init\" && $sdk_cmd > sout_bash; - echo \"\$?\" > status_bash; - echo \"\$PATH\" > path_bash; - echo \"\$ANT_HOME\" > anthome_bash" - fish -c "$sdk_cmd > sout_fish; - echo \"\$status\" > status_fish; - echo \"\$PATH\" > path_fish; - echo \"\$ANT_HOME\" > anthome_fish" - - # For nicer diffs: one entry per line, sorted - string split ":" (cat path_bash) | sort > path_bash - string split ":" (cat path_fish) \ - | string split " " \ - | sort > path_fish - # split by spaces for fish 2.* - - for out in sout status path anthome - if [ (checksum "$out"_bash) != (checksum "$out"_fish) ] - echo " - $out bad:" - diff "$out"_bash "$out"_fish | sed -e 's/^/ /' - set failures (math $failures + 1) - else - echo " - $out ok!" - end - end - echo "" -end - -rm {sout,status,path}_{bash,fish} - -echo "Ran $test_count commands with 3 checks each." -echo "$failures/$check_count checks failed." -exit $failures