From 0c15f199cd34a26737d2cc811e091157c3633986 Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Mon, 25 May 2020 20:17:31 +0200 Subject: [PATCH 1/9] Migrate completion tests to Cucumber. Also retire Fish 2.x + macOS test; installation of custom brew didn't work out anymore. --- .idea/runConfigurations/Test.xml | 17 ++ .travis.yml | 16 +- README.md | 16 ++ test/Dockerfile | 33 ++++ test/Gemfile | 6 + test/Homebrew-Formula-fish-2.7.1.rb | 60 ------ test/completion.rb | 162 ---------------- test/features/completions.feature | 186 +++++++++++++++++++ test/features/step_definitions/completion.rb | 26 +++ test/features/step_definitions/setup.rb | 31 ++++ test/features/support/helpers.rb | 42 +++++ test/prepare_tests.sh | 5 +- 12 files changed, 364 insertions(+), 236 deletions(-) create mode 100644 .idea/runConfigurations/Test.xml create mode 100644 test/Dockerfile create mode 100644 test/Gemfile delete mode 100644 test/Homebrew-Formula-fish-2.7.1.rb delete mode 100644 test/completion.rb create mode 100644 test/features/completions.feature create mode 100644 test/features/step_definitions/completion.rb create mode 100644 test/features/step_definitions/setup.rb create mode 100644 test/features/support/helpers.rb 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..7a17061 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,13 +16,6 @@ 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: @@ -33,18 +26,19 @@ matrix: 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; sdk version; ruby --version; 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 script: - - ruby test/completion.rb - - fish test/wrapper.fish + - (cd test && cucumber) + # TODO: Migrate these to Cucumber: + - bash test/prepare_tests.sh && 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" diff --git a/README.md b/README.md index 38e6031..56b3490 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,22 @@ _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: + +```bash +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. + ## Acknowledgements * Completion originally by [Ted Wise](https://github.com/ctwise); see his diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..09f8669 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,33 @@ +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 + +# "Install" sdkman-for-fish +RUN mkdir -p $TEST_HOME/.config/fish/ +COPY completions $TEST_HOME/.config/fish/completions/ +COPY conf.d $TEST_HOME/.config/fish/conf.d/ +COPY completions $TEST_HOME/.config/fish/functions/ +RUN ls -R $TEST_HOME/.config/fish/ + +# Run tests +COPY test ./ +CMD cucumber 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/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..74ee349 --- /dev/null +++ b/test/features/completions.feature @@ -0,0 +1,186 @@ +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 "" + Examples: + | cmd | completions | + | b | b, broadcast | + | c | c, current | + | d | d, default | + | f | flush | + | h | h, help | + | i | i, install | + | in | install | + | l | list, ls | + | o | offline | + | r | rm | + | s | selfupdate | + | u | u, ug, uninstall, update, upgrade, use | + | un | uninstall | + | up | update, upgrade | + | us | use | + | v | v, version | + # Currently uncovered; include to catch new upstream commands: + | a | | + | e | | + | g | | + | j | | + | k | | + | m | | + | n | | + | p | | + | q | | + | t | | + | w | | + | x | | + | y | | + | z | | + + Scenario Outline: Completion for 'install' + When the user enters "install " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | an | ant | + | xyz | | + | 1. | | + | gra | gradle, grails | + | grad | gradle | + | gradk | | +# | ant 1.10. | 1.10.0, 1.10.1 | # TODO: list installable versions --> issue #4 + | ant 1.10.2-mine /tm | /tmp/ | + | 'ant 1.10.2-mine /tmp ' | | + + Scenario Outline: Completion for 'uninstall' + When the user enters "uninstall " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | ant, crash | + | a | ant | + | j | | + | 1. | | + | an | ant | # installed + | gr | | # none installed + | xyz | | # no such candidate + | 'an ' | | # no such candidate installed + | 'ant 1' | 1.10.1, 1.9.9 | + | 'ant 1.10.' | 1.10.1 | + | 'ant 2' | | + | 'ant 1.10.1 ' | | + + Scenario Outline: Completion for 'list' + When the user enters "list " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | an | ant | + | xyz | | + | 1. | | + | 'ant ' | | + + Scenario Outline: Completion for 'use' + When the user enters "use " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | ant, crash | + | an | ant | + | j | | + | 1. | | + | 'ant ' | 1.10.1, 1.9.9 | + | 'ant 1.10.1 ' | | + + Scenario Outline: Completion for 'default' + When the user enters "default " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | ant, crash | + | an | ant | + | j | | + | 1. | | + | 'ant ' | 1.10.1, 1.9.9 | + | 'ant 1.10.1 ' | | + + Scenario Outline: Completion for 'current' + When the user enters "current " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | an | ant | # --> installed version + | ja | java | # --> not installed + | xyz | | + | 1. | | + | 'ant ' | | + + Scenario Outline: Completion for 'upgrade' + When the user enters "upgrade " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | ant, crash | + | an | ant | + | j | | + | 1. | | + | 'ant ' | | + + Scenario Outline: Completion for 'offline' + When the user enters "offline " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | disable, enable | + | e | enable | + | d | disable | + | a | | + | 'enable ' | | + + Scenario Outline: Completion for 'selfupdate' + When the user enters "selfupdate " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | force | + | f | force | + | a | | + | 'force ' | | + + Scenario Outline: Completion for 'flush' + When the user enters "flush " into the prompt + Then completion should propose "" + Examples: + | cmd | completions | + | | archives, broadcast, temp | + | b | broadcast | + | a | archives | + | t | temp | + | x | | + | 'temp ' | | + + + Scenario Outline: Completion for commands without parameters + When the user enters "" into the prompt + Then completion should propose "" + Examples: + | cmd | + | 'version ' | + | 'version a' | + | 'broadcast ' | + | 'broadcast a' | + | 'help ' | + | 'help a' | + | 'update ' | + | 'update a' | diff --git a/test/features/step_definitions/completion.rb b/test/features/step_definitions/completion.rb new file mode 100644 index 0000000..69f8cea --- /dev/null +++ b/test/features/step_definitions/completion.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'open3' + +module CompletionHelper + def complete(cmd) + completions = run_fish_command("complete -C\"sdk #{cmd}\"") + + completions.split("\n") \ + .map { |line| line.split(/\s+/)[0].strip } \ + .sort \ + .uniq \ + .join(', ') + # TODO: Why do we get duplicates in the Docker container? + # --> remove uniq + end +end +World CompletionHelper + +When('the user enters {string} into the prompt') do |cmd| + @response = complete(cmd.gsub(/["']/, '')) +end + +Then(/^completion should propose "(.*)"$/) do |completions| + expect(@response).to eq(completions) +end diff --git a/test/features/step_definitions/setup.rb b/test/features/step_definitions/setup.rb new file mode 100644 index 0000000..0609b47 --- /dev/null +++ b/test/features/step_definitions/setup.rb @@ -0,0 +1,31 @@ +$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| + 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 + +# 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| + %r{/([^/]+)/([^/]+)$}.match(candidate_dir) do |match| + candidate = match[1] + version = match[2] + run_bash_command("sdk rm #{candidate} #{version}") unless version == 'current' + end + end +end diff --git a/test/features/support/helpers.rb b/test/features/support/helpers.rb new file mode 100644 index 0000000..f6096d8 --- /dev/null +++ b/test/features/support/helpers.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +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) + stdout, stderr, status = Open3.capture3("bash -c 'source \"$HOME/.sdkman/bin/sdkman-init.sh\"; #{cmd}'") + unless status.success? + warn(stderr) + raise "Bash command failed: #{stderr}" + end + + stdout +end + +def run_fish_command(cmd) + # NB: Fish errors out if we don't set terminal dimensions + stdout, stderr, status = Open3.capture3("fish -c 'stty rows 80 columns 80; #{cmd}'") + unless status.success? + warn(stderr) + raise 'Fish command failed' + end + + stdout +end diff --git a/test/prepare_tests.sh b/test/prepare_tests.sh index 981d4e0..6305b42 100644 --- a/test/prepare_tests.sh +++ b/test/prepare_tests.sh @@ -3,8 +3,7 @@ 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 +# --> test of `sdk use` in wrapper.fish 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 +sdk default ant 1.10.1 From 479fa1e541f7549d45bfa21382c89235476cb189 Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 00:00:04 +0200 Subject: [PATCH 2/9] Account for fuzzy completions. Introduced in Fish 3.1 cf. https://github.com/fish-shell/fish-shell/issues/5467 Needed to make tests less strict. Instead of checking the exact list of matches, we require the expected ones and exclude some others. --- completions/sdk.fish | 6 +- test/features/completions.feature | 223 ++++++++++--------- test/features/step_definitions/completion.rb | 19 +- test/features/support/parameter_types.rb | 21 ++ 4 files changed, 154 insertions(+), 115 deletions(-) create mode 100644 test/features/support/parameter_types.rb 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/features/completions.feature b/test/features/completions.feature index 74ee349..d2a2710 100644 --- a/test/features/completions.feature +++ b/test/features/completions.feature @@ -14,166 +14,179 @@ Feature: Shell Completion Scenario Outline: Commands complete When the user enters "" into the prompt Then completion should propose "" + But completion should not propose Examples: - | cmd | completions | - | b | b, broadcast | - | c | c, current | - | d | d, default | - | f | flush | - | h | h, help | - | i | i, install | - | in | install | - | l | list, ls | - | o | offline | - | r | rm | - | s | selfupdate | - | u | u, ug, uninstall, update, upgrade, use | - | un | uninstall | - | up | update, upgrade | - | us | use | - | v | v, version | - # Currently uncovered; include to catch new upstream commands: - | a | | - | e | | - | g | | - | j | | - | k | | - | m | | - | n | | - | p | | - | q | | - | t | | - | w | | - | x | | - | y | | - | z | | + | 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 | - | an | ant | - | xyz | | - | 1. | | - | gra | gradle, grails | - | grad | gradle | - | gradk | | -# | ant 1.10. | 1.10.0, 1.10.1 | # TODO: list installable versions --> issue #4 - | ant 1.10.2-mine /tm | /tmp/ | - | 'ant 1.10.2-mine /tmp ' | | + | 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 | - | | ant, crash | - | a | ant | - | j | | - | 1. | | - | an | ant | # installed - | gr | | # none installed - | xyz | | # no such candidate - | 'an ' | | # no such candidate installed - | 'ant 1' | 1.10.1, 1.9.9 | - | 'ant 1.10.' | 1.10.1 | - | 'ant 2' | | - | 'ant 1.10.1 ' | | + | 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 | - | an | ant | - | xyz | | - | 1. | | - | 'ant ' | | + | 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 | - | | ant, crash | - | an | ant | - | j | | - | 1. | | - | 'ant ' | 1.10.1, 1.9.9 | - | 'ant 1.10.1 ' | | + | 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 | - | | ant, crash | - | an | ant | - | j | | - | 1. | | - | 'ant ' | 1.10.1, 1.9.9 | - | 'ant 1.10.1 ' | | + | 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 | - | an | ant | # --> installed version - | ja | java | # --> not installed - | xyz | | - | 1. | | - | 'ant ' | | + | 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 | - | | ant, crash | - | an | ant | - | j | | - | 1. | | - | 'ant ' | | + | 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 | - | | disable, enable | - | e | enable | - | d | disable | - | a | | - | 'enable ' | | + | 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 | - | | force | - | f | force | - | a | | - | 'force ' | | + | 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 | - | | archives, broadcast, temp | - | b | broadcast | - | a | archives | - | t | temp | - | x | | - | 'temp ' | | + | 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 propose "" + Then completion should not propose /.*/ Examples: | cmd | | 'version ' | diff --git a/test/features/step_definitions/completion.rb b/test/features/step_definitions/completion.rb index 69f8cea..d7651dd 100644 --- a/test/features/step_definitions/completion.rb +++ b/test/features/step_definitions/completion.rb @@ -7,12 +7,8 @@ module CompletionHelper completions = run_fish_command("complete -C\"sdk #{cmd}\"") completions.split("\n") \ - .map { |line| line.split(/\s+/)[0].strip } \ - .sort \ - .uniq \ - .join(', ') + .map { |line| line.split(/\s+/)[0].strip } # TODO: Why do we get duplicates in the Docker container? - # --> remove uniq end end World CompletionHelper @@ -21,6 +17,15 @@ When('the user enters {string} into the prompt') do |cmd| @response = complete(cmd.gsub(/["']/, '')) end -Then(/^completion should propose "(.*)"$/) do |completions| - expect(@response).to eq(completions) +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/support/parameter_types.rb b/test/features/support/parameter_types.rb new file mode 100644 index 0000000..a1f5f76 --- /dev/null +++ b/test/features/support/parameter_types.rb @@ -0,0 +1,21 @@ +# 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 +) From 56bc601b174ad3c8a1b4fecf07db4375611a06e5 Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 02:49:06 +0200 Subject: [PATCH 3/9] Migrate wrapper test to Cucumber. --- .travis.yml | 1 - test/Dockerfile | 6 +- test/features/step_definitions/completion.rb | 5 +- test/features/step_definitions/wrapper.rb | 39 ++++++++++ test/features/support/helpers.rb | 75 ++++++++++++++++---- test/features/support/parameter_types.rb | 9 +++ test/features/wrapper.feature | 30 ++++++++ test/prepare_tests.sh | 9 --- test/wrapper.fish | 64 ----------------- 9 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 test/features/step_definitions/wrapper.rb create mode 100644 test/features/wrapper.feature delete mode 100644 test/prepare_tests.sh delete mode 100644 test/wrapper.fish diff --git a/.travis.yml b/.travis.yml index 7a17061..dda17be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,6 @@ install: script: - (cd test && cucumber) # TODO: Migrate these to Cucumber: - - bash test/prepare_tests.sh && 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" diff --git a/test/Dockerfile b/test/Dockerfile index 09f8669..4956943 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -23,9 +23,9 @@ RUN curl -s "https://get.sdkman.io" | bash # "Install" sdkman-for-fish RUN mkdir -p $TEST_HOME/.config/fish/ -COPY completions $TEST_HOME/.config/fish/completions/ -COPY conf.d $TEST_HOME/.config/fish/conf.d/ -COPY completions $TEST_HOME/.config/fish/functions/ +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 diff --git a/test/features/step_definitions/completion.rb b/test/features/step_definitions/completion.rb index d7651dd..474038b 100644 --- a/test/features/step_definitions/completion.rb +++ b/test/features/step_definitions/completion.rb @@ -4,10 +4,9 @@ require 'open3' module CompletionHelper def complete(cmd) - completions = run_fish_command("complete -C\"sdk #{cmd}\"") + completions = run_fish_command("complete -C\"sdk #{cmd}\"")[:stdout] - completions.split("\n") \ - .map { |line| line.split(/\s+/)[0].strip } + completions.map { |line| line.split(/\s+/)[0].strip } # TODO: Why do we get duplicates in the Docker container? 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 index f6096d8..ae57b3c 100644 --- a/test/features/support/helpers.rb +++ b/test/features/support/helpers.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'fileutils' +require 'tmpdir' + def list_installed_candidates candidates = {} @@ -17,26 +20,70 @@ end def installed?(candidate, version = nil) candidates = list_installed_candidates - candidates.key?(candidate) && (version.nil? || candidates[candidate].include?(version)) + candidates.key?(candidate) \ + && (version.nil? || candidates[candidate].include?(version)) end def run_bash_command(cmd) - stdout, stderr, status = Open3.capture3("bash -c 'source \"$HOME/.sdkman/bin/sdkman-init.sh\"; #{cmd}'") - unless status.success? - warn(stderr) - raise "Bash command failed: #{stderr}" - end + Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| + files = %i[status stdout stderr env].map { |s| + [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] + }.to_h - stdout + 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]) + } + end end +# # 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.* + def run_fish_command(cmd) - # NB: Fish errors out if we don't set terminal dimensions - stdout, stderr, status = Open3.capture3("fish -c 'stty rows 80 columns 80; #{cmd}'") - unless status.success? - warn(stderr) - raise 'Fish command failed' - end + Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| + files = %i[status stdout stderr env].map { |s| + [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] + }.to_h - stdout + 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]) + } + end end diff --git a/test/features/support/parameter_types.rb b/test/features/support/parameter_types.rb index a1f5f76..27db9af 100644 --- a/test/features/support/parameter_types.rb +++ b/test/features/support/parameter_types.rb @@ -19,3 +19,12 @@ ParameterType( end end ) + +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 6305b42..0000000 --- a/test/prepare_tests.sh +++ /dev/null @@ -1,9 +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 -sdk install ant 1.9.9 -echo "y" | sdk install ant 1.10.1 -sdk default ant 1.10.1 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 From 0a948c81c1ac35adad5621b1ccc1dedea7500bfe Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 04:22:01 +0200 Subject: [PATCH 4/9] Migrate reinitialization test to Cucumber. --- .travis.yml | 1 - test/features/corner_cases.feature | 13 ++++++ .../features/step_definitions/corner_cases.rb | 45 +++++++++++++++++++ test/features/support/helpers.rb | 4 +- test/features/support/parameter_types.rb | 7 +++ test/reinitialize.fish | 27 ----------- 6 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 test/features/corner_cases.feature create mode 100644 test/features/step_definitions/corner_cases.rb delete mode 100755 test/reinitialize.fish diff --git a/.travis.yml b/.travis.yml index dda17be..8f9305c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,5 @@ install: script: - (cd test && cucumber) # TODO: Migrate these to Cucumber: - - 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" diff --git a/test/features/corner_cases.feature b/test/features/corner_cases.feature new file mode 100644 index 0000000..4327d06 --- /dev/null +++ b/test/features/corner_cases.feature @@ -0,0 +1,13 @@ +Feature: Corner Cases + + Scenario: SDKMAN_DIR unset + 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: SDKMAN_DIR set to a location the current user can't write at + 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.) diff --git a/test/features/step_definitions/corner_cases.rb b/test/features/step_definitions/corner_cases.rb new file mode 100644 index 0000000..8152faa --- /dev/null +++ b/test/features/step_definitions/corner_cases.rb @@ -0,0 +1,45 @@ +# 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 diff --git a/test/features/support/helpers.rb b/test/features/support/helpers.rb index ae57b3c..ba1ce4e 100644 --- a/test/features/support/helpers.rb +++ b/test/features/support/helpers.rb @@ -48,7 +48,7 @@ def run_bash_command(cmd) status: File.read(files[:status]).to_i, stdout: File.readlines(files[:stdout]), stderr: File.readlines(files[:stderr]), - env: File.readlines(files[:env]) + env: File.readlines(files[:env]).map { |l| l.strip.split('=', 2) }.to_h } end end @@ -83,7 +83,7 @@ def run_fish_command(cmd) status: File.read(files[:status]).to_i, stdout: File.readlines(files[:stdout]), stderr: File.readlines(files[:stderr]), - env: File.readlines(files[:env]) + env: File.readlines(files[:env]).map { |l| l.strip.split('=', 2) }.to_h } end end diff --git a/test/features/support/parameter_types.rb b/test/features/support/parameter_types.rb index 27db9af..492bdde 100644 --- a/test/features/support/parameter_types.rb +++ b/test/features/support/parameter_types.rb @@ -20,6 +20,13 @@ ParameterType( end ) +ParameterType( + name: 'env_name', + regexp: /[A-Z_]+/, + type: String, + transformer: ->(s) { s } +) + ParameterType( name: 'env_glob', regexp: /[A-Z_*]+/, 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.) From 42aff3b4c5ff348c7400866cc94010f9d19ff72b Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 04:48:12 +0200 Subject: [PATCH 5/9] Migrate PATH zombie test to Cucumber. --- .travis.yml | 1 - test/check_for_path_zombies.fish | 9 -------- test/features/corner_cases.feature | 5 +++++ .../features/step_definitions/corner_cases.rb | 5 +++++ test/features/step_definitions/setup.rb | 22 ++++++++++++++----- 5 files changed, 27 insertions(+), 15 deletions(-) delete mode 100644 test/check_for_path_zombies.fish diff --git a/.travis.yml b/.travis.yml index 8f9305c..cec1ac2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,5 +38,4 @@ install: script: - (cd test && cucumber) # TODO: Migrate these to Cucumber: - - 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" 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/features/corner_cases.feature b/test/features/corner_cases.feature index 4327d06..8a8a443 100644 --- a/test/features/corner_cases.feature +++ b/test/features/corner_cases.feature @@ -11,3 +11,8 @@ Feature: Corner Cases 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/step_definitions/corner_cases.rb b/test/features/step_definitions/corner_cases.rb index 8152faa..198cae4 100644 --- a/test/features/step_definitions/corner_cases.rb +++ b/test/features/step_definitions/corner_cases.rb @@ -43,3 +43,8 @@ Then('environment variable {env_name} has the original value') do |name| 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/setup.rb b/test/features/step_definitions/setup.rb index 0609b47..82b6766 100644 --- a/test/features/step_definitions/setup.rb +++ b/test/features/step_definitions/setup.rb @@ -14,6 +14,22 @@ 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: @@ -22,10 +38,6 @@ end # --> clean up after _all_ features at least at_exit do Dir["#{ENV['HOME']}/.sdkman/candidates/*/*"].each do |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 + _uninstall_candidate_version(candidate_dir) end end From 7e97c4c1c19b30dbf6a85baa659c7cd538e5a10a Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 05:07:24 +0200 Subject: [PATCH 6/9] Migrate installer test to Cucumber. --- .travis.yml | 2 -- test/features/installer.feature | 11 ++++++++++ test/features/step_definitions/installer.rb | 23 +++++++++++++++++++++ test/remove_sdkman.sh | 5 ----- 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 test/features/installer.feature create mode 100644 test/features/step_definitions/installer.rb delete mode 100644 test/remove_sdkman.sh diff --git a/.travis.yml b/.travis.yml index cec1ac2..27836d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,5 +37,3 @@ install: script: - (cd test && cucumber) - # TODO: Migrate these to Cucumber: - - bash test/remove_sdkman.sh > /dev/null && fish -c "echo 'y' | sdk" > /dev/null && fish -c "sdk version" 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/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/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' From efd9cf0c0b746ee068ba5e0cdeab0c233fc07249 Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 05:35:28 +0200 Subject: [PATCH 7/9] Polish Travis build. --- .travis.yml | 18 ++++++++++++------ test/features/support/helpers.rb | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 27836d2..d17c78f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,19 +21,25 @@ matrix: 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 - bundle install --gemfile=test/Gemfile --no-cache - - uname -a; fish --version; sdk version; ruby --version; cucumber --version + - |- + 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/ + - |- + 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: - (cd test && cucumber) diff --git a/test/features/support/helpers.rb b/test/features/support/helpers.rb index ba1ce4e..99cad9e 100644 --- a/test/features/support/helpers.rb +++ b/test/features/support/helpers.rb @@ -44,6 +44,7 @@ def run_bash_command(cmd) raise "Bash command failed: #{out}" end + puts File.readlines(files[:env]) # TODO remove { status: File.read(files[:status]).to_i, stdout: File.readlines(files[:stdout]), From 2afb22f6afe64bdffc15a0396cf62990f09bb01e Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 06:01:37 +0200 Subject: [PATCH 8/9] Fix tests on macOS. Also clarifies a test description. --- test/features/corner_cases.feature | 4 ++-- test/features/support/helpers.rb | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/features/corner_cases.feature b/test/features/corner_cases.feature index 8a8a443..a2c211f 100644 --- a/test/features/corner_cases.feature +++ b/test/features/corner_cases.feature @@ -1,11 +1,11 @@ Feature: Corner Cases - Scenario: SDKMAN_DIR unset + 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: SDKMAN_DIR set to a location the current user can't write at + 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 diff --git a/test/features/support/helpers.rb b/test/features/support/helpers.rb index 99cad9e..4ea155c 100644 --- a/test/features/support/helpers.rb +++ b/test/features/support/helpers.rb @@ -26,9 +26,9 @@ end def run_bash_command(cmd) Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| - files = %i[status stdout stderr env].map { |s| + files = %i[status stdout stderr env].map do |s| [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] - }.to_h + end.to_h out, status = Open3.capture2e(<<~BASH bash -c 'source "#{ENV['HOME']}/.sdkman/bin/sdkman-init.sh" && \ @@ -44,28 +44,24 @@ def run_bash_command(cmd) raise "Bash command failed: #{out}" end - puts File.readlines(files[:env]) # TODO remove { status: File.read(files[:status]).to_i, stdout: File.readlines(files[:stdout]), stderr: File.readlines(files[:stderr]), - env: File.readlines(files[:env]).map { |l| l.strip.split('=', 2) }.to_h + 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 -# # 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.* - def run_fish_command(cmd) Dir.mktmpdir(%w[sdkman-for-fish-test_ _fish]) do |tmp_dir| - files = %i[status stdout stderr env].map { |s| + files = %i[status stdout stderr env].map do |s| [s, FileUtils.touch("#{tmp_dir}/#{s}")[0]] - }.to_h + end.to_h out, status = Open3.capture2e(<<~FISH fish -c '#{cmd} > #{files[:stdout]} ^ #{files[:stderr]}; \ @@ -84,7 +80,11 @@ def run_fish_command(cmd) status: File.read(files[:status]).to_i, stdout: File.readlines(files[:stdout]), stderr: File.readlines(files[:stderr]), - env: File.readlines(files[:env]).map { |l| l.strip.split('=', 2) }.to_h + 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 From 15997dcba8e0aa413836e912180ea8c2efe33e88 Mon Sep 17 00:00:00 2001 From: Raphael Reitzig <4246780+reitzig@users.noreply.github.com> Date: Tue, 26 May 2020 16:35:31 +0200 Subject: [PATCH 9/9] Allow to pass parameters to Cucumber in Docker. Also add some comments. --- README.md | 27 ++++++++++++++++++++----- test/Dockerfile | 10 ++++++++- test/features/step_definitions/setup.rb | 9 +++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 56b3490..39fe94e 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ [![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 +Version 1.5.0 tested with - - fish 2.7.1 and 3.0.2, and - - SDKMAN! 5.7.4, on + - fish 2.7.1 and 3.1.2, and + - SDKMAN! 5.8.2, on - Ubuntu 18.04 LTS and macOS 10.13 ## Install @@ -38,13 +38,29 @@ 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: -```bash +```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 @@ -53,6 +69,7 @@ A run configuration for Jetbrains IDEs is included. 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/test/Dockerfile b/test/Dockerfile index 4956943..4044d2b 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -21,6 +21,13 @@ RUN groupadd -r 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/ @@ -30,4 +37,5 @@ RUN ls -R $TEST_HOME/.config/fish/ # Run tests COPY test ./ -CMD cucumber +ENTRYPOINT ["cucumber"] +CMD [] diff --git a/test/features/step_definitions/setup.rb b/test/features/step_definitions/setup.rb index 82b6766..69fdd04 100644 --- a/test/features/step_definitions/setup.rb +++ b/test/features/step_definitions/setup.rb @@ -7,6 +7,15 @@ Given(/^SDKMAN! candidate list is up to date$/) do 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