diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e9150a673d654f166f4d9ea8ab28fc3ade65964e..08a8c0e2903f8a12be4714a1732a588cb0f3f376 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,4 +13,5 @@ linter:
 test:
   stage: test 
   script:
-    - ./run-tests
+    - make -sj SANITIZER=all
+    - make -sk test SANITIZER=all
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..6084a98ea308a086df2cb22aadd2b0568348b752
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,179 @@
+NAME := image-transformer
+
+##### Compiler / analyzer common configuration.
+
+CC = clang
+LINKER = $(CC)
+
+RM = rm -rf
+MKDIR = mkdir -p
+
+# Clang-tidy
+CLANG_TIDY = clang-tidy
+
+_noop =
+_space = $(noop) $(noop)
+_comma = ,
+
+# Using `+=` to let user define their own checks in command line
+CLANG_TIDY_CHECKS += $(strip $(file < clang-tidy-checks.txt))
+CLANG_TIDY_ARGS = \
+	-warnings-as-errors=* -header-filter="$(abspath $(INCDIR.main))/.*" \
+	-checks="$(subst $(_space),$(_comma),$(CLANG_TIDY_CHECKS))" \
+
+# Sanitizers
+CFLAGS.none :=
+CFLAGS.asan := -fsanitize=address
+CFLAGS.lsan := -fsanitize=leak
+CFLAGS.msan := -fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls
+CFLAGS.usan := -fsanitize=undefined
+
+SANITIZER ?= none
+ifeq ($(SANITIZER),)
+override SANITIZER := none
+endif
+
+ifeq ($(words $(SANITIZER)),1)
+ifeq ($(filter $(SANITIZER),all asan lsan msan usan none),)
+$(error Please provide correct argument value for SANITIZER: all, asan, lsan, msan, usan or none)
+endif
+endif
+
+# Using `+=` to let user define their own flags in command line
+CFLAGS += $(CFLAGS.$(SANITIZER))
+LDFLAGS += $(CFLAGS.$(SANITIZER))
+
+ifeq ($(SANITIZER),none)
+OBJDIR = obj
+BUILDDIR = build
+else
+OBJDIR = obj/$(SANITIZER)
+BUILDDIR = build/$(SANITIZER)
+endif
+
+##### Configuration for `main` target.
+
+SOLUTION_DIR = solution
+
+SRCDIR.main = $(SOLUTION_DIR)/src
+INCDIR.main = $(SOLUTION_DIR)/include
+OBJDIR.main = $(OBJDIR)/$(SOLUTION_DIR)
+
+SOURCES.main += $(wildcard $(SRCDIR.main)/*.c) $(wildcard $(SRCDIR.main)/*/*.c)
+TARGET.main  := $(BUILDDIR)/$(NAME)
+
+CFLAGS.main += $(strip $(file < $(SOLUTION_DIR)/compile_flags.txt)) $(CFLAGS) -I$(INCDIR.main)
+
+##### Configuration for `tester` target.
+
+TESTER_DIR = tester
+TESTER_SCRIPT = $(TESTER_DIR)/tester.sh
+
+SRCDIR.tester = $(TESTER_DIR)/src
+INCDIR.tester = $(TESTER_DIR)/include
+OBJDIR.tester = $(OBJDIR)/$(TESTER_DIR)
+
+SOURCES.tester += $(wildcard $(SRCDIR.tester)/*.c)
+TARGET.tester  := $(BUILDDIR)/image-tester
+
+CFLAGS.tester += $(strip $(file < $(TESTER_DIR)/compile_flags.txt)) $(CFLAGS) -I$(INCDIR.tester)
+
+##### Rule templates. Should be instantiated with $(eval $(call template, ...))
+
+# I use $$(var) in some variables to avoid variable expanding too early.
+# I do not remember when $(var) is expanded in `define` rules, but $$(var)
+# is expanded exactly at $(eval ...) call.
+
+# Template for running submake on each SANITIZER value when SANITIZER=all is used. 
+# $(1) - SANITIZER value (none/asan/lsan/msan/usan)
+define make-sanitizer-rule
+
+GOALS.$(1) := $$(patsubst %,%.$(1),$$(MAKECMDGOALS))
+
+$$(MAKECMDGOALS): $$(GOALS.$(1))
+.PHONY: $$(GOALS.$(1))
+
+$$(GOALS.$(1)):
+	@echo Running 'make $$(patsubst %.$(1),%,$$@)' with SANITIZER=$(1)
+	@$(MAKE) $$(patsubst %.$(1),%,$$@) SANITIZER=$(1)
+
+endef
+
+# Template for compilation and linking rules + depfile generation and inclusion
+# $(1) - target name (main/tester)
+define make-compilation-rule
+
+OBJECTS.$(1) := $$(SOURCES.$(1):$$(SRCDIR.$(1))/%.c=$$(OBJDIR.$(1))/%.o)
+SRCDEPS.$(1) := $$(OBJECTS.$(1):%.o=%.o.d)
+
+DIRS.$(1) := $$(sort $$(dir $$(OBJECTS.$(1)) $$(TARGET.$(1))))
+DIRS += $$(DIRS.$(1))
+
+.PHONY: build-$(1)
+
+build-$(1): $$(TARGET.$(1))
+
+$$(TARGET.$(1)): $$(OBJECTS.$(1)) | $$(DIRS.$(1))
+	$(LINKER) $(LDFLAGS) $$(OBJECTS.$(1)) -o $$@
+
+$$(OBJDIR.$(1))/%.o: $$(SRCDIR.$(1))/%.c | $$(DIRS.$(1))
+	$(CC) $(CFLAGS.$(1)) -M -MP $$< >$$@.d
+	$(CC) $(CFLAGS.$(1)) -c $$< -o $$@
+
+-include $$(SRCDEPS.$(1))
+
+endef
+
+# Template for testing rules.
+# $(1) - directory with test
+define make-test-rule
+
+TST_NAME.$(1) := $$(notdir $(1))
+
+TST_INPUT.$(1) := $(1)/input.bmp
+TST_OUTPUT.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1)).bmp
+TST_EXPECTED.$(1) := $(1)/output_expected.bmp
+
+TST_LOG_OUT.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1))_out.log
+TST_LOG_ERR.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1))_err.log
+
+.PHONY: $$(TST_OUTPUT.$(1)) test-$$(TST_NAME.$(1))
+test: test-$$(TST_NAME.$(1))
+
+test-$$(TST_NAME.$(1)): build-main build-tester
+	$(TESTER_SCRIPT) $$(TST_NAME.$(1)) \
+		--main-cmd '$(TARGET.main) $$(TST_INPUT.$(1)) $$(TST_OUTPUT.$(1))' \
+		--tester-cmd '$(TARGET.tester) $$(TST_OUTPUT.$(1)) $$(TST_EXPECTED.$(1))' \
+		--log-dir '$(OBJDIR.tester)'
+
+endef
+
+##### Rules and targets.
+
+.PHONY: all test clean check
+
+ifeq ($(MAKECMDGOALS),)
+MAKECMDGOALS := all
+endif
+
+
+ifeq ($(SANITIZER),all)
+# Do all the work in sub-makes
+$(foreach sanitizer,none asan lsan msan usan,$(eval $(call make-sanitizer-rule,$(sanitizer))))
+else
+
+all: build-main
+
+check:
+	$(CLANG_TIDY) $(CLANG_TIDY_ARGS) $(SOURCES.main)
+
+$(foreach target,main tester,$(eval $(call make-compilation-rule,$(target))))
+$(foreach test,$(sort $(wildcard $(TESTER_DIR)/tests/*)),$(eval $(call make-test-rule,$(test))))
+
+clean:
+	$(RM) $(OBJDIR) $(BUILDDIR)
+
+$(sort $(DIRS)):
+	$(MKDIR) $@
+
+endif
diff --git a/README.md b/README.md
index c963a05ba8e1c84cafdf53641f0b9521caacbd1b..d3b00991a09b744ce075398e4b46c37cee21a0bf 100644
--- a/README.md
+++ b/README.md
@@ -225,15 +225,50 @@ bmp-заголовка.
 ```
 
 - Архитектура приложения описана в предыдущем разделе. 
-- Мы предоставляем вам `Makefile`, который нужно использовать. Заголовочные файлы ищутся в `include`.
-  Ваша программа будет тестироваться с помощью `clang` и проверяться
-  статическим анализатором `clang-tidy`.  Флаги и формат его запуска можно
-  посмотреть
-  [тут](https://gitlab.se.ifmo.ru/low-level-programming/docker-c-test-machine/-/blob/master/to-docker-image/run-checks.sh).
-  Тесты запускаются в
+- Код размещается в директории `solution/src`, заголовочные файлы ищутся в `solution/include`.
+
+## Система сборки и тестирования
+
+- Используйте команду `make` для того, чтобы собрать ваш код в исполняемый файл
+  `build/image-transformer`. Проверьте, что в вашей системе установлены программа
+  `make` версии как минимум 4.0 и компилятор `clang`. Флаги для компиляции берутся
+  из `compile_flags.txt` &mdash; вы можете добавить свои флаги в конец
+  этого файла или в саму команду `make`. Например, чтобы собрать код с оптимизациями
+  и протестировать скорость выполнения, вы можете использовать `make CFLAGS=-O3`.
+  С помощью `LDFLAGS` можно передать дополнительные параметры линковщику.
+- Используйте `make check`, чтобы проверить вашу программу через статический анализатор
+  `clang-tidy`. Список проверок описан в файле `clang-tidy-checks.txt`. Вы можете добавить
+  свои проверки в конец этого файла или в параметр `CLANG_TIDY_CHECKS` для `make`.
+- Вы можете собрать код с поддержкой определенных динамических анализаторов (санитайзеров).
+  Санитайзеры могут дать подробную информацию о возможных и реальных ошибках в программе вместо
+  классического сообщения о segmentation fault. Исполняемые файлы будут также размещены в директории `build`, но для санитайзеров выделены отдельные
+  поддиректории с их названием.
+  Поддерживаются следующие санитайзеры:
+  - `make SANITIZER=asan` &mdash; [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html),
+    набор проверок на некорректное использование адресов памяти. Примеры:
+    use-after-free, double-free, выход за пределы стека, кучи или статического блока.
+  - `make SANITIZER=lsan` &mdash; [LeakSanitizer](https://clang.llvm.org/docs/LeakSanitizer.html),
+    проверки на утечки памяти.
+  - `make SANITIZER=msan` &mdash; [MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html),
+    проверяет, что любая используемая ячейка памяти проинициализирована на момент чтения из нее.
+  - `make SANITIZER=usan` &mdash; [UndefinedBehaviourSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html),
+    набор базовых проверок на неопределенное поведение. Примеры: переполнение численного типа,
+    null-pointer dereference.
+  - `make SANITIZER=none` &mdash; собрать код без санитайзеров (по умолчанию).
+  - `make SANITIZER=all` &mdash; собрать 5 версий кода, каждая с одной из предыдущих опций.
+- Используйте `make clean` для очистки временных файлов и директории `build`.
+- Директория `tester` содержит код и изображения для тестирования вашей программы. Используйте
+  `make -k test`, чтобы запустить тесты и вывести отчет по их выполнению. Параметры `CFLAGS`,
+  `LDFLAGS` и `SANITIZER` также могут быть переданы программам, используемым в тестах.
+- Чтобы запустить конкретный тест, посмотрите его название в отчете всех тестов (оно же название
+  директории с изображениями в `testers/tests`) и запустите `make -k test-<name>`. Например,
+  чтобы запустить тест №1 ([изначальная картинка](testers/tests/1/input.bmp) ->
+  [ожидаемый результат](testers/tests/1/output_expected.bmp)), используйте `make -k test-1`.
+- Ваша программа будет тестироваться в
   [Docker-контейнере](https://gitlab.se.ifmo.ru/low-level-programming/docker-c-test-machine/),
-  вы можете использовать его для тестирования вашего кода локально.
-  Скрипт `run-tests` запускает тесты.
+  вы можете использовать его для тестирования вашего кода локально. В контейнере будут запущены:
+  - Статический анализатор - аналогичен команде `make check`
+  - Тесты с санитайзерами - аналогичен команде `make -k test SANITIZER=all`
 
 # Для самопроверки
 
diff --git a/clang-tidy-checks.txt b/clang-tidy-checks.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5a20e55760f0f711ec2c604531b3b81cfebd4cb7
--- /dev/null
+++ b/clang-tidy-checks.txt
@@ -0,0 +1,65 @@
+clang-analyzer-*
+llvm-include-order
+misc-*
+performance-*
+readability-redundant-*
+readability-simplify-*
+readability-const-*
+readability-implicit-bool-*
+readability-identifier-naming
+readability-inconsistent-declaration-parameter-name
+readability-misleading-indentation
+readability-named-parameter
+bugprone-argument-comment
+bugprone-assert-side-effect
+bugprone-bad-signal-to-kill-thread
+bugprone-bool-pointer-implicit-conversion
+bugprone-branch-clone
+bugprone-copy-constructor-init
+bugprone-dangling-handle
+bugprone-dynamic-static-initializers
+bugprone-exception-escape
+bugprone-fold-init-type
+bugprone-forward-declaration-namespace
+bugprone-forwarding-reference-overload
+bugprone-inaccurate-erase
+bugprone-incorrect-roundings
+bugprone-infinite-loop
+bugprone-integer-division
+bugprone-lambda-function-name
+bugprone-macro-parentheses
+bugprone-macro-repeated-side-effects
+bugprone-misplaced-operator-in-strlen-in-alloc
+bugprone-misplaced-pointer-arithmetic-in-alloc
+bugprone-misplaced-widening-cast
+bugprone-move-forwarding-reference
+bugprone-multiple-statement-macro
+bugprone-narrowing-conversions
+bugprone-no-escape
+bugprone-not-null-terminated-result
+bugprone-parent-virtual-call
+bugprone-posix-return
+bugprone-signed-char-misuse
+bugprone-sizeof-container
+bugprone-sizeof-expression
+bugprone-spuriously-wake-up-functions
+bugprone-string-constructor
+bugprone-string-integer-assignment
+bugprone-string-literal-with-embedded-nul
+bugprone-suspicious-enum-usage
+bugprone-suspicious-include
+bugprone-suspicious-memset-usage
+bugprone-suspicious-missing-comma
+bugprone-suspicious-semicolon
+bugprone-suspicious-string-compare
+bugprone-swapped-arguments
+bugprone-terminating-continue
+bugprone-throw-keyword-missing
+bugprone-too-small-loop-variable
+bugprone-undefined-memory-manipulation
+bugprone-undelegated-constructor
+bugprone-unhandled-self-assignment
+bugprone-unused-raii
+bugprone-unused-return-value
+bugprone-use-after-move
+bugprone-virtual-near-miss
diff --git a/run-tests b/run-tests
deleted file mode 100755
index 69f7150f4100b5303214c8b9f3ddb965e795bb4e..0000000000000000000000000000000000000000
--- a/run-tests
+++ /dev/null
@@ -1,45 +0,0 @@
-echo "--------------------"
-echo "Building tester"
-echo "--------------------"
-
-make -C tester
-
-echo "Compiling and testing with sanitizers"
-
-sanitizers=( '-fsanitize=leak -fsanitize=address'
-             '-fsanitize=memory -fsanitize-memory-track-origins=2' 
-             '-fsanitize=undefined'
-           )
-
-
-COMPARE_TEST_RESULTS=./tester/build/bmp-compare 
-
-for sanitizer in "${sanitizers[@]}"
-do 
-  echo "--------------------"
-  echo "Starting tests with the following sanitizer enabled: $sanitizer"
-  echo "--------------------"
-  make -C solution clean && make -C solution with="$sanitizer"  || { echo "Unable to compile; see log" ; exit 1 ; } 
-
-# Here we launch tests
-
-  for test in `ls tester/tests/* -d` ;
-  do
-    fst="$test/input.bmp" 
-    snd="$test/output.bmp"
-    expected="${snd/.bmp/_expected.bmp}"
-
-    ./solution/build/image-transformer "$fst" "$snd"
-
-    echo -e "\ncomparing: $snd and $expected"
-
-    $COMPARE_TEST_RESULTS "$snd" "$expected" && echo -e "OK\n" || exit 1 ;
-  
-  done 
-# tests end
-
-  
-echo ""
-
-done
-
diff --git a/solution/Makefile b/solution/Makefile
deleted file mode 100644
index 397c0a3d1c2000327ddd6505e1a3ff19cc2be4aa..0000000000000000000000000000000000000000
--- a/solution/Makefile
+++ /dev/null
@@ -1,38 +0,0 @@
-CC        = clang
-LINKER	  = clang
-BUILDDIR  = build
-OBJDIR	  = obj
-SRCDIR    = src
-TESTDIR   = tests
-
-
-INCLUDEDIR = include
-CFLAGS	   = -c -std=c17 -I$(INCLUDEDIR) -ggdb -Wall -Werror -pedantic -Wno-attributes
-LDFLAGS    =
-
-SOURCES     = $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/transform/*.c)
-OBJECTS     = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
-
-TARGET      = image-transformer
-
-ifneq ($(with), )
-    CFLAGS += $(with)
-    LDFLAGS += $(with)
-endif
-
-
-all: $(BUILDDIR)/$(TARGET)
-
-$(BUILDDIR)/$(TARGET): $(OBJECTS) | $(BUILDDIR) $(BUILDDIR)/transform
-	$(LINKER) $(LDFLAGS) $(OBJECTS) -o $@
-
-$(OBJECTS): $(OBJDIR)/%.o:$(SRCDIR)/%.c | $(OBJDIR) $(OBJDIR)/transform
-	$(CC) $(CFLAGS) -c $< -o $@
-
-$(BUILDDIR) $(BUILDDIR)/transform $(OBJDIR) $(OBJDIR)/transform:
-	mkdir -p $@
-
-clean:
-	rm -rf $(BUILDDIR) $(OBJDIR)
-
-.PHONY: clean
diff --git a/tester/Makefile b/tester/Makefile
deleted file mode 100644
index d51859461c3cb535a564f9e4a50121b998a4d1ea..0000000000000000000000000000000000000000
--- a/tester/Makefile
+++ /dev/null
@@ -1,32 +0,0 @@
-CC		  = clang
-LINKER	  = clang
-BUILDDIR  = build
-OBJDIR	  = obj
-SRCDIR	  = src
-TESTDIR   = tests
-
-
-INCLUDEDIR = include
-CFLAGS	   = -c -std=c17 -I$(INCLUDEDIR) -ggdb -Wall -Werror -pedantic -Wno-attributes
-LDFLAGS	   =
-
-SOURCES     = $(wildcard $(SRCDIR)/*.c)
-OBJECTS     = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
-
-TARGET      = bmp-compare
-
-all: $(BUILDDIR)/$(TARGET)
-
-$(BUILDDIR)/$(TARGET): $(OBJECTS) | $(BUILDDIR)
-	$(LINKER) $(LDFLAGS) $(OBJECTS) -o $@
-
-$(OBJECTS): $(OBJDIR)/%.o:$(SRCDIR)/%.c | $(OBJDIR)
-	$(CC) $(CFLAGS) -c $< -o $@
-
-$(BUILDDIR) $(OBJDIR) :
-	mkdir -p $@
-
-clean:
-	rm -rf $(BUILDDIR) $(OBJDIR)
-
-.PHONY: clean
diff --git a/tester/tester.sh b/tester/tester.sh
new file mode 100755
index 0000000000000000000000000000000000000000..06e166a29054b1476011feb2099d50c816751d30
--- /dev/null
+++ b/tester/tester.sh
@@ -0,0 +1,91 @@
+#!/bin/bash --
+
+usage="tester.sh --main-cmd '<cmd>' --tester-cmd '<cmd>' <test_name> [--log-dir '<dir>']"
+
+POSITIONAL=()
+while [ $# -gt 0 ]; do
+    key="$1"; shift
+
+    case $key in
+        --main-cmd)
+            main_cmd="$1"; shift
+            ;;
+        --tester-cmd)
+            tester_cmd="$1"; shift
+            ;;
+        --log-dir)
+            log_dir="$1"; shift
+            ;;
+        -h|--help)
+            echo $usage
+            exit
+            ;;
+        *)
+            POSITIONAL+=("$key")
+            ;;
+    esac
+done
+
+set -- "${POSITIONAL[@]}"
+test_name=$1
+# Ignoring everything that is left
+
+if [ -z "$main_cmd" ] || [ -z "$tester_cmd" ]; then
+    echo "Error: --main-cmd and --tester-cmd are required." 1>&2
+    echo $usage 1>&2
+    exit 1
+fi
+
+if [ -z "$test_name" ]; then
+    echo "Error: at least one positional argument is required." 1>&2
+    echo $usage 1>&2
+    exit 1
+fi
+
+if [ -z "$log_dir" ]; then
+    log_out=/dev/stdout
+    log_err=/dev/stderr
+else
+    mkdir -p "$log_dir"
+    log_out="$log_dir/${test_name}_out.log"
+    log_err="$log_dir/${test_name}_err.log"
+fi
+
+
+echo "********************************************************************************"
+echo "$test_name: Started"
+
+$main_cmd >"$log_out" 2>"$log_err"
+rc=$?
+
+if [ "$rc" -ne "0" ]; then
+    echo
+    echo "Failed at creating output. Command: $main_cmd"
+    if [ ! -z "$log_dir" ]; then
+	    [ ! -e "$log_out" ] && echo "*** stdout log: $log_out ***" && cat $log_out
+	    [ ! -e "$log_err" ] && echo "*** stderr log: $log_err ***" && cat $log_err
+    fi
+    echo
+    echo "$test_name: Failed with exit code $rc"
+    echo "********************************************************************************"
+    exit $rc
+fi
+
+$tester_cmd >$log_out 2>$log_err
+rc=$?
+
+if [ "$rc" -ne "0" ]; then
+    echo
+    echo "Actual and expected results differ. Command: $tester_cmd"
+    if [ ! -z "$log_dir" ]; then
+	    test -s "$log_out" && echo "*** stdout log: $log_out ***" && cat $log_out
+	    test -s "$log_err" && echo "*** stderr log: $log_err ***" && cat $log_err
+    fi
+    echo
+    echo "$test_name: Failed with exit code $rc"
+    echo "********************************************************************************"
+    exit $rc
+fi
+
+echo "$test_name: Finished successfully"
+echo "********************************************************************************"