diff --git a/repos/os/run/init.run b/repos/os/run/init.run
new file mode 100644
index 0000000000..42ae61f89e
--- /dev/null
+++ b/repos/os/run/init.run
@@ -0,0 +1,129 @@
+#
+# Build
+#
+
+set build_components { core init drivers/timer app/dummy test/init }
+
+build $build_components
+
+create_boot_directory
+
+#
+# Generate config
+#
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+#
+# Boot modules
+#
+
+set boot_modules { core ld.lib.so init timer report_rom test-init dummy }
+
+build_boot_image $boot_modules
+
+append qemu_args " -nographic "
+
+run_genode_until {.*child "test-init" exited with exit value 0.*} 60
+
diff --git a/repos/os/src/app/dummy/main.cc b/repos/os/src/app/dummy/main.cc
new file mode 100644
index 0000000000..beba60f62e
--- /dev/null
+++ b/repos/os/src/app/dummy/main.cc
@@ -0,0 +1,98 @@
+/*
+ * \brief Dummy component used for automated component-composition tests
+ * \author Norman Feske
+ * \date 2017-02-16
+ */
+
+/*
+ * Copyright (C) 2017 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU General Public License version 2.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Dummy {
+
+ struct Log_connections;
+ struct Main;
+ using namespace Genode;
+}
+
+
+struct Dummy::Log_connections
+{
+ Env &_env;
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ typedef Registered Connection;
+
+ Registry _connections;
+
+ Log_connections(Env &env, Xml_node node) : _env(env)
+ {
+ unsigned const count = node.attribute_value("count", 0UL);
+
+ log("going to create ", count, " LOG connections");
+
+ for (unsigned i = 0; i < count; i++)
+ new (_heap) Connection(_connections, _env, Session_label { i });
+
+ log("created all LOG connections");
+ }
+
+ ~Log_connections()
+ {
+ _connections.for_each([&] (Connection &c) { destroy(_heap, &c); });
+
+ log("destroyed all LOG connections");
+ }
+};
+
+
+struct Dummy::Main
+{
+ Env &_env;
+
+ Constructible _timer;
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Constructible _log_connections;
+
+ Main(Env &env) : _env(env)
+ {
+ _config.xml().for_each_sub_node([&] (Xml_node node) {
+
+ if (node.type() == "create_log_connections")
+ _log_connections.construct(_env, node);
+
+ if (node.type() == "destroy_log_connections")
+ _log_connections.destruct();
+
+ if (node.type() == "sleep") {
+
+ if (!_timer.constructed())
+ _timer.construct(_env);
+
+ _timer->msleep(node.attribute_value("ms", 100UL));
+ }
+
+ if (node.type() == "log")
+ log(node.attribute_value("string", String<50>()));
+ });
+ }
+};
+
+
+
+
+void Component::construct(Genode::Env &env) { static Dummy::Main main(env); }
diff --git a/repos/os/src/app/dummy/target.mk b/repos/os/src/app/dummy/target.mk
new file mode 100644
index 0000000000..a8da11df29
--- /dev/null
+++ b/repos/os/src/app/dummy/target.mk
@@ -0,0 +1,3 @@
+TARGET = dummy
+SRC_CC = main.cc
+LIBS += base
diff --git a/repos/os/src/test/init/main.cc b/repos/os/src/test/init/main.cc
new file mode 100644
index 0000000000..fddceff22f
--- /dev/null
+++ b/repos/os/src/test/init/main.cc
@@ -0,0 +1,329 @@
+/*
+ * \brief Test for the init component
+ * \author Norman Feske
+ * \date 2017-02-16
+ */
+
+/*
+ * Copyright (C) 2017 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU General Public License version 2.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Test {
+
+ struct Log_message_handler;
+ class Log_session_component;
+ class Log_root;
+ struct Main;
+
+ using namespace Genode;
+
+ static bool xml_attribute_matches(Xml_node, Xml_node);
+ static bool xml_matches(Xml_node, Xml_node);
+}
+
+
+/***************
+ ** Utilities **
+ ***************/
+
+static inline bool Test::xml_attribute_matches(Xml_node condition, Xml_node node)
+{
+ typedef String<32> Name;
+ typedef String<64> Value;
+
+ Name const name = condition.attribute_value("name", Name());
+ Value const value = condition.attribute_value("value", Value());
+
+ return node.attribute_value(name.string(), Value()) == value;
+}
+
+
+/**
+ * Return true if 'node' has expected content
+ *
+ * \expected description of the XML content expected in 'node'
+ */
+static inline bool Test::xml_matches(Xml_node expected, Xml_node node)
+{
+ bool matches = true;
+ expected.for_each_sub_node([&] (Xml_node condition) {
+
+ if (condition.type() == "attribute")
+ matches = matches && xml_attribute_matches(condition, node);
+
+ if (condition.type() == "node") {
+
+ typedef String<32> Name;
+ Name const name = condition.attribute_value("name", Name());
+
+ bool at_least_one_sub_node_matches = false;
+ node.for_each_sub_node(name.string(), [&] (Xml_node sub_node) {
+ if (xml_matches(condition, sub_node))
+ at_least_one_sub_node_matches = true; });
+
+ matches = matches && at_least_one_sub_node_matches;
+ }
+ });
+ return matches;
+}
+
+
+struct Test::Log_message_handler
+{
+ typedef String Message;
+
+ enum Result { EXPECTED, UNEXPECTED, IGNORED };
+
+ virtual Result handle_log_message(Message const &message) = 0;
+};
+
+
+namespace Genode
+{
+ static inline void print(Output &output, Test::Log_message_handler::Result result)
+ {
+ using Genode::print;
+ switch (result) {
+ case Test::Log_message_handler::EXPECTED: print(output, "expected"); break;
+ case Test::Log_message_handler::UNEXPECTED: print(output, "expected"); break;
+ case Test::Log_message_handler::IGNORED: print(output, "ignored"); break;
+ }
+ }
+}
+
+
+class Test::Log_session_component : public Rpc_object
+{
+ private:
+
+ Session_label const _label;
+
+ Log_message_handler &_handler;
+
+ public:
+
+ Log_session_component(Session_label const &label, Log_message_handler &handler)
+ :
+ _label(label), _handler(handler)
+ { }
+
+ size_t write(String const &string)
+ {
+ /* strip known line delimiter from incoming message */
+ unsigned n = 0;
+ Genode::String<16> const pattern("\033[0m\n");
+ for (char const *s = string.string(); s[n] && pattern != s + n; n++);
+
+ Log_message_handler::Message const
+ message("[", _label, "] ", Cstring(string.string(), n));
+
+ Log_message_handler::Result const result =
+ _handler.handle_log_message(message);
+
+ log(message, " (", result, ")");
+
+ return strlen(string.string());
+ }
+};
+
+
+class Test::Log_root : public Root_component
+{
+ private:
+
+ Log_message_handler &_handler;
+
+ public:
+
+ Log_root(Entrypoint &ep, Allocator &md_alloc, Log_message_handler &handler)
+ :
+ Root_component(ep, md_alloc), _handler(handler)
+ { }
+
+ Log_session_component *_create_session(const char *args, Affinity const &)
+ {
+ Session_label const label = label_from_args(args);
+
+ return new (md_alloc()) Log_session_component(label, _handler);
+ }
+};
+
+
+struct Test::Main : Log_message_handler
+{
+ Env &_env;
+
+ Timer::Connection _timer { _env };
+
+ Reporter _init_config_reporter { _env, "config", "init.config" };
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ void _publish_report(Reporter &reporter, Xml_node node)
+ {
+ typedef String<64> Version;
+ Version const version = node.attribute_value("version", Version());
+
+ Reporter::Xml_generator xml(reporter, [&] () {
+
+ if (version.valid())
+ xml.attribute("version", version);
+
+ xml.append(node.content_base(), node.content_size());
+ });
+ }
+
+ Log_message_handler::Message _expected_log_message;
+
+ unsigned const _num_steps = _config.xml().num_sub_nodes();
+ unsigned _curr_step = 0;
+
+ Xml_node _curr_step_xml() const { return _config.xml().sub_node(_curr_step); }
+
+ /*
+ * Handling of state reports generated by init
+ */
+ Attached_rom_dataspace _init_state { _env, "state" };
+
+ Signal_handler _init_state_handler {
+ _env.ep(), *this, &Main::_handle_init_state };
+
+ void _handle_init_state()
+ {
+ _init_state.update();
+ _execute_curr_step();
+ }
+
+ void _advance_step()
+ {
+ _curr_step++;
+
+ /* exit when reaching the end of the sequence */
+ if (_curr_step == _num_steps) {
+ _env.parent().exit(0);
+ sleep_forever();
+ }
+ };
+
+ void _execute_curr_step()
+ {
+ for (;;) {
+ Xml_node const step = _curr_step_xml();
+
+ log("step ", _curr_step, " (", step.type(), ")");
+
+ if (step.type() == "expect_log")
+ return;
+
+ if (step.type() == "expect_init_state") {
+ if (xml_matches(step, _init_state.xml())) {
+ _advance_step();
+ continue;
+ } else {
+ warning("init state does not match: ", _init_state.xml());
+ warning("expected condition: ", step);
+ }
+ return;
+ }
+
+ if (step.type() == "init_config") {
+ _publish_report(_init_config_reporter, step);
+ _advance_step();
+ continue;
+ }
+
+ if (step.type() == "message") {
+ typedef String<80> Message;
+ Message const message = step.attribute_value("string", Message());
+ log("\n--- ", message, " ---");
+ _advance_step();
+ continue;
+ }
+
+ if (step.type() == "nop") {
+ _advance_step();
+ continue;
+ }
+
+ if (step.type() == "sleep") {
+ unsigned long const timeout_ms = step.attribute_value("ms", 250UL);
+ _timer.trigger_once(timeout_ms*1000);
+ return;
+ }
+
+ error("unexpected step: ", step);
+ throw Exception();
+ }
+ }
+
+ /**
+ * Log_message_handler interface
+ */
+ Result handle_log_message(Log_message_handler::Message const &message) override
+ {
+ typedef Log_message_handler::Message Message;
+
+ if (_curr_step_xml().type() != "expect_log")
+ return IGNORED;
+
+ Message const expected = _curr_step_xml().attribute_value("string", Message());
+
+ if (message != expected)
+ return IGNORED;
+
+ _advance_step();
+ _execute_curr_step();
+ return EXPECTED;
+ }
+
+ /*
+ * Timer handling
+ */
+ Signal_handler _timer_handler { _env.ep(), *this, &Main::_handle_timer };
+
+ void _handle_timer()
+ {
+ if (_curr_step_xml().type() != "sleep") {
+ error("got spurious timeout signal");
+ throw Exception();
+ }
+
+ _advance_step();
+ _execute_curr_step();
+ }
+
+ /*
+ * LOG service provided to init
+ */
+ Sliced_heap _sliced_heap { _env.ram(), _env.rm() };
+
+ Log_root _log_root { _env.ep(), _sliced_heap, *this };
+
+
+ Main(Env &env) : _env(env)
+ {
+ _timer.sigh(_timer_handler);
+ _init_config_reporter.enabled(true);
+ _init_state.sigh(_init_state_handler);
+ _execute_curr_step();
+
+ _env.parent().announce(_env.ep().manage(_log_root));
+ }
+};
+
+
+void Component::construct(Genode::Env &env) { static Test::Main main(env); }
+
diff --git a/repos/os/src/test/init/target.mk b/repos/os/src/test/init/target.mk
new file mode 100644
index 0000000000..cd7d34b848
--- /dev/null
+++ b/repos/os/src/test/init/target.mk
@@ -0,0 +1,3 @@
+TARGET = test-init
+SRC_CC = main.cc
+LIBS += base