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` — РІС‹ можете добавить СЃРІРѕРё флаги РІ конец + этого файла или РІ саму команду `make`. Например, чтобы собрать РєРѕРґ СЃ оптимизациями + Рё протестировать скорость выполнения, РІС‹ можете использовать `make CFLAGS=-O3`. + РЎ помощью `LDFLAGS` можно передать дополнительные параметры линковщику. +- Рспользуйте `make check`, чтобы проверить вашу программу через статический анализатор + `clang-tidy`. РЎРїРёСЃРѕРє проверок описан РІ файле `clang-tidy-checks.txt`. Р’С‹ можете добавить + СЃРІРѕРё проверки РІ конец этого файла или РІ параметр `CLANG_TIDY_CHECKS` для `make`. +- Р’С‹ можете собрать РєРѕРґ СЃ поддержкой определенных динамических анализаторов (санитайзеров). + Санитайзеры РјРѕРіСѓС‚ дать РїРѕРґСЂРѕР±РЅСѓСЋ информацию Рѕ возможных Рё реальных ошибках РІ программе вместо + классического сообщения Рѕ segmentation fault. Рсполняемые файлы Р±СѓРґСѓС‚ также размещены РІ директории `build`, РЅРѕ для санитайзеров выделены отдельные + поддиректории СЃ РёС… названием. + Поддерживаются следующие санитайзеры: + - `make SANITIZER=asan` — [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html), + набор проверок РЅР° некорректное использование адресов памяти. Примеры: + use-after-free, double-free, выход Р·Р° пределы стека, кучи или статического блока. + - `make SANITIZER=lsan` — [LeakSanitizer](https://clang.llvm.org/docs/LeakSanitizer.html), + проверки РЅР° утечки памяти. + - `make SANITIZER=msan` — [MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html), + проверяет, что любая используемая ячейка памяти проинициализирована РЅР° момент чтения РёР· нее. + - `make SANITIZER=usan` — [UndefinedBehaviourSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html), + набор базовых проверок РЅР° неопределенное поведение. Примеры: переполнение численного типа, + null-pointer dereference. + - `make SANITIZER=none` — собрать РєРѕРґ без санитайзеров (РїРѕ умолчанию). + - `make SANITIZER=all` — собрать 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 "********************************************************************************"