Merge branch 'dev'

This commit is contained in:
Raphael Reitzig 2020-06-05 11:15:43 +02:00
commit 6a57978d27
24 changed files with 697 additions and 363 deletions

17
.idea/runConfigurations/Test.xml generated Normal file
View file

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="sdkman-for-fish-tests" />
<option name="buildCliOptions" value="" />
<option name="command" value="" />
<option name="containerName" value="" />
<option name="contextFolderPath" value="." />
<option name="entrypoint" value="" />
<option name="commandLineOptions" value="--rm" />
<option name="sourceFilePath" value="test/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View file

@ -16,35 +16,30 @@ matrix:
- sourceline: "ppa:fish-shell/release-3" - sourceline: "ppa:fish-shell/release-3"
packages: packages:
- fish - 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 - os: osx
env: FISH=3 env: FISH=3
addons: addons:
homebrew: homebrew:
packages: 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 update: true # TODO: build should be green without, but isn't
before_install: before_install:
- curl -s "https://get.sdkman.io" | bash - 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: install:
- mkdir -p "${HOME}"/.config/fish/{completions,conf.d,functions} - |-
- cp completions/* "${HOME}"/.config/fish/completions/ mkdir -p "${HOME}"/.config/fish/{completions,conf.d,functions}
- cp conf.d/* "${HOME}"/.config/fish/conf.d/ cp completions/* "${HOME}"/.config/fish/completions/
- cp functions/* "${HOME}"/.config/fish/functions/ cp conf.d/* "${HOME}"/.config/fish/conf.d/
- uname -a; fish --version cp functions/* "${HOME}"/.config/fish/functions/
script: script:
- ruby test/completion.rb - (cd test && cucumber)
- 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"

View file

@ -2,14 +2,14 @@
[![Build Status][travis-badge]][travis-link] [![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. Also adds binaries from installed SDKs to the PATH.
Version 1.4.0 tested with Version 1.4.0 tested with
- fish 2.7.1 and 3.0.2, and - fish 2.7.1 and 3.1.2, and
- SDKMAN! 5.7.4, on - SDKMAN! 5.8.2, on
- Ubuntu 18.04 LTS and macOS 10.13. - Ubuntu 18.04 LTS and macOS 10.13
## Install ## Install
@ -29,6 +29,38 @@ _Note:_
It's all in the background; you should be able to run `sdk` and binaries installed It's all in the background; you should be able to run `sdk` and binaries installed
with `sdk` as you would expect. 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 ## Acknowledgements
* Completion originally by [Ted Wise](https://github.com/ctwise); see his * 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). 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 [SDKMAN!]: https://github.com/sdkman/sdkman-cli
[fish]: https://fishshell.com/
[fisher]: https://github.com/jorgebucaran/fisher [fisher]: https://github.com/jorgebucaran/fisher
[travis-link]: https://travis-ci.org/reitzig/sdkman-for-fish [travis-link]: https://travis-ci.org/reitzig/sdkman-for-fish
[travis-badge]: https://travis-ci.org/reitzig/sdkman-for-fish.svg?branch=master [travis-badge]: https://travis-ci.org/reitzig/sdkman-for-fish.svg?branch=master

View file

@ -88,13 +88,13 @@ complete -c sdk -f -n '__fish_sdkman_no_command' \
-d 'Install new version' -d 'Install new version'
complete -c sdk -f -n '__fish_sdkman_using_command i install' \ complete -c sdk -f -n '__fish_sdkman_using_command i install' \
-a "(__fish_sdkman_candidates)" -a "(__fish_sdkman_candidates)"
# TODO complete available versions --> issue #4
complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \
# TODO complete available versions --> #4
-a 'a.b.c' \ -a 'a.b.c' \
-d "version list unavailable" -d "version list unavailable"
complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \
-a 'x.y.z' \ -a 'x.y.z' \
-d "Add your own; specify path!" -d "Specify path to install custom version."
# Implicit: complete files as fourth parameter # Implicit: complete files as fourth parameter
complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 3 i install' complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 3 i install'
# block # block

41
test/Dockerfile Normal file
View file

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

6
test/Gemfile Normal file
View file

@ -0,0 +1,6 @@
source "https://rubygems.org"
group :test do
gem 'cucumber', '~> 3.1.0'
gem 'rspec', '~> 3.7.0'
end

View file

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

View file

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

View file

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

View file

@ -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 "<cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
Examples:
| cmd | completions | exclusions |
| an | ant | crash |
| xyz | | /.*/ |
| 1. | | /.*/ |
| 'ant ' | | /.*/ |
Scenario Outline: Completion for 'use'
When the user enters "use <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
Examples:
| cmd | completions | exclusions |
| | force | /^(?!force).*$/ |
| f | force | /^(?!force).*$/ |
| a | | /.*/ |
| 'force ' | | /.*/ |
Scenario Outline: Completion for 'flush'
When the user enters "flush <cmd>" into the prompt
Then completion should propose "<completions>"
But completion should not propose <exclusions>
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 "<cmd>" into the prompt
Then completion should not propose /.*/
Examples:
| cmd |
| 'version ' |
| 'version a' |
| 'broadcast ' |
| 'broadcast a' |
| 'help ' |
| 'help a' |
| 'update ' |
| 'update a' |

View file

@ -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/"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
)

View file

@ -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 "<command>" 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 |

View file

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

View file

@ -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.)

View file

@ -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'

View file

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