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] 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