diff --git a/README.md b/README.md index 433a9cab6..2918dad98 100644 --- a/README.md +++ b/README.md @@ -10,40 +10,68 @@ Please see the [contributing guide](https://github.com/exercism/x-api/blob/maste ### Development Dependencies -Currently, development of the Haskell track assumes that you are using -GHC 7.8.3 with Haskell Platform installed. The following packages need -to be installed if not using Haskell Platform: +There are currently two distinct ways to set up an environment to colaborate +with the development of the Haskell track: + +- Using a global GHC installation. +- Using *Stack*. + +The first method is more convenient when you just want to write code without +caring too much about which packages are being used. The second one is better +to track exercise's dependencies and test them against multiple GHC versions. + +#### Using a global GHC installation + +If you have a recent Haskell Platform installed, probably most of the packages +you need are already installed for you. Otherwise, you can manually install +the missing ones. + +The following is the list of all packages used in this repository, just don't +trust this list to be updated. + +These packages come installed with GHC: + +- array +- base +- containers +- filepath +- directory +- process +- time +- unix + +These are already installed in recent version of the Haskell Platform: + +- attoparsec +- HUnit +- text +- parallel +- QuickCheck +- random +- split +- stm +- vector + +These you will have to add to your instalation: + +- lens +- old-locale + +##### Installing missing packages ```bash -$ comm -13 \ - <(ghc-pkg --global list --simple-output --names-only \ - | tr ' ' '\n') \ - <(find . -name '*.hs' \ - | xargs awk \ - '/^import/ {if ($2 == "qualified") {print $3} else {print $2}}' \ - | sort -u \ - | xargs -n1 ghc-pkg find-module --simple-output --names-only \ - | cut -d' ' -f1 \ - | sort -u) -HUnit -QuickCheck -attoparsec -parallel -random -regex-base -regex-compat -split -stm -text -vector +$ cabal install lens ``` -### Running tests +This will download and installed the package named *lens*. + + +##### Running tests All the tests: ```bash -$./_test/check-exercises.hs +$ ./_test/check-exercises.hs -- accumulate Cases: 5 Tried: 5 Errors: 0 Failures: 0 […] @@ -68,6 +96,91 @@ Cases: 8 Tried: 8 Errors: 0 Failures: 0 SUCCESS! ``` +#### Using Stack + +If you have stack installed, you can use it to handle all the dependencies +for you. But first, you need to transform the exercises in *stack projects*. + +##### Creating a project for an exercise + +The `_test/stackalize` bash script will extract the exercise's dependencies +from `_test/dependencies.txt` and create a *stack project* for you: + +```bash +$ _test/stackalize --resolver lts-6.4 exercises/leap +``` + +This will transform the exercise *leap* in a *stack project* using the +resolver *lts-6.4*. Change it for your favourite [Stackage snapshot](https://www.stackage.org/snapshots). + +*You can make you life easier adding _test to your path.* + +##### Testing with default settings + +To download, install the compiler and packages needed, compile and test an +exercise, run the following commands: + +```bash +$ cd exercises/leap +$ stack test +``` + +##### Testing with advanced options + +Testing if it compiles without warnings and pass the tests: + +```bash +$ stack clean +$ stack test --pedantic +``` + +Testing with a specific resolver: + +```bash +$ stack test --resolver lts-2.22 # GHC-7.8.4 +$ stack test --resolver lts-6.4 # GHC-7.10.3 +$ stack test --resolver nightly-2016-06-21 # GHC-8.0.1 +$ stack test --resolver nightly # Newest compiler and packages. +``` + +If you are making major changes to an exercise, it's recommended that +you test it against those three versions of GHC with `--pedantic`, to be sure +it will not fail when your *pull request* is tested on *Travis-CI*. + +##### Undoing the stack project + +If you need to make a *commit*, you can remove the *stack project* and +change the exercise back to it's previous form: + +```bash +$ stackalize --undo exercises/leap +``` + +This command will intentionally leave the *.stack-work* folder intact, +to preserve the cache in case you decide to test it with *stack* again. + +#### The Exercises + +Each exercise in the Haskell track is composed of at least two files: + +- *name_test.hs* # Both filenames must be all lowecase and any deviations +- *example.hs* # will generate failures in the future. + +Optionally, a third file with a stub can also be provided: + +- *ModuleName.hs* # This file must be named exactly as the module. + +#### Tracking dependencies + +We also keep track of all the packages used by the examples and test files in +*_test/dependencies.txt*. If you are implementing a new exercise or changing +the dependencies of a existing one, please don't forget to update it. + +At the moment, we may not detect incorrectly specified dependencies in your +*pull requests* automatically. But soon we expect *Travis-CI* be able to +catch it, so please declare in *dependencies.txt* all the packages used by +the examples and tests. + ## License The MIT License (MIT) diff --git a/_test/dependencies b/_test/dependencies new file mode 100755 index 000000000..96b3d3a90 --- /dev/null +++ b/_test/dependencies @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +programName=`basename "$0"` +dependencies="`dirname ${BASH_SOURCE}`/dependencies.txt" +example="example.hs" +testSuffix="_test.hs" + +getDependencies () { + ( + for pattern in "$@"; do + grep "${pattern}" "${dependencies}" | cut -d: -f2 | xargs -n1 + done + ) | sort -u +} + +exampleDependencies () { + ( + for exercise in "$@"; do + getDependencies "${exercise}/${example}" + done + ) | sort -u +} + +testDependencies () { + ( + for exercise in "$@"; do + getDependencies "${exercise}/${exercise}${testSuffix}" + done + ) | sort -u +} + +selectedDependencies () { + comm ${commArgs} --output-delimiter " " \ + <(exampleDependencies "$@") \ + <(testDependencies "$@") | xargs -n1 +} + +eval set -- `getopt -o "" --long file:,examples,tests,only-in-examples,only-in-tests,examples-and-tests,examples-xor-tests -n ${programName} -- "$@"` + +commArgs="" + +while true ; do + case "$1" in + --examples ) commArgs="-2" ; shift ;; + --tests ) commArgs="-1" ; shift ;; + --only-in-examples ) commArgs="-23" ; shift ;; + --only-in-tests ) commArgs="-13" ; shift ;; + --examples-and-tests) commArgs="-12" ; shift ;; + --examples-xor-tests) commArgs="-3" ; shift ;; + --file ) case "$2" in + "") shift 2 ;; + * ) dependencies=$2 ; shift 2 ;; + esac ;; + -- ) shift ; break ;; + *) echo "$0: Unexpected error using getopt." ; exit 1 ;; + esac +done + +selectedDependencies "$@" diff --git a/_test/dependencies.txt b/_test/dependencies.txt new file mode 100644 index 000000000..2e18275fc --- /dev/null +++ b/_test/dependencies.txt @@ -0,0 +1,140 @@ +accumulate/example.hs: base +accumulate/accumulate_test.hs: base HUnit +allergies/example.hs: base +allergies/allergies_test.hs: base HUnit +anagram/example.hs: base +anagram/anagram_test.hs: base HUnit +atbash-cipher/example.hs: base split +atbash-cipher/atbash-cipher_test.hs: base HUnit +bank-account/example.hs: base stm +bank-account/bank-account_test.hs: base HUnit +beer-song/example.hs: base +beer-song/beer-song_test.hs: base HUnit +binary/example.hs: base +binary/binary_test.hs: base HUnit +binary-search-tree/example.hs: base +binary-search-tree/binary-search-tree_test.hs: base HUnit +bob/example.hs: base +bob/bob_test.hs: base HUnit +clock/example.hs: base +clock/clock_test.hs: base HUnit +connect/example.hs: base array +connect/connect_test.hs: base HUnit +crypto-square/example.hs: base split +crypto-square/crypto-square_test.hs: base HUnit +custom-set/example.hs: base +custom-set/custom-set_test.hs: base HUnit +difference-of-squares/example.hs: base +difference-of-squares/difference-of-squares_test.hs: base HUnit +etl/example.hs: base containers +etl/etl_test.hs: base HUnit containers +food-chain/example.hs: base array +food-chain/food-chain_test.hs: base HUnit +forth/example.hs: base containers text +forth/forth_test.hs: base HUnit text +gigasecond/example.hs: base time +gigasecond/gigasecond_test.hs: base HUnit old-locale time +go-counting/example.hs: base array containers +go-counting/go-counting_test.hs: base HUnit containers +grade-school/example.hs: base containers +grade-school/grade-school_test.hs: base HUnit +grains/example.hs: base +grains/grains_test.hs: base HUnit +hamming/example.hs: base +hamming/hamming_test.hs: base HUnit +hexadecimal/example.hs: base +hexadecimal/hexadecimal_test.hs: base HUnit +house/example.hs: base +house/house_test.hs: base HUnit +kindergarten-garden/example.hs: base containers split +kindergarten-garden/kindergarten-garden_test.hs: base HUnit +largest-series-product/example.hs: base +largest-series-product/largest-series-product_test.hs: base HUnit +leap/example.hs: base +leap/leap_test.hs: base HUnit +lens-person/example.hs: base lens time +lens-person/lens-person_test.hs: base HUnit time +linked-list/example.hs: base stm +linked-list/linked-list_test.hs: base HUnit +list-ops/example.hs: base +list-ops/list-ops_test.hs: base HUnit +luhn/example.hs: base +luhn/luhn_test.hs: base HUnit +matrix/example.hs: base vector +matrix/matrix_test.hs: base HUnit vector +meetup/example.hs: base time +meetup/meetup_test.hs: base HUnit time +minesweeper/example.hs: base array +minesweeper/minesweeper_test.hs: base HUnit +nth-prime/example.hs: base +nth-prime/nth-prime_test.hs: base HUnit +nucleotide-count/example.hs: base containers +nucleotide-count/nucleotide-count_test.hs: base HUnit containers +ocr-numbers/example.hs: base containers split +ocr-numbers/ocr-numbers_test.hs: base HUnit +octal/example.hs: base +octal/octal_test.hs: base QuickCheck +palindrome-products/example.hs: base +palindrome-products/palindrome-products_test.hs: base HUnit containers +parallel-letter-frequency/example.hs: base containers parallel text +parallel-letter-frequency/parallel-letter-frequency_test.hs: base HUnit containers text +pascals-triangle/example.hs: base +pascals-triangle/pascals-triangle_test.hs: base HUnit +phone-number/example.hs: base +phone-number/phone-number_test.hs: base HUnit +pig-latin/example.hs: base +pig-latin/pig-latin_test.hs: base HUnit +pov/example.hs: base +pov/pov_test.hs: base HUnit +prime-factors/example.hs: base +prime-factors/prime-factors_test.hs: base HUnit +pythagorean-triplet/example.hs: base +pythagorean-triplet/pythagorean-triplet_test.hs: base HUnit +queen-attack/example.hs: base +queen-attack/queen-attack_test.hs: base HUnit +raindrops/example.hs: base +raindrops/raindrops_test.hs: base HUnit +rna-transcription/example.hs: base +rna-transcription/rna-transcription_test.hs: base HUnit +robot-name/example.hs: base random +robot-name/robot-name_test.hs: base HUnit +robot-simulator/example.hs: base +robot-simulator/robot-simulator_test.hs: base HUnit +roman-numerals/example.hs: base +roman-numerals/roman-numerals_test.hs: base HUnit +saddle-points/example.hs: base array containers +saddle-points/saddle-points_test.hs: base HUnit array +say/example.hs: base array +say/say_test.hs: base HUnit +scrabble-score/example.hs: base containers +scrabble-score/scrabble-score_test.hs: base HUnit +secret-handshake/example.hs: base +secret-handshake/secret-handshake_test.hs: base HUnit +series/example.hs: base +series/series_test.hs: base HUnit +sgf-parsing/example.hs: base attoparsec containers text +sgf-parsing/sgf-parsing_test.hs: base HUnit containers text +sieve/example.hs: base vector +sieve/sieve_test.hs: base HUnit +simple-cipher/example.hs: base random +simple-cipher/simple-cipher_test.hs: base HUnit +simple-linked-list/example.hs: base +simple-linked-list/simple-linked-list_test.hs: base HUnit +space-age/example.hs: base +space-age/space-age_test.hs: base HUnit +strain/example.hs: base +strain/strain_test.hs: base HUnit +sublist/example.hs: base +sublist/sublist_test.hs: base HUnit +sum-of-multiples/example.hs: base +sum-of-multiples/sum-of-multiples_test.hs: base HUnit +triangle/example.hs: base containers +triangle/triangle_test.hs: base HUnit +trinary/example.hs: base +trinary/trinary_test.hs: base QuickCheck +word-count/example.hs: base containers split +word-count/word-count_test.hs: base HUnit containers +wordy/example.hs: base attoparsec text +wordy/wordy_test.hs: base HUnit +zipper/example.hs: base +zipper/zipper_test.hs: base HUnit diff --git a/_test/stackalize b/_test/stackalize new file mode 100755 index 000000000..7bb929979 --- /dev/null +++ b/_test/stackalize @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +shopt -s extglob + +exampleFile="example.hs" +testSuffix="_test.hs" +srcDir="src" +testDir="test" +programName=`basename "$0"` + +depsFinder="`dirname ${BASH_SOURCE}`/dependencies" + +generateStackYaml () { + echo "resolver: ${stackResolver}" +} + +generatePackageYaml () { + local exercise=`basename ${PWD}` + local commonDeps=`${depsFinder} --examples-and-tests ${exercise}` + local exampleDeps=`${depsFinder} --only-in-examples ${exercise}` + local testDeps=`${depsFinder} --only-in-tests ${exercise}` + + echo "name: $exercise" + echo "" + echo "dependencies:" + + for dependency in ${commonDeps}; do + echo " - ${dependency}" + done + + echo "" + echo "library:" + echo " source-dirs: ${srcDir}" + echo " dependencies:" + + for dependency in ${exampleDeps}; do + echo " - ${dependency}" + done + + echo "" + echo "tests:" + echo " test:" + echo " main: ${exercise}${testSuffix}" + echo " source-dirs: ${testDir}" + echo " dependencies:" + echo " - ${exercise}" + + for dependency in ${testDeps}; do + echo " - ${dependency}" + done +} + +stackalizeProject () { + local exercise=`basename ${PWD}` + local module=`grep "^module" ${exampleFile} | cut -d " " -f 2` + if [ ! -e "${exampleFile}" ] || + [ ! -e "${exercise}${testSuffix}" ] ; then + echo "$0: Cannot stackalize. Exercise files not found." + exit 1 + else + mkdir .extraFiles && # This first move all files to .extraFiles + mv * .extraFiles && # to avoid additional files breaking the build. + mkdir ${srcDir} && # After that we move again just the example + mkdir ${testDir} && # and the test to their destination folders. + mv .extraFiles/${exampleFile} ${srcDir}/${module}.hs && + mv .extraFiles/${exercise}${testSuffix} ${testDir} && + (generateStackYaml > stack.yaml) && + (generatePackageYaml > package.yaml) + fi +} + +stackalizeUndoProject () { + local sourceFiles=`find ${srcDir} -type f` + local exercise=`basename ${PWD}` + if [ -z $sourceFiles ] || + [ ! -e "${testDir}/${exercise}${testSuffix}" ] ; then + echo "$0: Cannot unstackalize. Exercise files not found." + exit 1; + else + local module=$(grep "^module" ${sourceFiles} | cut -d " " -f 2) + local extra=`find .extraFiles -mindepth 1 -maxdepth 1` + mv $srcDir/${module}.hs ./${exampleFile} && + mv $testDir/${exercise}$testSuffix . && + if [ ! -z "$extra" ]; then + mv ${extra} . + fi + rm -rf .extraFiles $srcDir $testDir $exercise.cabal package.yaml stack.yaml + fi +} + +stackalize () { + for dir in "$@"; do + ( + cd $dir + stackalizeProject + ) + done +} + +stackalizeUndo () { + for dir in "$@"; do + ( + cd $dir + stackalizeUndoProject + ) + done +} + + +stackResolver="" +stackUndo="NO" + +eval set -- `getopt -o "a" --long resolver:,undo -n ${programName} -- "$@"` + + +while true ; do + case "$1" in + --undo ) stackUndo="YES" ; shift 1;; + --resolver) stackResolver="$2"; shift 2;; + -- ) shift; break ;; + *) echo "$0: Unexpected error using getopt."; exit 1;; + esac +done + +if [ $# -eq 0 ]; then + folders="." +else + folders="$@" +fi + +if [ "${stackUndo}" == "YES" ]; then + stackalizeUndo ${folders} +elif [ "${stackResolver}" == "" ]; then + echo "$0: No resolver specified." + exit 1 +else + stackalize ${folders} +fi +