diff --git a/.ghci b/.ghci
index 825dbc44eefd7299ac88daac92fb8372201ccea0..13c79f5f439aaf917adf7591368659ca623222c4 100644
--- a/.ghci
+++ b/.ghci
@@ -1 +1 @@
-:set -isrc -ihtest
+:set -isrc -itest/hs
diff --git a/.gitignore b/.gitignore
index e9ed7e74cb1d35a09f66921051a91dca03a0cb2e..db07174a7710f4408eecde12ffd348f7ee34219b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,17 +87,17 @@
 /man/*.gen
 /man/footer.man
 
-# htest
-/htest/hail
-/htest/hbal
-/htest/hcheck
-/htest/hinfo
-/htest/hroller
-/htest/hscan
-/htest/hspace
-/htest/hpc-htools
-/htest/hpc-mon-collector
-/htest/test
+# test/hs
+/test/hs/hail
+/test/hs/hbal
+/test/hs/hcheck
+/test/hs/hinfo
+/test/hs/hroller
+/test/hs/hscan
+/test/hs/hspace
+/test/hs/hpc-htools
+/test/hs/hpc-mon-collector
+/test/hs/test
 
 # tools
 /tools/kvm-ifup
@@ -129,4 +129,4 @@
 # automatically-built Haskell files
 /src/Ganeti/Constants.hs
 /src/Ganeti/Version.hs
-/htest/Test/Ganeti/TestImports.hs
+/test/hs/Test/Ganeti/TestImports.hs
diff --git a/Makefile.am b/Makefile.am
index 0b0bfe71534d9e7f1dd45f6ed381572f2c52167c..83d3507a09a056f1b9712b0568e2c4468bfff2c9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -65,15 +65,15 @@ HS_DIRS = \
 	src/Ganeti/HTools/Backend \
 	src/Ganeti/HTools/Program \
 	src/Ganeti/Query \
-	htest \
-	htest/Test \
-	htest/Test/Ganeti \
-	htest/Test/Ganeti/Block \
-	htest/Test/Ganeti/Block/Drbd \
-	htest/Test/Ganeti/Confd \
-	htest/Test/Ganeti/HTools \
-	htest/Test/Ganeti/HTools/Backend \
-	htest/Test/Ganeti/Query
+	test/hs \
+	test/hs/Test \
+	test/hs/Test/Ganeti \
+	test/hs/Test/Ganeti/Block \
+	test/hs/Test/Ganeti/Block/Drbd \
+	test/hs/Test/Ganeti/Confd \
+	test/hs/Test/Ganeti/HTools \
+	test/hs/Test/Ganeti/HTools/Backend \
+	test/hs/Test/Ganeti/Query
 
 DIRS = \
 	$(HS_DIRS) \
@@ -84,9 +84,9 @@ DIRS = \
 	doc/examples \
 	doc/examples/gnt-debug \
 	doc/examples/hooks \
-	htest/data \
-	htest/data/rapi \
-	htest/shelltests \
+	test/data/htools \
+	test/data/htools/rapi \
+	test/hs/shelltests \
 	lib \
 	lib/build \
 	lib/client \
@@ -184,7 +184,7 @@ CLEANFILES = \
 	$(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
 	$(HS_BUILT_TEST_HELPERS) \
 	src/ganeti-confd \
-	.hpc/*.mix src/*.tix htest/*.tix \
+	.hpc/*.mix src/*.tix test/hs/*.tix \
 	doc/hs-lint.html
 
 GENERATED_FILES = \
@@ -415,14 +415,14 @@ HS_HTOOLS_PROGS = $(HS_BIN_ROLES) hail
 
 HS_ALL_PROGS = \
 	$(HS_PROGS) \
-	htest/hpc-htools \
-	htest/hpc-mon-collector \
-	htest/test \
+	test/hs/hpc-htools \
+	test/hs/hpc-mon-collector \
+	test/hs/test \
 	src/hconfd \
 	src/rpc-test
 
 HS_PROG_SRCS = $(patsubst %,%.hs,$(HS_ALL_PROGS))
-HS_BUILT_TEST_HELPERS = $(HS_BIN_ROLES:%=htest/%) htest/hail
+HS_BUILT_TEST_HELPERS = $(HS_BIN_ROLES:%=test/hs/%) test/hs/hail
 
 HFLAGS = \
 	-O -Wall -Werror -isrc \
@@ -432,7 +432,7 @@ HFLAGS = \
 
 # extra flags that can be overriden on the command line (e.g. -Wwarn, etc.)
 HEXTRA =
-# internal extra flags (used for htest/test mainly)
+# internal extra flags (used for test/hs/test mainly)
 HEXTRA_INT =
 # exclude options for coverage reports
 HPCEXCL = --exclude Main \
@@ -515,49 +515,49 @@ HS_LIB_SRCS = \
 	src/Ganeti/Utils.hs
 
 HS_TEST_SRCS = \
-	htest/Test/Ganeti/Attoparsec.hs \
-	htest/Test/Ganeti/BasicTypes.hs \
-	htest/Test/Ganeti/Block/Drbd/Parser.hs \
-	htest/Test/Ganeti/Block/Drbd/Types.hs \
-	htest/Test/Ganeti/Common.hs \
-	htest/Test/Ganeti/Confd/Types.hs \
-	htest/Test/Ganeti/Confd/Utils.hs \
-	htest/Test/Ganeti/Daemon.hs \
-	htest/Test/Ganeti/Errors.hs \
-	htest/Test/Ganeti/HTools/Backend/Simu.hs \
-	htest/Test/Ganeti/HTools/Backend/Text.hs \
-	htest/Test/Ganeti/HTools/CLI.hs \
-	htest/Test/Ganeti/HTools/Cluster.hs \
-	htest/Test/Ganeti/HTools/Container.hs \
-	htest/Test/Ganeti/HTools/Graph.hs \
-	htest/Test/Ganeti/HTools/Instance.hs \
-	htest/Test/Ganeti/HTools/Loader.hs \
-	htest/Test/Ganeti/HTools/Node.hs \
-	htest/Test/Ganeti/HTools/PeerMap.hs \
-	htest/Test/Ganeti/HTools/Types.hs \
-	htest/Test/Ganeti/JSON.hs \
-	htest/Test/Ganeti/Jobs.hs \
-	htest/Test/Ganeti/JQueue.hs \
-	htest/Test/Ganeti/Luxi.hs \
-	htest/Test/Ganeti/Network.hs \
-	htest/Test/Ganeti/Objects.hs \
-	htest/Test/Ganeti/OpCodes.hs \
-	htest/Test/Ganeti/Query/Filter.hs \
-	htest/Test/Ganeti/Query/Language.hs \
-	htest/Test/Ganeti/Query/Query.hs \
-	htest/Test/Ganeti/Rpc.hs \
-	htest/Test/Ganeti/Ssconf.hs \
-	htest/Test/Ganeti/THH.hs \
-	htest/Test/Ganeti/TestCommon.hs \
-	htest/Test/Ganeti/TestHTools.hs \
-	htest/Test/Ganeti/TestHelper.hs \
-	htest/Test/Ganeti/Types.hs \
-	htest/Test/Ganeti/Utils.hs
+	test/hs/Test/Ganeti/Attoparsec.hs \
+	test/hs/Test/Ganeti/BasicTypes.hs \
+	test/hs/Test/Ganeti/Block/Drbd/Parser.hs \
+	test/hs/Test/Ganeti/Block/Drbd/Types.hs \
+	test/hs/Test/Ganeti/Common.hs \
+	test/hs/Test/Ganeti/Confd/Types.hs \
+	test/hs/Test/Ganeti/Confd/Utils.hs \
+	test/hs/Test/Ganeti/Daemon.hs \
+	test/hs/Test/Ganeti/Errors.hs \
+	test/hs/Test/Ganeti/HTools/Backend/Simu.hs \
+	test/hs/Test/Ganeti/HTools/Backend/Text.hs \
+	test/hs/Test/Ganeti/HTools/CLI.hs \
+	test/hs/Test/Ganeti/HTools/Cluster.hs \
+	test/hs/Test/Ganeti/HTools/Container.hs \
+	test/hs/Test/Ganeti/HTools/Graph.hs \
+	test/hs/Test/Ganeti/HTools/Instance.hs \
+	test/hs/Test/Ganeti/HTools/Loader.hs \
+	test/hs/Test/Ganeti/HTools/Node.hs \
+	test/hs/Test/Ganeti/HTools/PeerMap.hs \
+	test/hs/Test/Ganeti/HTools/Types.hs \
+	test/hs/Test/Ganeti/JSON.hs \
+	test/hs/Test/Ganeti/Jobs.hs \
+	test/hs/Test/Ganeti/JQueue.hs \
+	test/hs/Test/Ganeti/Luxi.hs \
+	test/hs/Test/Ganeti/Network.hs \
+	test/hs/Test/Ganeti/Objects.hs \
+	test/hs/Test/Ganeti/OpCodes.hs \
+	test/hs/Test/Ganeti/Query/Filter.hs \
+	test/hs/Test/Ganeti/Query/Language.hs \
+	test/hs/Test/Ganeti/Query/Query.hs \
+	test/hs/Test/Ganeti/Rpc.hs \
+	test/hs/Test/Ganeti/Ssconf.hs \
+	test/hs/Test/Ganeti/THH.hs \
+	test/hs/Test/Ganeti/TestCommon.hs \
+	test/hs/Test/Ganeti/TestHTools.hs \
+	test/hs/Test/Ganeti/TestHelper.hs \
+	test/hs/Test/Ganeti/Types.hs \
+	test/hs/Test/Ganeti/Utils.hs
 
 HS_LIBTEST_SRCS = $(HS_LIB_SRCS) $(HS_TEST_SRCS)
 
 HS_BUILT_SRCS = \
-	htest/Test/Ganeti/TestImports.hs \
+	test/hs/Test/Ganeti/TestImports.hs \
 	src/Ganeti/Constants.hs \
 	src/Ganeti/Version.hs
 HS_BUILT_SRCS_IN = $(patsubst %,%.in,$(HS_BUILT_SRCS))
@@ -684,17 +684,17 @@ $(HS_ALL_PROGS): %: %.hs $(HS_LIBTEST_SRCS) $(HS_BUILT_SRCS) Makefile
 	  $(HEXTRA) $(HEXTRA_INT) $@
 	@touch "$@"
 
-# for the htest/test binary, we need to enable profiling/coverage
-htest/test: HEXTRA_INT=-fhpc -ihtest
+# for the test/hs/test binary, we need to enable profiling/coverage
+test/hs/test: HEXTRA_INT=-fhpc -itest/hs
 
 # we compile the hpc-htools binary with the program coverage
-htest/hpc-htools: HEXTRA_INT=-fhpc
+test/hs/hpc-htools: HEXTRA_INT=-fhpc
 
 # we compile the hpc-mon-collector binary with the program coverage
-htest/hpc-mon-collector: HEXTRA_INT=-fhpc
+test/hs/hpc-mon-collector: HEXTRA_INT=-fhpc
 
 # test dependency
-htest/offline-tests.sh: htest/hpc-htools htest/hpc-mon-collector
+test/hs/offline-tests.sh: test/hs/hpc-htools test/hs/hpc-mon-collector
 
 # rules for building profiling-enabled versions of the haskell
 # programs: hs-prof does the full two-step build, whereas
@@ -828,8 +828,8 @@ EXTRA_DIST = \
 	$(HS_LIBTEST_SRCS) $(HS_BUILT_SRCS_IN) \
 	$(HS_PROG_SRCS) \
 	src/lint-hints.hs \
-	htest/cli-tests-defs.sh \
-	htest/offline-test.sh \
+	test/hs/cli-tests-defs.sh \
+	test/hs/offline-test.sh \
 	.ghci
 
 man_MANS = \
@@ -872,36 +872,36 @@ maninput = \
 	man/footer.man man/footer.html $(mangen)
 
 TEST_FILES = \
-	htest/data/clean-nonzero-score.data \
-	htest/data/common-suffix.data \
-	htest/data/empty-cluster.data \
-	htest/data/hail-alloc-drbd.json \
-	htest/data/hail-change-group.json \
-	htest/data/hail-invalid-reloc.json \
-	htest/data/hail-node-evac.json \
-	htest/data/hail-reloc-drbd.json \
-	htest/data/hbal-excl-tags.data \
-	htest/data/hbal-split-insts.data \
-	htest/data/invalid-node.data \
-	htest/data/missing-resources.data \
-	htest/data/n1-failure.data \
-	htest/data/rapi/groups.json \
-	htest/data/rapi/info.json \
-	htest/data/rapi/instances.json \
-	htest/data/rapi/nodes.json \
-	htest/shelltests/htools-balancing.test \
-	htest/shelltests/htools-basic.test \
-	htest/shelltests/htools-dynutil.test \
-	htest/shelltests/htools-excl.test \
-	htest/shelltests/htools-hail.test \
-	htest/shelltests/htools-hspace.test \
-	htest/shelltests/htools-invalid.test \
-	htest/shelltests/htools-multi-group.test \
-	htest/shelltests/htools-no-backend.test \
-	htest/shelltests/htools-rapi.test \
-	htest/shelltests/htools-single-group.test \
-	htest/shelltests/htools-text-backend.test \
-	htest/shelltests/htools-mon-collector.test \
+	test/data/htools/clean-nonzero-score.data \
+	test/data/htools/common-suffix.data \
+	test/data/htools/empty-cluster.data \
+	test/data/htools/hail-alloc-drbd.json \
+	test/data/htools/hail-change-group.json \
+	test/data/htools/hail-invalid-reloc.json \
+	test/data/htools/hail-node-evac.json \
+	test/data/htools/hail-reloc-drbd.json \
+	test/data/htools/hbal-excl-tags.data \
+	test/data/htools/hbal-split-insts.data \
+	test/data/htools/invalid-node.data \
+	test/data/htools/missing-resources.data \
+	test/data/htools/n1-failure.data \
+	test/data/htools/rapi/groups.json \
+	test/data/htools/rapi/info.json \
+	test/data/htools/rapi/instances.json \
+	test/data/htools/rapi/nodes.json \
+	test/hs/shelltests/htools-balancing.test \
+	test/hs/shelltests/htools-basic.test \
+	test/hs/shelltests/htools-dynutil.test \
+	test/hs/shelltests/htools-excl.test \
+	test/hs/shelltests/htools-hail.test \
+	test/hs/shelltests/htools-hspace.test \
+	test/hs/shelltests/htools-invalid.test \
+	test/hs/shelltests/htools-multi-group.test \
+	test/hs/shelltests/htools-no-backend.test \
+	test/hs/shelltests/htools-rapi.test \
+	test/hs/shelltests/htools-single-group.test \
+	test/hs/shelltests/htools-text-backend.test \
+	test/hs/shelltests/htools-mon-collector.test \
 	test/data/bdev-drbd-8.0.txt \
 	test/data/bdev-drbd-8.3.txt \
 	test/data/bdev-drbd-disk.txt \
@@ -1040,7 +1040,7 @@ python_tests = \
 	test/py/qa.qa_config_unittest.py \
 	test/py/tempfile_fork_unittest.py
 
-haskell_tests = htest/test
+haskell_tests = test/hs/test
 
 dist_TESTS = \
 	test/py/check-cert-expired_unittest.bash \
@@ -1056,10 +1056,10 @@ check_SCRIPTS =
 
 if WANT_HSTESTS
 nodist_TESTS += $(haskell_tests)
-dist_TESTS += htest/offline-test.sh
+dist_TESTS += test/hs/offline-test.sh
 check_SCRIPTS += \
-	htest/hpc-htools \
-	htest/hpc-mon-collector \
+	test/hs/hpc-htools \
+	test/hs/hpc-mon-collector \
 	$(HS_BUILT_TEST_HELPERS)
 endif
 
@@ -1106,8 +1106,8 @@ srclink_files = \
 	test/py/import-export_unittest.bash \
 	test/py/cli-test.bash \
 	test/py/bash_completion.bash \
-	htest/offline-test.sh \
-	htest/cli-tests-defs.sh \
+	test/hs/offline-test.sh \
+	test/hs/cli-tests-defs.sh \
 	$(all_python_code) \
 	$(HS_LIBTEST_SRCS) $(HS_PROG_SRCS)
 
@@ -1269,7 +1269,7 @@ src/Ganeti/Constants.hs: src/Ganeti/Constants.hs.in \
 	  PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(CONVERT_CONSTANTS); \
 	} > $@
 
-htest/Test/Ganeti/TestImports.hs: htest/Test/Ganeti/TestImports.hs.in \
+test/hs/Test/Ganeti/TestImports.hs: test/hs/Test/Ganeti/TestImports.hs.in \
 	$(built_base_sources)
 	set -e; \
 	{ cat $< ; \
@@ -1433,7 +1433,7 @@ tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
 tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
 tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
 tools/node-cleanup: MODULE = ganeti.tools.node_cleanup
-$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst htest/%,%,$@)
+$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@)
 
 $(PYTHON_BOOTSTRAP): Makefile | stamp-directories
 	test -n "$(MODULE)" || { echo Missing module; exit 1; }
@@ -1469,7 +1469,7 @@ $(HS_BUILT_TEST_HELPERS): Makefile
 	  echo '# This file is automatically generated, do not edit!'; \
 	  echo "# Edit Makefile.am instead."; \
 	  echo; \
-	  echo "HTOOLS=$(TESTROLE) exec ./htest/hpc-htools \"\$$@\""; \
+	  echo "HTOOLS=$(TESTROLE) exec ./test/hs/hpc-htools \"\$$@\""; \
 	} > $@
 	chmod u+x $@
 
@@ -1552,11 +1552,11 @@ check-local: check-dirs $(GENERATED_FILES)
 	test -z "$$error"
 
 .PHONY: hs-check
-hs-check: htest/test htest/hpc-htools htest/hpc-mon-collector $(HS_BUILT_TEST_HELPERS) \
+hs-check: test/hs/test test/hs/hpc-htools test/hs/hpc-mon-collector $(HS_BUILT_TEST_HELPERS) \
 	| $(BUILT_PYTHON_SOURCES)
 	@rm -f *.tix
-	./htest/test
-	HBINARY="./htest/hpc-htools" ./htest/offline-test.sh
+	./test/hs/test
+	HBINARY="./test/hs/hpc-htools" ./test/hs/offline-test.sh
 
 # E111: indentation is not a multiple of four
 # E121: continuation line indentation is not a multiple of four
@@ -1606,7 +1606,7 @@ pep8: $(GENERATED_FILES)
 	  --repeat $(pep8_python_code)
 
 # FIXME: remove ignore "Use void" when GHC 6.x is deprecated
-HLINT_EXCLUDES = src/Ganeti/THH.hs htest/hpc-htools.hs
+HLINT_EXCLUDES = src/Ganeti/THH.hs test/hs/hpc-htools.hs
 .PHONY: hlint
 hlint: $(HS_BUILT_SRCS) src/lint-hints.hs
 	@test -n "$(HLINT)" || { echo 'hlint' not found during configure; exit 1; }
@@ -1768,7 +1768,7 @@ py-coverage: $(GENERATED_FILES) $(python_tests)
 	$(python_tests)
 
 .PHONY: hs-coverage
-hs-coverage: $(haskell_tests) htest/hpc-htools htest/hpc-mon-collector
+hs-coverage: $(haskell_tests) test/hs/hpc-htools test/hs/hpc-mon-collector
 	rm -f *.tix
 	$(MAKE) $(AM_MAKEFLAGS) hs-check
 	@mkdir_p@ $(COVERAGE_HS_DIR)
diff --git a/autotools/run-in-tempdir b/autotools/run-in-tempdir
index 86f643944bdd05228e7ff1bb3cb74700c45aee60..12897831204b9090d9ac4152e722e2ff44a5a6af 100755
--- a/autotools/run-in-tempdir
+++ b/autotools/run-in-tempdir
@@ -8,26 +8,25 @@ set -e
 tmpdir=$(mktemp -d -t gntbuild.XXXXXXXX)
 trap "rm -rf $tmpdir" EXIT
 
-mkdir $tmpdir/doc
+# fully copy items
+cp -r autotools daemons scripts lib tools qa $tmpdir
 
-cp -r autotools daemons scripts lib tools test qa $tmpdir
+mkdir $tmpdir/doc
 ln -s $PWD/doc/examples $tmpdir/doc
 
+mkdir $tmpdir/test/
+cp -r test/py $tmpdir/test/py
+ln -s $PWD/test/data $tmpdir/test
+ln -s $PWD/test/hs $tmpdir/test
+
 mv $tmpdir/lib $tmpdir/ganeti
 ln -T -s $tmpdir/ganeti $tmpdir/lib
 
-mkdir -p $tmpdir/src $tmpdir/htest
+mkdir -p $tmpdir/src $tmpdir/test/hs
 for hfile in htools ganeti-confd mon-collector; do
   if [ -e src/$hfile ]; then
     ln -s $PWD/src/$hfile $tmpdir/src/
   fi
 done
 
-for hfile in hpc-htools test offline-test.sh cli-tests-defs.sh \
-  hbal hscan hspace hinfo hcheck hail hroller hpc-mon-collector; do
-  if [ -e htest/$hfile ]; then
-    ln -s $PWD/htest/$hfile $tmpdir/htest/
-  fi
-done
-
 cd $tmpdir && GANETI_TEMP_DIR="$tmpdir" "$@"
diff --git a/doc/devnotes.rst b/doc/devnotes.rst
index 77112dd942d503d12e69aab6a2492d86778f5b8f..81823c36db2af31aeabf766c44b44617135f4dc9 100644
--- a/doc/devnotes.rst
+++ b/doc/devnotes.rst
@@ -180,7 +180,7 @@ For Python tests::
 
 For Haskell tests::
 
-  $ make htest/test && ./htest/test -t %pattern%
+  $ make test/hs/test && ./test/hs/test -t %pattern%
 
 Where ``pattern`` can be a simple test pattern (e.g. ``comma``,
 matching any test whose name contains ``comma``), a test pattern
diff --git a/htest/hpc-htools.hs b/htest/hpc-htools.hs
deleted file mode 120000
index edd10816f5adf396f9334379feb160d62f8866cd..0000000000000000000000000000000000000000
--- a/htest/hpc-htools.hs
+++ /dev/null
@@ -1 +0,0 @@
-../src/htools.hs
\ No newline at end of file
diff --git a/htest/hpc-mon-collector.hs b/htest/hpc-mon-collector.hs
deleted file mode 120000
index 1a34a1a26a1dc328c428a301541c13be048069a3..0000000000000000000000000000000000000000
--- a/htest/hpc-mon-collector.hs
+++ /dev/null
@@ -1 +0,0 @@
-../src/mon-collector.hs
\ No newline at end of file
diff --git a/htest/shelltests/htools-basic.test b/htest/shelltests/htools-basic.test
deleted file mode 100644
index 9b45b31bccf362ecc4d543f282769a155804fd71..0000000000000000000000000000000000000000
--- a/htest/shelltests/htools-basic.test
+++ /dev/null
@@ -1,43 +0,0 @@
-# help/version tests
-./htest/hail --version
->>>= 0
-./htest/hail --help
->>>= 0
-./htest/hail --help-completion
->>>= 0
-./htest/hbal --version
->>>= 0
-./htest/hbal --help
->>>= 0
-./htest/hbal --help-completion
->>>= 0
-./htest/hspace --version
->>>= 0
-./htest/hspace --help
->>>= 0
-./htest/hspace --help-completion
->>>= 0
-./htest/hscan --version
->>>= 0
-./htest/hscan --help
->>>= 0
-./htest/hscan --help-completion
->>>= 0
-./htest/hinfo --version
->>>= 0
-./htest/hinfo --help
->>>= 0
-./htest/hinfo --help-completion
->>>= 0
-./htest/hcheck --version
->>>= 0
-./htest/hcheck --help
->>>= 0
-./htest/hcheck --help-completion
->>>= 0
-./htest/hroller --version
->>>= 0
-./htest/hroller --help
->>>= 0
-./htest/hroller --help-completion
->>>= 0
diff --git a/htest/shelltests/htools-dynutil.test b/htest/shelltests/htools-dynutil.test
deleted file mode 100644
index ed01a790717d5a7a5ab76786cd868b1f78b76a67..0000000000000000000000000000000000000000
--- a/htest/shelltests/htools-dynutil.test
+++ /dev/null
@@ -1,19 +0,0 @@
-echo a > $T/dynu; ./htest/hbal -U $T/dynu $BACKEND_DYNU
->>>2 /Cannot parse line/
->>>= !0
-
-echo a b c d e f g h > $T/dynu; ./htest/hbal -U $T/dynu $BACKEND_DYNU
->>>2 /Cannot parse line/
->>>= !0
-
-echo inst cpu mem dsk net >$T/dynu; ./htest/hbal -U $T/dynu $BACKEND_DYNU
->>>2 /cannot parse string '(cpu|mem|dsk|net)'/
->>>= !0
-
-# unknown instances are currently just ignored
-echo no-such-inst 2 2 2 2 > $T/dynu; ./htest/hbal -U $T/dynu $BACKEND_DYNU
->>>= 0
-
-# new-0 is the name of the first instance allocated by hspace
-echo new-0 2 2 2 2 > $T/dynu; ./htest/hbal -U $T/dynu $BACKEND_DYNU
->>>= 0
diff --git a/htest/shelltests/htools-excl.test b/htest/shelltests/htools-excl.test
deleted file mode 100644
index 22b12c9affadb0a0ee2da292aaa7ec53c6a189e9..0000000000000000000000000000000000000000
--- a/htest/shelltests/htools-excl.test
+++ /dev/null
@@ -1,15 +0,0 @@
-./htest/hbal $BACKEND_EXCL --exclude-instances no-such-instance
->>>2 /Unknown instance/
->>>= !0
-
-./htest/hbal $BACKEND_EXCL --select-instances no-such-instances
->>>2 /Unknown instance/
->>>= !0
-
-./htest/hbal $BACKEND_EXCL --exclude-instances new-0 --select-instances new-1
->>>= 0
-
-# Test exclusion tags too (both from the command line and cluster tags).
-./htest/hbal -t $TESTDATA_DIR/hbal-excl-tags.data --exclusion-tags test
->>> /Cluster score improved/
->>>= 0
diff --git a/htest/shelltests/htools-hspace.test b/htest/shelltests/htools-hspace.test
deleted file mode 100644
index cee8c132e60b5396926374ce8aa3d8a22ae35c4f..0000000000000000000000000000000000000000
--- a/htest/shelltests/htools-hspace.test
+++ /dev/null
@@ -1,8 +0,0 @@
-# test that hspace machine readable output looks correct
-./htest/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8
->>> /^HTS_OK=1/
->>>= 0
-
-# test again via a file and shell parsing
-./htest/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8 > $T/capacity && sh -c ". $T/capacity && test x\$HTS_OK = x1"
->>>= 0
diff --git a/htest/shelltests/htools-single-group.test b/htest/shelltests/htools-single-group.test
deleted file mode 100644
index 38281827ab24d1b7dbb09b95559280d2dd0bce80..0000000000000000000000000000000000000000
--- a/htest/shelltests/htools-single-group.test
+++ /dev/null
@@ -1,37 +0,0 @@
-# standard single-group tests
-./htest/hinfo -v -v -p --print-instances -t$T/simu-onegroup.standard
->>>= 0
-
-./htest/hbal  -v -v -p --print-instances -t$T/simu-onegroup.standard
->>>= 0
-
-# hbal should not be able to balance
-./htest/hbal -t$T/simu-onegroup.standard
->>> /(Nothing to do, exiting|No solution found)/
->>>= 0
-
-
-# tiered single-group tests
-./htest/hinfo -v -v -p --print-instances -t$T/simu-onegroup.tiered
->>>= 0
-
-./htest/hbal  -v -v -p --print-instances -t$T/simu-onegroup.tiered
->>>= 0
-
-# hbal should not be able to balance
-./htest/hbal -t$T/simu-onegroup.tiered
->>> /(Nothing to do, exiting|No solution found)/
->>>= 0
-
-# hcheck should not find reason to rebalance
-./htest/hcheck -t$T/simu-onegroup.tiered --machine-readable
->>> /HCHECK_INIT_CLUSTER_NEED_REBALANCE=0/
->>>= 0
-
-# hroller should be able to print the solution
-./htest/hroller -t$T/simu-onegroup.tiered
->>>= 0
-
-# hroller should be able to print the solution, in verbose mode as well
-./htest/hroller -t$T/simu-onegroup.tiered -v -v
->>>= 0
diff --git a/htest/data/clean-nonzero-score.data b/test/data/htools/clean-nonzero-score.data
similarity index 100%
rename from htest/data/clean-nonzero-score.data
rename to test/data/htools/clean-nonzero-score.data
diff --git a/htest/data/common-suffix.data b/test/data/htools/common-suffix.data
similarity index 100%
rename from htest/data/common-suffix.data
rename to test/data/htools/common-suffix.data
diff --git a/htest/data/empty-cluster.data b/test/data/htools/empty-cluster.data
similarity index 100%
rename from htest/data/empty-cluster.data
rename to test/data/htools/empty-cluster.data
diff --git a/htest/data/hail-alloc-drbd.json b/test/data/htools/hail-alloc-drbd.json
similarity index 100%
rename from htest/data/hail-alloc-drbd.json
rename to test/data/htools/hail-alloc-drbd.json
diff --git a/htest/data/hail-change-group.json b/test/data/htools/hail-change-group.json
similarity index 100%
rename from htest/data/hail-change-group.json
rename to test/data/htools/hail-change-group.json
diff --git a/htest/data/hail-invalid-reloc.json b/test/data/htools/hail-invalid-reloc.json
similarity index 100%
rename from htest/data/hail-invalid-reloc.json
rename to test/data/htools/hail-invalid-reloc.json
diff --git a/htest/data/hail-node-evac.json b/test/data/htools/hail-node-evac.json
similarity index 100%
rename from htest/data/hail-node-evac.json
rename to test/data/htools/hail-node-evac.json
diff --git a/htest/data/hail-reloc-drbd.json b/test/data/htools/hail-reloc-drbd.json
similarity index 100%
rename from htest/data/hail-reloc-drbd.json
rename to test/data/htools/hail-reloc-drbd.json
diff --git a/htest/data/hbal-excl-tags.data b/test/data/htools/hbal-excl-tags.data
similarity index 100%
rename from htest/data/hbal-excl-tags.data
rename to test/data/htools/hbal-excl-tags.data
diff --git a/htest/data/hbal-split-insts.data b/test/data/htools/hbal-split-insts.data
similarity index 100%
rename from htest/data/hbal-split-insts.data
rename to test/data/htools/hbal-split-insts.data
diff --git a/htest/data/invalid-node.data b/test/data/htools/invalid-node.data
similarity index 100%
rename from htest/data/invalid-node.data
rename to test/data/htools/invalid-node.data
diff --git a/htest/data/missing-resources.data b/test/data/htools/missing-resources.data
similarity index 100%
rename from htest/data/missing-resources.data
rename to test/data/htools/missing-resources.data
diff --git a/htest/data/n1-failure.data b/test/data/htools/n1-failure.data
similarity index 100%
rename from htest/data/n1-failure.data
rename to test/data/htools/n1-failure.data
diff --git a/htest/data/rapi/groups.json b/test/data/htools/rapi/groups.json
similarity index 100%
rename from htest/data/rapi/groups.json
rename to test/data/htools/rapi/groups.json
diff --git a/htest/data/rapi/info.json b/test/data/htools/rapi/info.json
similarity index 100%
rename from htest/data/rapi/info.json
rename to test/data/htools/rapi/info.json
diff --git a/htest/data/rapi/instances.json b/test/data/htools/rapi/instances.json
similarity index 100%
rename from htest/data/rapi/instances.json
rename to test/data/htools/rapi/instances.json
diff --git a/htest/data/rapi/nodes.json b/test/data/htools/rapi/nodes.json
similarity index 100%
rename from htest/data/rapi/nodes.json
rename to test/data/htools/rapi/nodes.json
diff --git a/htest/Test/Ganeti/Attoparsec.hs b/test/hs/Test/Ganeti/Attoparsec.hs
similarity index 100%
rename from htest/Test/Ganeti/Attoparsec.hs
rename to test/hs/Test/Ganeti/Attoparsec.hs
diff --git a/htest/Test/Ganeti/BasicTypes.hs b/test/hs/Test/Ganeti/BasicTypes.hs
similarity index 100%
rename from htest/Test/Ganeti/BasicTypes.hs
rename to test/hs/Test/Ganeti/BasicTypes.hs
diff --git a/htest/Test/Ganeti/Block/Drbd/Parser.hs b/test/hs/Test/Ganeti/Block/Drbd/Parser.hs
similarity index 100%
rename from htest/Test/Ganeti/Block/Drbd/Parser.hs
rename to test/hs/Test/Ganeti/Block/Drbd/Parser.hs
diff --git a/htest/Test/Ganeti/Block/Drbd/Types.hs b/test/hs/Test/Ganeti/Block/Drbd/Types.hs
similarity index 100%
rename from htest/Test/Ganeti/Block/Drbd/Types.hs
rename to test/hs/Test/Ganeti/Block/Drbd/Types.hs
diff --git a/htest/Test/Ganeti/Common.hs b/test/hs/Test/Ganeti/Common.hs
similarity index 100%
rename from htest/Test/Ganeti/Common.hs
rename to test/hs/Test/Ganeti/Common.hs
diff --git a/htest/Test/Ganeti/Confd/Types.hs b/test/hs/Test/Ganeti/Confd/Types.hs
similarity index 100%
rename from htest/Test/Ganeti/Confd/Types.hs
rename to test/hs/Test/Ganeti/Confd/Types.hs
diff --git a/htest/Test/Ganeti/Confd/Utils.hs b/test/hs/Test/Ganeti/Confd/Utils.hs
similarity index 100%
rename from htest/Test/Ganeti/Confd/Utils.hs
rename to test/hs/Test/Ganeti/Confd/Utils.hs
diff --git a/htest/Test/Ganeti/Daemon.hs b/test/hs/Test/Ganeti/Daemon.hs
similarity index 100%
rename from htest/Test/Ganeti/Daemon.hs
rename to test/hs/Test/Ganeti/Daemon.hs
diff --git a/htest/Test/Ganeti/Errors.hs b/test/hs/Test/Ganeti/Errors.hs
similarity index 100%
rename from htest/Test/Ganeti/Errors.hs
rename to test/hs/Test/Ganeti/Errors.hs
diff --git a/htest/Test/Ganeti/HTools/Backend/Simu.hs b/test/hs/Test/Ganeti/HTools/Backend/Simu.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Backend/Simu.hs
rename to test/hs/Test/Ganeti/HTools/Backend/Simu.hs
diff --git a/htest/Test/Ganeti/HTools/Backend/Text.hs b/test/hs/Test/Ganeti/HTools/Backend/Text.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Backend/Text.hs
rename to test/hs/Test/Ganeti/HTools/Backend/Text.hs
diff --git a/htest/Test/Ganeti/HTools/CLI.hs b/test/hs/Test/Ganeti/HTools/CLI.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/CLI.hs
rename to test/hs/Test/Ganeti/HTools/CLI.hs
diff --git a/htest/Test/Ganeti/HTools/Cluster.hs b/test/hs/Test/Ganeti/HTools/Cluster.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Cluster.hs
rename to test/hs/Test/Ganeti/HTools/Cluster.hs
diff --git a/htest/Test/Ganeti/HTools/Container.hs b/test/hs/Test/Ganeti/HTools/Container.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Container.hs
rename to test/hs/Test/Ganeti/HTools/Container.hs
diff --git a/htest/Test/Ganeti/HTools/Graph.hs b/test/hs/Test/Ganeti/HTools/Graph.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Graph.hs
rename to test/hs/Test/Ganeti/HTools/Graph.hs
diff --git a/htest/Test/Ganeti/HTools/Instance.hs b/test/hs/Test/Ganeti/HTools/Instance.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Instance.hs
rename to test/hs/Test/Ganeti/HTools/Instance.hs
diff --git a/htest/Test/Ganeti/HTools/Loader.hs b/test/hs/Test/Ganeti/HTools/Loader.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Loader.hs
rename to test/hs/Test/Ganeti/HTools/Loader.hs
diff --git a/htest/Test/Ganeti/HTools/Node.hs b/test/hs/Test/Ganeti/HTools/Node.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Node.hs
rename to test/hs/Test/Ganeti/HTools/Node.hs
diff --git a/htest/Test/Ganeti/HTools/PeerMap.hs b/test/hs/Test/Ganeti/HTools/PeerMap.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/PeerMap.hs
rename to test/hs/Test/Ganeti/HTools/PeerMap.hs
diff --git a/htest/Test/Ganeti/HTools/Types.hs b/test/hs/Test/Ganeti/HTools/Types.hs
similarity index 100%
rename from htest/Test/Ganeti/HTools/Types.hs
rename to test/hs/Test/Ganeti/HTools/Types.hs
diff --git a/htest/Test/Ganeti/JQueue.hs b/test/hs/Test/Ganeti/JQueue.hs
similarity index 100%
rename from htest/Test/Ganeti/JQueue.hs
rename to test/hs/Test/Ganeti/JQueue.hs
diff --git a/htest/Test/Ganeti/JSON.hs b/test/hs/Test/Ganeti/JSON.hs
similarity index 100%
rename from htest/Test/Ganeti/JSON.hs
rename to test/hs/Test/Ganeti/JSON.hs
diff --git a/htest/Test/Ganeti/Jobs.hs b/test/hs/Test/Ganeti/Jobs.hs
similarity index 100%
rename from htest/Test/Ganeti/Jobs.hs
rename to test/hs/Test/Ganeti/Jobs.hs
diff --git a/htest/Test/Ganeti/Luxi.hs b/test/hs/Test/Ganeti/Luxi.hs
similarity index 100%
rename from htest/Test/Ganeti/Luxi.hs
rename to test/hs/Test/Ganeti/Luxi.hs
diff --git a/htest/Test/Ganeti/Network.hs b/test/hs/Test/Ganeti/Network.hs
similarity index 100%
rename from htest/Test/Ganeti/Network.hs
rename to test/hs/Test/Ganeti/Network.hs
diff --git a/htest/Test/Ganeti/Objects.hs b/test/hs/Test/Ganeti/Objects.hs
similarity index 99%
rename from htest/Test/Ganeti/Objects.hs
rename to test/hs/Test/Ganeti/Objects.hs
index 9f6448233d7549a5ae38a44524e78f0c8aa5e1aa..3526902d32581f10bfc69c04d82f1bf282ff67ad 100644
--- a/htest/Test/Ganeti/Objects.hs
+++ b/test/hs/Test/Ganeti/Objects.hs
@@ -268,7 +268,7 @@ prop_Config_serialisation =
 -- | Custom HUnit test to check the correspondence between Haskell-generated
 -- networks and their Python decoded, validated and re-encoded version.
 -- For the technical background of this unit test, check the documentation
--- of "case_py_compat_types" of htest/Test/Ganeti/Opcodes.hs
+-- of "case_py_compat_types" of test/hs/Test/Ganeti/Opcodes.hs
 case_py_compat_networks :: HUnit.Assertion
 case_py_compat_networks = do
   let num_networks = 500::Int
diff --git a/htest/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs
similarity index 100%
rename from htest/Test/Ganeti/OpCodes.hs
rename to test/hs/Test/Ganeti/OpCodes.hs
diff --git a/htest/Test/Ganeti/Query/Filter.hs b/test/hs/Test/Ganeti/Query/Filter.hs
similarity index 100%
rename from htest/Test/Ganeti/Query/Filter.hs
rename to test/hs/Test/Ganeti/Query/Filter.hs
diff --git a/htest/Test/Ganeti/Query/Language.hs b/test/hs/Test/Ganeti/Query/Language.hs
similarity index 100%
rename from htest/Test/Ganeti/Query/Language.hs
rename to test/hs/Test/Ganeti/Query/Language.hs
diff --git a/htest/Test/Ganeti/Query/Query.hs b/test/hs/Test/Ganeti/Query/Query.hs
similarity index 100%
rename from htest/Test/Ganeti/Query/Query.hs
rename to test/hs/Test/Ganeti/Query/Query.hs
diff --git a/htest/Test/Ganeti/Rpc.hs b/test/hs/Test/Ganeti/Rpc.hs
similarity index 100%
rename from htest/Test/Ganeti/Rpc.hs
rename to test/hs/Test/Ganeti/Rpc.hs
diff --git a/htest/Test/Ganeti/Ssconf.hs b/test/hs/Test/Ganeti/Ssconf.hs
similarity index 100%
rename from htest/Test/Ganeti/Ssconf.hs
rename to test/hs/Test/Ganeti/Ssconf.hs
diff --git a/htest/Test/Ganeti/THH.hs b/test/hs/Test/Ganeti/THH.hs
similarity index 100%
rename from htest/Test/Ganeti/THH.hs
rename to test/hs/Test/Ganeti/THH.hs
diff --git a/htest/Test/Ganeti/TestCommon.hs b/test/hs/Test/Ganeti/TestCommon.hs
similarity index 99%
rename from htest/Test/Ganeti/TestCommon.hs
rename to test/hs/Test/Ganeti/TestCommon.hs
index a684a2b4de0218847e995259a586df8fc3925823..72ae311ac94408420a4735f4de876e5f251dacb7 100644
--- a/htest/Test/Ganeti/TestCommon.hs
+++ b/test/hs/Test/Ganeti/TestCommon.hs
@@ -298,5 +298,5 @@ readPythonTestData filename = do
 -- | Returns the content of the specified haskell test data file.
 readTestData :: String -> IO String
 readTestData filename = do
-    name <- testDataFilename "/htest/data/" filename
+    name <- testDataFilename "/test/data/htools/" filename
     readFile name
diff --git a/htest/Test/Ganeti/TestHTools.hs b/test/hs/Test/Ganeti/TestHTools.hs
similarity index 100%
rename from htest/Test/Ganeti/TestHTools.hs
rename to test/hs/Test/Ganeti/TestHTools.hs
diff --git a/htest/Test/Ganeti/TestHelper.hs b/test/hs/Test/Ganeti/TestHelper.hs
similarity index 100%
rename from htest/Test/Ganeti/TestHelper.hs
rename to test/hs/Test/Ganeti/TestHelper.hs
diff --git a/htest/Test/Ganeti/TestImports.hs.in b/test/hs/Test/Ganeti/TestImports.hs.in
similarity index 100%
rename from htest/Test/Ganeti/TestImports.hs.in
rename to test/hs/Test/Ganeti/TestImports.hs.in
diff --git a/htest/Test/Ganeti/Types.hs b/test/hs/Test/Ganeti/Types.hs
similarity index 100%
rename from htest/Test/Ganeti/Types.hs
rename to test/hs/Test/Ganeti/Types.hs
diff --git a/htest/Test/Ganeti/Utils.hs b/test/hs/Test/Ganeti/Utils.hs
similarity index 100%
rename from htest/Test/Ganeti/Utils.hs
rename to test/hs/Test/Ganeti/Utils.hs
diff --git a/htest/cli-tests-defs.sh b/test/hs/cli-tests-defs.sh
similarity index 92%
rename from htest/cli-tests-defs.sh
rename to test/hs/cli-tests-defs.sh
index 86dd64f1d19415815d256ef8ace2ab52b5d3ea1b..b33a756e76f82bc435dae60f20abc136801a5ad2 100644
--- a/htest/cli-tests-defs.sh
+++ b/test/hs/cli-tests-defs.sh
@@ -19,9 +19,9 @@
 
 # This is an shell testing configuration fragment.
 
-HBINARY=${HBINARY:-./htest/hpc-htools}
+HBINARY=${HBINARY:-./test/hs/hpc-htools}
 
-export TESTDATA_DIR=${TOP_SRCDIR:-.}/htest/data
+export TESTDATA_DIR=${TOP_SRCDIR:-.}/test/data/htools
 export PYTESTDATA_DIR=${TOP_SRCDIR:-.}/test/data
 
 hbal() {
diff --git a/test/hs/hpc-htools.hs b/test/hs/hpc-htools.hs
new file mode 120000
index 0000000000000000000000000000000000000000..27f95cd6e7a7d2287e7fe308f84a3722f63ab383
--- /dev/null
+++ b/test/hs/hpc-htools.hs
@@ -0,0 +1 @@
+../../src/htools.hs
\ No newline at end of file
diff --git a/test/hs/hpc-mon-collector.hs b/test/hs/hpc-mon-collector.hs
new file mode 120000
index 0000000000000000000000000000000000000000..2ae7dc5f240c03b45cd48ed952a6262509cb38e3
--- /dev/null
+++ b/test/hs/hpc-mon-collector.hs
@@ -0,0 +1 @@
+../../src/mon-collector.hs
\ No newline at end of file
diff --git a/htest/live-test.sh b/test/hs/live-test.sh
similarity index 100%
rename from htest/live-test.sh
rename to test/hs/live-test.sh
diff --git a/htest/offline-test.sh b/test/hs/offline-test.sh
similarity index 85%
rename from htest/offline-test.sh
rename to test/hs/offline-test.sh
index 54eb81c80b135212405beaf3fb3a38af40d90238..cbcba41e0d1905cf39562fe786b9cac4f42683a6 100755
--- a/htest/offline-test.sh
+++ b/test/hs/offline-test.sh
@@ -34,12 +34,12 @@ echo Using $T as temporary dir
 
 echo -n Generating hspace simulation data for hinfo and hbal...
 # this cluster spec should be fine
-./htest/hspace --simu p,4,8T,64g,16 -S $T/simu-onegroup \
+./test/hs/hspace --simu p,4,8T,64g,16 -S $T/simu-onegroup \
   --disk-template drbd -l 8 -v -v -v >/dev/null 2>&1
 echo OK
 
 echo -n Generating hinfo and hbal test files for multi-group...
-./htest/hspace --simu p,4,8T,64g,16 --simu p,4,8T,64g,16 \
+./test/hs/hspace --simu p,4,8T,64g,16 --simu p,4,8T,64g,16 \
   -S $T/simu-twogroups --disk-template drbd -l 8 >/dev/null 2>&1
 echo OK
 
@@ -48,7 +48,7 @@ echo -n Generating test files for rebalancing...
 # policy, then we change all nodes from this group to the allocable
 # one, and we check for rebalancing
 FROOT="$T/simu-rebal-orig"
-./htest/hspace --simu u,4,8T,64g,16 --simu p,4,8T,64g,16 \
+./test/hs/hspace --simu u,4,8T,64g,16 --simu p,4,8T,64g,16 \
   -S $FROOT --disk-template drbd -l 8 >/dev/null 2>&1
 for suffix in standard tiered; do
   RELOC="$T/simu-rebal-merged.$suffix"
@@ -75,19 +75,19 @@ echo OK
 echo -n Checking file-based RAPI...
 mkdir -p $T/hscan
 export RAPI_URL="file://$TESTDATA_DIR/rapi"
-./htest/hscan -d $T/hscan/ -p -v -v $RAPI_URL >/dev/null 2>&1
+./test/hs/hscan -d $T/hscan/ -p -v -v $RAPI_URL >/dev/null 2>&1
 # check that we file parsing is correct, i.e. hscan saves correct text
 # files, and is idempotent (rapi+text == rapi); more is tested in
 # shelltest later
 RAPI_TXT="$(ls $T/hscan/*.data|head -n1)"
-./htest/hinfo -p --print-instances -m $RAPI_URL > $T/hscan/direct.hinfo 2>&1
-./htest/hinfo -p --print-instances -t $RAPI_TXT > $T/hscan/fromtext.hinfo 2>&1
+./test/hs/hinfo -p --print-instances -m $RAPI_URL > $T/hscan/direct.hinfo 2>&1
+./test/hs/hinfo -p --print-instances -t $RAPI_TXT > $T/hscan/fromtext.hinfo 2>&1
 echo OK
 
 echo Running shelltest...
 
 shelltest $SHELLTESTARGS \
-  ${TOP_SRCDIR:-.}/htest/shelltests/htools-*.test \
+  ${TOP_SRCDIR:-.}/test/hs/shelltests/htools-*.test \
   -- --hide-successes
 
 echo All OK
diff --git a/htest/shelltests/htools-balancing.test b/test/hs/shelltests/htools-balancing.test
similarity index 66%
rename from htest/shelltests/htools-balancing.test
rename to test/hs/shelltests/htools-balancing.test
index 6c54d554065863d04c99b1b14798d68c3551158e..bd3ef57e6a868cf9b267dd2857255f154d3bcff9 100644
--- a/htest/shelltests/htools-balancing.test
+++ b/test/hs/shelltests/htools-balancing.test
@@ -1,32 +1,32 @@
 ### std tests
 
 # test basic parsing
-./htest/hinfo -v -v -p --print-instances $BACKEND_BAL_STD
+./test/hs/hinfo -v -v -p --print-instances $BACKEND_BAL_STD
 >>>= 0
-./htest/hbal -v -v -v -p --print-instances $BACKEND_BAL_STD -G group-01
+./test/hs/hbal -v -v -v -p --print-instances $BACKEND_BAL_STD -G group-01
 >>> !/(Nothing to do, exiting|No solution found)/
 >>>2 !/(Nothing to do, exiting|No solution found)/
 >>>= 0
 
 # test command output
-./htest/hbal $BACKEND_BAL_STD -G group-01 -C -S $T/simu-rebal.standard
+./test/hs/hbal $BACKEND_BAL_STD -G group-01 -C -S $T/simu-rebal.standard
 >>> /gnt-instance (failover|migrate|replace-disks)/
 >>>= 0
 
 # test that hbal won't execute rebalances when using the text backend
-./htest/hbal $BACKEND_BAL_STD -G group-01 -X
+./test/hs/hbal $BACKEND_BAL_STD -G group-01 -X
 >>>2
 Error: hbal: Execution of commands possible only on LUXI
 >>>= !0
 
 # test that hbal won't execute any moves if we request an absurdly-high
 # minimum-improvement
-./htest/hbal $BACKEND_BAL_STD -G group-01 -C --min-gain 10000 --min-gain-limit 10000
+./test/hs/hbal $BACKEND_BAL_STD -G group-01 -C --min-gain 10000 --min-gain-limit 10000
 >>>/No solution found/
 >>>= 0
 
 # test saving commands
-./htest/hbal $BACKEND_BAL_STD -G group-01 -C$T/rebal-cmds.standard
+./test/hs/hbal $BACKEND_BAL_STD -G group-01 -C$T/rebal-cmds.standard
 >>>= 0
 # and now check the file (depends on previous test)
 cat $T/rebal-cmds.standard
@@ -40,32 +40,32 @@ diff -u $T/simu-rebal-merged.standard $T/simu-rebal.standard.original
 >>>= 0
 
 # no double rebalance; depends on previous test
-./htest/hbal -t $T/simu-rebal.standard.balanced -G group-01
+./test/hs/hbal -t $T/simu-rebal.standard.balanced -G group-01
 >>> /(Nothing to do, exiting|No solution found)/
 >>>= 0
 
 # hcheck sees no reason to rebalance after rebalancing was already done
-./htest/hcheck -t$T/simu-rebal.standard.balanced --machine-readable
+./test/hs/hcheck -t$T/simu-rebal.standard.balanced --machine-readable
 >>> /HCHECK_INIT_CLUSTER_NEED_REBALANCE=0/
 >>>= 0
 
 ### now tiered tests
 
 # test basic parsing
-./htest/hinfo -v -v -p --print-instances $BACKEND_BAL_TIER
+./test/hs/hinfo -v -v -p --print-instances $BACKEND_BAL_TIER
 >>>= 0
-./htest/hbal -v -v -v -p --print-instances $BACKEND_BAL_TIER -G group-01
+./test/hs/hbal -v -v -v -p --print-instances $BACKEND_BAL_TIER -G group-01
 >>> !/(Nothing to do, exiting|No solution found)/
 >>>2 !/(Nothing to do, exiting|No solution found)/
 >>>= 0
 
 # test command output
-./htest/hbal $BACKEND_BAL_TIER -G group-01 -C -S $T/simu-rebal.tiered
+./test/hs/hbal $BACKEND_BAL_TIER -G group-01 -C -S $T/simu-rebal.tiered
 >>> /gnt-instance (failover|migrate|replace-disks)/
 >>>= 0
 
 # test saving commands
-./htest/hbal $BACKEND_BAL_TIER -G group-01 -C$T/rebal-cmds.tiered
+./test/hs/hbal $BACKEND_BAL_TIER -G group-01 -C$T/rebal-cmds.tiered
 >>>= 0
 # and now check the file (depends on previous test)
 cat $T/rebal-cmds.tiered
@@ -79,45 +79,45 @@ diff -u $T/simu-rebal-merged.tiered $T/simu-rebal.tiered.original
 >>>= 0
 
 # no double rebalance; depends on previous test
-./htest/hbal -t $T/simu-rebal.tiered.balanced -G group-01
+./test/hs/hbal -t $T/simu-rebal.tiered.balanced -G group-01
 >>> /(Nothing to do, exiting|No solution found)/
 >>>= 0
 
 ### now some other custom tests
 
 # n+1 bad instances are reported as such
-./htest/hbal -t$TESTDATA_DIR/n1-failure.data -G group-01
+./test/hs/hbal -t$TESTDATA_DIR/n1-failure.data -G group-01
 >>>/Initial check done: 4 bad nodes, 8 bad instances./
 >>>=0
 
 # same test again, different message check (shelltest can't test multiple
 # messages via regexp
-./htest/hbal -t$TESTDATA_DIR/n1-failure.data -G group-01
+./test/hs/hbal -t$TESTDATA_DIR/n1-failure.data -G group-01
 >>>/Cluster is not N\+1 happy, continuing but no guarantee that the cluster will end N\+1 happy./
 >>>2
 >>>=0
 
 # and hcheck should report this as needs rebalancing
-./htest/hcheck -t$TESTDATA_DIR/n1-failure.data
+./test/hs/hcheck -t$TESTDATA_DIR/n1-failure.data
 >>>/Cluster needs rebalancing./
 >>>= 1
 
 # ... unless we request no-simulation mode
-./htest/hcheck -t$TESTDATA_DIR/n1-failure.data --no-simulation
+./test/hs/hcheck -t$TESTDATA_DIR/n1-failure.data --no-simulation
 >>>/Running in no-simulation mode./
 >>>= 0
 
 # and a clean cluster should be reported as such
-./htest/hcheck $BACKEND_BAL_STD
+./test/hs/hcheck $BACKEND_BAL_STD
 >>>/No need to rebalance cluster, no problems found./
 >>>= 0
 
 # ... and even one with non-zero score
-./htest/hcheck -t $TESTDATA_DIR/clean-nonzero-score.data
+./test/hs/hcheck -t $TESTDATA_DIR/clean-nonzero-score.data
 >>>/No need to rebalance cluster, no problems found./
 >>>= 0
 
 # hbal should work on empty groups as well
-./htest/hbal -t$TESTDATA_DIR/n1-failure.data -G group-02
+./test/hs/hbal -t$TESTDATA_DIR/n1-failure.data -G group-02
 >>>/Group size 0 nodes, 0 instances/
 >>>= 0
diff --git a/test/hs/shelltests/htools-basic.test b/test/hs/shelltests/htools-basic.test
new file mode 100644
index 0000000000000000000000000000000000000000..251d27dabca064c49b2ce1aa2c8a01f9605d2343
--- /dev/null
+++ b/test/hs/shelltests/htools-basic.test
@@ -0,0 +1,43 @@
+# help/version tests
+./test/hs/hail --version
+>>>= 0
+./test/hs/hail --help
+>>>= 0
+./test/hs/hail --help-completion
+>>>= 0
+./test/hs/hbal --version
+>>>= 0
+./test/hs/hbal --help
+>>>= 0
+./test/hs/hbal --help-completion
+>>>= 0
+./test/hs/hspace --version
+>>>= 0
+./test/hs/hspace --help
+>>>= 0
+./test/hs/hspace --help-completion
+>>>= 0
+./test/hs/hscan --version
+>>>= 0
+./test/hs/hscan --help
+>>>= 0
+./test/hs/hscan --help-completion
+>>>= 0
+./test/hs/hinfo --version
+>>>= 0
+./test/hs/hinfo --help
+>>>= 0
+./test/hs/hinfo --help-completion
+>>>= 0
+./test/hs/hcheck --version
+>>>= 0
+./test/hs/hcheck --help
+>>>= 0
+./test/hs/hcheck --help-completion
+>>>= 0
+./test/hs/hroller --version
+>>>= 0
+./test/hs/hroller --help
+>>>= 0
+./test/hs/hroller --help-completion
+>>>= 0
diff --git a/test/hs/shelltests/htools-dynutil.test b/test/hs/shelltests/htools-dynutil.test
new file mode 100644
index 0000000000000000000000000000000000000000..a4b60415aa2549dc06c2b3338ebdfc9daf89f7fe
--- /dev/null
+++ b/test/hs/shelltests/htools-dynutil.test
@@ -0,0 +1,19 @@
+echo a > $T/dynu; ./test/hs/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /Cannot parse line/
+>>>= !0
+
+echo a b c d e f g h > $T/dynu; ./test/hs/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /Cannot parse line/
+>>>= !0
+
+echo inst cpu mem dsk net >$T/dynu; ./test/hs/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /cannot parse string '(cpu|mem|dsk|net)'/
+>>>= !0
+
+# unknown instances are currently just ignored
+echo no-such-inst 2 2 2 2 > $T/dynu; ./test/hs/hbal -U $T/dynu $BACKEND_DYNU
+>>>= 0
+
+# new-0 is the name of the first instance allocated by hspace
+echo new-0 2 2 2 2 > $T/dynu; ./test/hs/hbal -U $T/dynu $BACKEND_DYNU
+>>>= 0
diff --git a/test/hs/shelltests/htools-excl.test b/test/hs/shelltests/htools-excl.test
new file mode 100644
index 0000000000000000000000000000000000000000..b22880c0f68fa04c640e41dff2f1f0d43a5137a3
--- /dev/null
+++ b/test/hs/shelltests/htools-excl.test
@@ -0,0 +1,15 @@
+./test/hs/hbal $BACKEND_EXCL --exclude-instances no-such-instance
+>>>2 /Unknown instance/
+>>>= !0
+
+./test/hs/hbal $BACKEND_EXCL --select-instances no-such-instances
+>>>2 /Unknown instance/
+>>>= !0
+
+./test/hs/hbal $BACKEND_EXCL --exclude-instances new-0 --select-instances new-1
+>>>= 0
+
+# Test exclusion tags too (both from the command line and cluster tags).
+./test/hs/hbal -t $TESTDATA_DIR/hbal-excl-tags.data --exclusion-tags test
+>>> /Cluster score improved/
+>>>= 0
diff --git a/htest/shelltests/htools-hail.test b/test/hs/shelltests/htools-hail.test
similarity index 57%
rename from htest/shelltests/htools-hail.test
rename to test/hs/shelltests/htools-hail.test
index 2f1c9f9c7304e9a907b6c1ab678146fe65d50f48..55dd3ca20a114ab28534406b7047b9c480dc7ca8 100644
--- a/htest/shelltests/htools-hail.test
+++ b/test/hs/shelltests/htools-hail.test
@@ -1,77 +1,77 @@
 # test that on invalid files it can't parse the request
-./htest/hail /dev/null
+./test/hs/hail /dev/null
 >>>2 /Invalid JSON/
 >>>= !0
 
 # another invalid example
-echo '[]' | ./htest/hail -
+echo '[]' | ./test/hs/hail -
 >>>2 /Unable to read JSObject/
 >>>= !0
 
 # empty dict
-echo '{}' | ./htest/hail -
+echo '{}' | ./test/hs/hail -
 >>>2 /key 'request' not found/
 >>>= !0
 
-echo '{"request": 0}' | ./htest/hail -
+echo '{"request": 0}' | ./test/hs/hail -
 >>>2 /key 'request'/
 >>>= !0
 
-./htest/hail $TESTDATA_DIR/hail-invalid-reloc.json
+./test/hs/hail $TESTDATA_DIR/hail-invalid-reloc.json
 >>>2 /key 'name': Unable to read String/
 >>>= !0
 
 # and now start the real tests
-./htest/hail $TESTDATA_DIR/hail-alloc-drbd.json
+./test/hs/hail $TESTDATA_DIR/hail-alloc-drbd.json
 >>> /"success":true,.*,"result":\["node2","node1"\]/
 >>>= 0
 
-./htest/hail $TESTDATA_DIR/hail-reloc-drbd.json
+./test/hs/hail $TESTDATA_DIR/hail-reloc-drbd.json
 >>> /"success":true,.*,"result":\["node1"\]/
 >>>= 0
 
-./htest/hail $TESTDATA_DIR/hail-node-evac.json
+./test/hs/hail $TESTDATA_DIR/hail-node-evac.json
 >>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
 >>>= 0
 
-./htest/hail $TESTDATA_DIR/hail-change-group.json
+./test/hs/hail $TESTDATA_DIR/hail-change-group.json
 >>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
 >>>= 0
 
 # check that hail can use the simu backend
-./htest/hail --simu p,8,8T,16g,16 $TESTDATA_DIR/hail-alloc-drbd.json
+./test/hs/hail --simu p,8,8T,16g,16 $TESTDATA_DIR/hail-alloc-drbd.json
 >>> /"success":true,/
 >>>= 0
 
 # check that hail can use the text backend
-./htest/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
+./test/hs/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
 >>> /"success":true,/
 >>>= 0
 
 # check that hail can use the simu backend
-./htest/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
+./test/hs/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
 >>> /"success":true,/
 >>>= 0
 
 # check that hail pre/post saved state differs after allocation
-./htest/hail -v -v -v -p $TESTDATA_DIR/hail-alloc-drbd.json -S $T/hail-alloc >/dev/null 2>&1 && ! diff -q $T/hail-alloc.pre-ialloc $T/hail-alloc.post-ialloc
+./test/hs/hail -v -v -v -p $TESTDATA_DIR/hail-alloc-drbd.json -S $T/hail-alloc >/dev/null 2>&1 && ! diff -q $T/hail-alloc.pre-ialloc $T/hail-alloc.post-ialloc
 >>> /Files .* and .* differ/
 >>>= 0
 
 # check that hail pre/post saved state differs after relocation
-./htest/hail -v -v -v -p $TESTDATA_DIR/hail-reloc-drbd.json -S $T/hail-reloc >/dev/null 2>&1 && ! diff -q $T/hail-reloc.pre-ialloc $T/hail-reloc.post-ialloc
+./test/hs/hail -v -v -v -p $TESTDATA_DIR/hail-reloc-drbd.json -S $T/hail-reloc >/dev/null 2>&1 && ! diff -q $T/hail-reloc.pre-ialloc $T/hail-reloc.post-ialloc
 >>> /Files .* and .* differ/
 >>>= 0
 
 # evac tests
-./htest/hail $T/hail-node-evac.json.primary-only
+./test/hs/hail $T/hail-node-evac.json.primary-only
 >>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
 >>>= 0
 
-./htest/hail $T/hail-node-evac.json.secondary-only
+./test/hs/hail $T/hail-node-evac.json.secondary-only
 >>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
 >>>= 0
 
-./htest/hail $T/hail-node-evac.json.all
+./test/hs/hail $T/hail-node-evac.json.all
 >>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
 >>>= 0
diff --git a/test/hs/shelltests/htools-hspace.test b/test/hs/shelltests/htools-hspace.test
new file mode 100644
index 0000000000000000000000000000000000000000..553c98dffacae8516ff360a1284c2473ce1b6242
--- /dev/null
+++ b/test/hs/shelltests/htools-hspace.test
@@ -0,0 +1,8 @@
+# test that hspace machine readable output looks correct
+./test/hs/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8
+>>> /^HTS_OK=1/
+>>>= 0
+
+# test again via a file and shell parsing
+./test/hs/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8 > $T/capacity && sh -c ". $T/capacity && test x\$HTS_OK = x1"
+>>>= 0
diff --git a/htest/shelltests/htools-invalid.test b/test/hs/shelltests/htools-invalid.test
similarity index 57%
rename from htest/shelltests/htools-invalid.test
rename to test/hs/shelltests/htools-invalid.test
index 649c540256d39d1f1195d459c0933c185f279659..4c08a5859a57e87ccc8a50b9be7d173620258217 100644
--- a/htest/shelltests/htools-invalid.test
+++ b/test/hs/shelltests/htools-invalid.test
@@ -1,59 +1,59 @@
 # invalid option test
-./htest/hail --no-such-option
+./test/hs/hail --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hbal --no-such-option
+./test/hs/hbal --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hspace --no-such-option
+./test/hs/hspace --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hscan --no-such-option
+./test/hs/hscan --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hinfo --no-such-option
+./test/hs/hinfo --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hcheck --no-such-option
+./test/hs/hcheck --no-such-option
 >>>= 2
 
 # invalid option test
-./htest/hroller --no-such-option
+./test/hs/hroller --no-such-option
 >>>= 2
 
 # extra arguments
-./htest/hspace unexpected-argument
+./test/hs/hspace unexpected-argument
 >>>2
 Error: This program doesn't take any arguments.
 >>>=1
 
-./htest/hbal unexpected-argument
+./test/hs/hbal unexpected-argument
 >>>2
 Error: This program doesn't take any arguments.
 >>>=1
 
-./htest/hinfo unexpected-argument
+./test/hs/hinfo unexpected-argument
 >>>2
 Error: This program doesn't take any arguments.
 >>>=1
 
-./htest/hcheck unexpected-argument
+./test/hs/hcheck unexpected-argument
 >>>2
 Error: This program doesn't take any arguments.
 >>>=1
 
-./htest/hroller unexpected-argument
+./test/hs/hroller unexpected-argument
 >>>2
 Error: This program doesn't take any arguments.
 >>>=1
 
 # hroller fails to build a graph for an empty cluster
-./htest/hroller -t$TESTDATA_DIR/empty-cluster.data
+./test/hs/hroller -t$TESTDATA_DIR/empty-cluster.data
 >>>2
 Error: Cannot create node graph
 >>>=1
diff --git a/htest/shelltests/htools-mon-collector.test b/test/hs/shelltests/htools-mon-collector.test
similarity index 93%
rename from htest/shelltests/htools-mon-collector.test
rename to test/hs/shelltests/htools-mon-collector.test
index de276e168764bdfd4774c891a563572cbd69b0f7..e0ab7a30904702793742c9b242d0f7c690722437 100644
--- a/htest/shelltests/htools-mon-collector.test
+++ b/test/hs/shelltests/htools-mon-collector.test
@@ -1,29 +1,29 @@
 # 1. Test that mon-collector won't run without specifying a personality
-./htest/hpc-mon-collector
+./test/hs/hpc-mon-collector
 >>>= !0
 
 # 2. Test that standard options are accepted, both at top level
 # and subcommands level
-./htest/hpc-mon-collector --help
+./test/hs/hpc-mon-collector --help
 >>>= 0
 
-./htest/hpc-mon-collector --help-completion
+./test/hs/hpc-mon-collector --help-completion
 >>>= 0
 
-./htest/hpc-mon-collector --version
+./test/hs/hpc-mon-collector --version
 >>>= 0
 
-./htest/hpc-mon-collector drbd --help
+./test/hs/hpc-mon-collector drbd --help
 >>>= 0
 
-./htest/hpc-mon-collector drbd --help-completion
+./test/hs/hpc-mon-collector drbd --help-completion
 >>>= 0
 
-./htest/hpc-mon-collector drbd --version
+./test/hs/hpc-mon-collector drbd --version
 >>>= 0
 
 # 3. Test that the drbd collector fails parsing /dev/null
-./htest/hpc-mon-collector drbd /dev/null
+./test/hs/hpc-mon-collector drbd /dev/null
 >>>2
 Error: ""
 []
@@ -31,22 +31,22 @@ Failed reading: versionInfo
 >>>= !0
 
 # 4. Test that a non-existent file is correctly reported
-./htest/hpc-mon-collector drbd /dev/no-such-file
+./test/hs/hpc-mon-collector drbd /dev/no-such-file
 >>>2/Error: reading from file: .* does not exist/
 >>>= !0
 
 # 5. Test that multiple files are rejected
-./htest/hpc-mon-collector drbd /dev/null /dev/null
+./test/hs/hpc-mon-collector drbd /dev/null /dev/null
 >>>2/takes only one argument/
 >>>= !0
 
 # 6. Test that a standard test file is parsed correctly
-./htest/hpc-mon-collector drbd $PYTESTDATA_DIR/proc_drbd83.txt
+./test/hs/hpc-mon-collector drbd $PYTESTDATA_DIR/proc_drbd83.txt
 >>>=0
 
 # 7. Test that the drbd collector fails parsing /dev/zero, but is not
 # stuck forever printing \NUL chars
-./htest/hpc-mon-collector drbd /dev/zero
+./test/hs/hpc-mon-collector drbd /dev/zero
 >>>2
 Error: "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL"
 []
diff --git a/htest/shelltests/htools-multi-group.test b/test/hs/shelltests/htools-multi-group.test
similarity index 52%
rename from htest/shelltests/htools-multi-group.test
rename to test/hs/shelltests/htools-multi-group.test
index 34a41b4871478008de8bfba47c988544f4658428..7bb1a21a621f0361129911cff2baf3a412b2bf42 100644
--- a/htest/shelltests/htools-multi-group.test
+++ b/test/hs/shelltests/htools-multi-group.test
@@ -1,47 +1,47 @@
 # standard multi-group tests
-./htest/hinfo -v -v -p --print-instances -t$T/simu-twogroups.standard
+./test/hs/hinfo -v -v -p --print-instances -t$T/simu-twogroups.standard
 >>>= 0
-./htest/hbal -t$T/simu-twogroups.standard
+./test/hs/hbal -t$T/simu-twogroups.standard
 >>>= !0
 
 # hbal should not be able to balance
-./htest/hbal -t$T/simu-twogroups.standard
+./test/hs/hbal -t$T/simu-twogroups.standard
 >>>2 /Found multiple node groups/
 >>>= !0
 
 # but hbal should be able to balance one node group
-./htest/hbal -t$T/simu-twogroups.standard -G group-01
+./test/hs/hbal -t$T/simu-twogroups.standard -G group-01
 >>>= 0
 # and it should not find an invalid group
-./htest/hbal -t$T/simu-twogroups.standard -G no-such-group
+./test/hs/hbal -t$T/simu-twogroups.standard -G no-such-group
 >>>= !0
 
 # tiered allocs multi-group tests
-./htest/hinfo -v -v -p --print-instances -t$T/simu-twogroups.tiered
+./test/hs/hinfo -v -v -p --print-instances -t$T/simu-twogroups.tiered
 >>>= 0
-./htest/hbal -t$T/simu-twogroups.tiered
+./test/hs/hbal -t$T/simu-twogroups.tiered
 >>>= !0
 
 # hbal should not be able to balance
-./htest/hbal -t$T/simu-twogroups.tiered
+./test/hs/hbal -t$T/simu-twogroups.tiered
 >>>2 /Found multiple node groups/
 >>>= !0
 
 # but hbal should be able to balance one node group
-./htest/hbal -t$T/simu-twogroups.tiered -G group-01
+./test/hs/hbal -t$T/simu-twogroups.tiered -G group-01
 >>>= 0
 # and it should not find an invalid group
-./htest/hbal -t$T/simu-twogroups.tiered -G no-such-group
+./test/hs/hbal -t$T/simu-twogroups.tiered -G no-such-group
 >>>= !0
 
 # hcheck should be able to run with multiple groups
-./htest/hcheck -t$T/simu-twogroups.tiered --machine-readable
+./test/hs/hcheck -t$T/simu-twogroups.tiered --machine-readable
 >>> /HCHECK_OK=1/
 >>>= 0
 
 # hcheck should be able to improve a group with split instances, and also
 # warn us about them
-./htest/hbal -t $TESTDATA_DIR/hbal-split-insts.data -G group-01 -O node-01-001 -v
+./test/hs/hbal -t $TESTDATA_DIR/hbal-split-insts.data -G group-01 -O node-01-001 -v
 >>> /Cluster score improved from .* to .*/
 >>>2/Found instances belonging to multiple node groups:/
 >>>= 0
diff --git a/htest/shelltests/htools-no-backend.test b/test/hs/shelltests/htools-no-backend.test
similarity index 67%
rename from htest/shelltests/htools-no-backend.test
rename to test/hs/shelltests/htools-no-backend.test
index fc2ac9c388e1c46fcad922e63d1e86f91631db31..66c14dee87206ea374d1458dfe934b0092bcde07 100644
--- a/htest/shelltests/htools-no-backend.test
+++ b/test/hs/shelltests/htools-no-backend.test
@@ -1,25 +1,25 @@
 # hail no input file
-./htest/hail
+./test/hs/hail
 >>>= 1
 
 # hbal no backend
-./htest/hbal
+./test/hs/hbal
 >>>= 1
 
 # hspace no backend
-./htest/hspace
+./test/hs/hspace
 >>>= 1
 
 # hinfo no backend
-./htest/hinfo
+./test/hs/hinfo
 >>>= 1
 
 # hroller no backend
-./htest/hroller
+./test/hs/hroller
 >>>= 1
 
 # hbal multiple backends
-./htest/hbal -t /dev/null -m localhost
+./test/hs/hbal -t /dev/null -m localhost
 >>>2
 Error: Only one of the rapi, luxi, and data files options should be given.
 >>>= 1
diff --git a/htest/shelltests/htools-rapi.test b/test/hs/shelltests/htools-rapi.test
similarity index 57%
rename from htest/shelltests/htools-rapi.test
rename to test/hs/shelltests/htools-rapi.test
index a0692c95a5d20e9bbbad877a678474e2d7c7def2..ffe2991acc7599a37d463ca8cb6ef2c44d494edf 100644
--- a/htest/shelltests/htools-rapi.test
+++ b/test/hs/shelltests/htools-rapi.test
@@ -1,8 +1,8 @@
 # test loading data via RAPI
-./htest/hinfo -v -v -p --print-instances -m $RAPI_URL
+./test/hs/hinfo -v -v -p --print-instances -m $RAPI_URL
 >>>= 0
 
-./htest/hbal -v -v -p --print-instances -m $RAPI_URL
+./test/hs/hbal -v -v -p --print-instances -m $RAPI_URL
 >>>= 0
 
 # this compares generated files from hscan
diff --git a/test/hs/shelltests/htools-single-group.test b/test/hs/shelltests/htools-single-group.test
new file mode 100644
index 0000000000000000000000000000000000000000..f8e629cb5eb0b9abc587389a0a004d7d9dfaf123
--- /dev/null
+++ b/test/hs/shelltests/htools-single-group.test
@@ -0,0 +1,37 @@
+# standard single-group tests
+./test/hs/hinfo -v -v -p --print-instances -t$T/simu-onegroup.standard
+>>>= 0
+
+./test/hs/hbal  -v -v -p --print-instances -t$T/simu-onegroup.standard
+>>>= 0
+
+# hbal should not be able to balance
+./test/hs/hbal -t$T/simu-onegroup.standard
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+
+# tiered single-group tests
+./test/hs/hinfo -v -v -p --print-instances -t$T/simu-onegroup.tiered
+>>>= 0
+
+./test/hs/hbal  -v -v -p --print-instances -t$T/simu-onegroup.tiered
+>>>= 0
+
+# hbal should not be able to balance
+./test/hs/hbal -t$T/simu-onegroup.tiered
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+# hcheck should not find reason to rebalance
+./test/hs/hcheck -t$T/simu-onegroup.tiered --machine-readable
+>>> /HCHECK_INIT_CLUSTER_NEED_REBALANCE=0/
+>>>= 0
+
+# hroller should be able to print the solution
+./test/hs/hroller -t$T/simu-onegroup.tiered
+>>>= 0
+
+# hroller should be able to print the solution, in verbose mode as well
+./test/hs/hroller -t$T/simu-onegroup.tiered -v -v
+>>>= 0
diff --git a/htest/shelltests/htools-text-backend.test b/test/hs/shelltests/htools-text-backend.test
similarity index 55%
rename from htest/shelltests/htools-text-backend.test
rename to test/hs/shelltests/htools-text-backend.test
index ab89804b77a47ae14b22ef381de7fbab0682ed80..839885276eefd93d18303b9dc7ce530c1bf126c7 100644
--- a/htest/shelltests/htools-text-backend.test
+++ b/test/hs/shelltests/htools-text-backend.test
@@ -1,30 +1,30 @@
 # missing resources test
-./htest/hbal -t $TESTDATA_DIR/missing-resources.data
+./test/hs/hbal -t $TESTDATA_DIR/missing-resources.data
 >>>2 /node node2 is missing .* ram and .* disk/
 >>>= 0
-./htest/hinfo -t $TESTDATA_DIR/missing-resources.data
+./test/hs/hinfo -t $TESTDATA_DIR/missing-resources.data
 >>>2 /node node2 is missing .* ram and .* disk/
 >>>= 0
 
 
 # common suffix test
-./htest/hbal -t $TESTDATA_DIR/common-suffix.data -v -v
+./test/hs/hbal -t $TESTDATA_DIR/common-suffix.data -v -v
 >>>/Stripping common suffix of '\.example\.com' from names/
 >>>= 0
-./htest/hinfo -t $TESTDATA_DIR/common-suffix.data -v -v
+./test/hs/hinfo -t $TESTDATA_DIR/common-suffix.data -v -v
 >>>/Stripping common suffix of '\.example\.com' from names/
 >>>= 0
 
 
 # invalid node test
-./htest/hbal -t $TESTDATA_DIR/invalid-node.data
+./test/hs/hbal -t $TESTDATA_DIR/invalid-node.data
 >>>2 /Unknown node '.*' for instance new-0/
 >>>= !0
 
-./htest/hspace -t $TESTDATA_DIR/invalid-node.data
+./test/hs/hspace -t $TESTDATA_DIR/invalid-node.data
 >>>2 /Unknown node '.*' for instance new-0/
 >>>= !0
 
-./htest/hinfo -t $TESTDATA_DIR/invalid-node.data
+./test/hs/hinfo -t $TESTDATA_DIR/invalid-node.data
 >>>2 /Unknown node '.*' for instance new-0/
 >>>= !0
diff --git a/htest/test.hs b/test/hs/test.hs
similarity index 100%
rename from htest/test.hs
rename to test/hs/test.hs