diff --git a/Makefile.am b/Makefile.am index 9f56159578..92963887be 100644 --- a/Makefile.am +++ b/Makefile.am @@ -86,13 +86,22 @@ libocispec/libocispec.la: libcrun_la_SOURCES = $(libcrun_SOURCES) libcrun_la_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -fvisibility=hidden -libcrun_la_LIBADD = libocispec/libocispec.la $(FOUND_LIBS) $(maybe_libyajl.la) +if ENABLE_COVERAGE +libcrun_la_CFLAGS += $(COVERAGE_CFLAGS) +libcrun_la_LDFLAGS = -Wl,--version-script=$(abs_top_srcdir)/libcrun.lds $(COVERAGE_LDFLAGS) +else libcrun_la_LDFLAGS = -Wl,--version-script=$(abs_top_srcdir)/libcrun.lds +endif +libcrun_la_LIBADD = libocispec/libocispec.la $(FOUND_LIBS) $(maybe_libyajl.la) # build a version with all the symbols visible for testing if BUILD_TESTS libcrun_testing_la_SOURCES = $(libcrun_SOURCES) libcrun_testing_la_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -fvisibility=default +if ENABLE_COVERAGE +libcrun_testing_la_CFLAGS += $(COVERAGE_CFLAGS) +libcrun_testing_la_LDFLAGS = $(COVERAGE_LDFLAGS) +endif libcrun_testing_la_LIBADD = libocispec/libocispec.la $(maybe_libyajl.la) endif @@ -137,16 +146,27 @@ dist-luarock: $(LUACRUN_ROCK) endif crun_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -D CRUN_LIBDIR="\"$(CRUN_LIBDIR)\"" +if ENABLE_COVERAGE +crun_CFLAGS += $(COVERAGE_CFLAGS) +endif crun_SOURCES = src/crun.c src/run.c src/delete.c src/kill.c src/pause.c src/unpause.c src/oci_features.c src/spec.c \ src/exec.c src/list.c src/create.c src/start.c src/state.c src/update.c src/ps.c \ src/checkpoint.c src/restore.c src/mounts.c src/run_create.c if DYNLOAD_LIBCRUN +if ENABLE_COVERAGE +crun_LDFLAGS = -Wl,--unresolved-symbols=ignore-all $(CRUN_LDFLAGS) $(COVERAGE_LDFLAGS) +else crun_LDFLAGS = -Wl,--unresolved-symbols=ignore-all $(CRUN_LDFLAGS) +endif else crun_LDADD = libcrun.la $(FOUND_LIBS) $(maybe_libyajl.la) +if ENABLE_COVERAGE +crun_LDFLAGS = $(CRUN_LDFLAGS) $(COVERAGE_LDFLAGS) +else crun_LDFLAGS = $(CRUN_LDFLAGS) endif +endif EXTRA_DIST = COPYING COPYING.libcrun README.md NEWS SECURITY.md rpm/crun.spec autogen.sh \ src/libcrun/blake3/blake3_impl.h src/libcrun/blake3/blake3.h \ @@ -187,6 +207,7 @@ TESTS_LDADD = libcrun_testing.la $(FOUND_LIBS) $(maybe_libyajl.la) tests_init_LDADD = tests_init_LDFLAGS = -static-libgcc -all-static +tests_init_CFLAGS = -g -O2 tests_init_SOURCES = tests/init.c tests_tests_libcrun_utils_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -I $(abs_top_builddir)/src -I $(abs_top_srcdir)/src @@ -338,4 +359,105 @@ clang-format: shellcheck: shellcheck autogen.sh build-aux/release.sh tests/run_all_tests.sh tests/*/*.sh contrib/*.sh -.PHONY: coverity sync generate-rust-bindings generate-signals.c generate-mount_flags.c clang-format shellcheck +# Code coverage targets +if ENABLE_COVERAGE + +# Clean coverage data +coverage-clean: + @rm -rf coverage-html coverage.info coverage.xml + @find . -name "*.gcda" -delete + @find . -name "*.gcno" -delete + +# Reset coverage counters +coverage-reset: + @if test -n "$(LCOV)"; then \ + $(LCOV) --zerocounters --directory .; \ + fi + +# Run tests and collect coverage data +coverage-check: coverage-reset + @echo "Running tests for coverage (single-threaded to avoid race conditions)..." + $(MAKE) -j1 check + @echo "Collecting coverage data..." + +# Generate HTML coverage report (preferred method with lcov) +coverage-html: coverage-check + @if test -n "$(LCOV)"; then \ + echo "Generating coverage report with lcov..."; \ + $(LCOV) --capture --directory . --output-file coverage.info; \ + $(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \ + genhtml coverage.info --output-directory coverage-html; \ + echo "Coverage report generated in coverage-html/index.html"; \ + elif test -n "$(GCOVR)"; then \ + echo "Generating coverage report with gcovr..."; \ + $(GCOVR) --html --html-details -o coverage.html \ + --exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \ + echo "Coverage report generated in coverage.html"; \ + else \ + echo "Generating coverage report with gcov..."; \ + mkdir -p coverage-html; \ + for src in $(libcrun_SOURCES) $(crun_SOURCES); do \ + if test -f "$${src}.gcno"; then \ + $(GCOV) -o . $$src || true; \ + fi; \ + done; \ + mv *.gcov coverage-html/ 2>/dev/null || true; \ + echo "Coverage files generated in coverage-html/"; \ + fi + +# Generate XML coverage report (for CI tools) +coverage-xml: coverage-check + @if test -n "$(GCOVR)"; then \ + echo "Generating XML coverage report with gcovr..."; \ + $(GCOVR) --xml -o coverage.xml \ + --exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \ + echo "Coverage report generated in coverage.xml"; \ + elif test -n "$(LCOV)"; then \ + echo "Generating XML coverage report with lcov..."; \ + $(LCOV) --capture --directory . --output-file coverage.info; \ + $(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \ + echo "Coverage data collected in coverage.info"; \ + else \ + echo "XML coverage requires gcovr or lcov"; \ + exit 1; \ + fi + +# Generate coverage summary +coverage-summary: coverage-check + @if test -n "$(LCOV)"; then \ + echo "Coverage summary (lcov):"; \ + $(LCOV) --capture --directory . --output-file coverage.info; \ + $(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \ + $(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \ + $(LCOV) --summary coverage.info; \ + elif test -n "$(GCOVR)"; then \ + echo "Coverage summary (gcovr):"; \ + $(GCOVR) --exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \ + else \ + echo "Coverage summary requires lcov or gcovr"; \ + fi + +else + +coverage-clean: + @echo "Coverage support not enabled. Reconfigure with --enable-coverage" + +coverage-reset coverage-check coverage-html coverage-xml coverage-summary: + @echo "Coverage support not enabled. Reconfigure with --enable-coverage" + +endif + +clean-local: coverage-clean + +# Coverage targets must not run in parallel due to race conditions in .gcda file writes +.NOTPARALLEL: coverage-reset coverage-check coverage-html coverage-xml coverage-summary + +.PHONY: coverity sync generate-rust-bindings generate-signals.c generate-mount_flags.c clang-format shellcheck coverage-clean coverage-reset coverage-check coverage-html coverage-xml coverage-summary diff --git a/configure.ac b/configure.ac index 3573b1a581..7bbe190f6f 100644 --- a/configure.ac +++ b/configure.ac @@ -366,6 +366,45 @@ if test -z "$GPERF"; then AC_MSG_NOTICE(gperf not found - cannot rebuild signal parser code) fi +dnl code coverage +AC_ARG_ENABLE([coverage], + AS_HELP_STRING([--enable-coverage], [Enable code coverage support]), + [enable_coverage=$enableval], [enable_coverage=no]) + +AS_IF([test "x$enable_coverage" = "xyes"], [ + AC_CHECK_TOOL([GCOV], [gcov]) + if test -z "$GCOV"; then + AC_MSG_ERROR([gcov is required for code coverage]) + fi + + AC_CHECK_TOOL([LCOV], [lcov]) + AC_CHECK_TOOL([GCOVR], [gcovr]) + + # Choose the best available coverage tool + if test -n "$LCOV"; then + coverage_tool=lcov + elif test -n "$GCOVR"; then + coverage_tool=gcovr + else + coverage_tool=gcov + fi + + AC_MSG_NOTICE([Using $coverage_tool for code coverage reporting]) + + # Add coverage flags + COVERAGE_CFLAGS="--coverage -g -O0 -fno-inline -fno-inline-small-functions -fno-default-inline" + COVERAGE_LDFLAGS="--coverage" + + AC_SUBST([COVERAGE_CFLAGS]) + AC_SUBST([COVERAGE_LDFLAGS]) + AC_SUBST([GCOV]) + AC_SUBST([LCOV]) + AC_SUBST([GCOVR]) + AC_SUBST([coverage_tool], [$coverage_tool]) +]) + +AM_CONDITIONAL([ENABLE_COVERAGE], [test "x$enable_coverage" = "xyes"]) + AC_SEARCH_LIBS([argp_parse], [argp], [], [AC_MSG_ERROR([*** argp functions not found - install libargp or argp_standalone])]) AM_CONDITIONAL([PYTHON_BINDINGS], [test "x$with_python_bindings" = "xyes"]) diff --git a/maint.mk b/maint.mk index c1fdf9ca2c..df25a6e646 100644 --- a/maint.mk +++ b/maint.mk @@ -1581,7 +1581,7 @@ init-coverage: lcov --directory . --zerocounters COVERAGE_CCOPTS ?= "-g --coverage" -COVERAGE_OUT ?= doc/coverage +COVERAGE_OUT ?= docs/coverage build-coverage: $(MAKE) $(AM_MAKEFLAGS) CFLAGS=$(COVERAGE_CCOPTS) CXXFLAGS=$(COVERAGE_CCOPTS) @@ -1594,7 +1594,10 @@ gen-coverage: genhtml --output-directory $(COVERAGE_OUT) \ $(COVERAGE_OUT)/$(PACKAGE).info \ --highlight --frames --legend \ - --title "$(PACKAGE_NAME)" + --title "$(PACKAGE_NAME)" \ + --ignore-errors unmapped + +.NOTPARALLEL: coverage init-coverage build-coverage gen-coverage coverage: $(MAKE) init-coverage diff --git a/tests/test_bpf_devices.py b/tests/test_bpf_devices.py index 7491d899fb..515c2dd986 100644 --- a/tests/test_bpf_devices.py +++ b/tests/test_bpf_devices.py @@ -47,27 +47,27 @@ def check_bpf_prerequisites(): """Check all prerequisites for BPF device tests. Returns 77 (skip) if not met, 0 if OK""" # Skip if not root if is_rootless(): - return 77 + return (77, "requires root privileges") # Skip if not cgroup v2 if not is_cgroup_v2_unified(): - return 77 + return (77, "requires cgroup v2") # Skip if systemd not available if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") # Skip if not running on systemd if not running_on_systemd(): - return 77 + return (77, "not running on systemd") # Skip if no BPF support if not has_bpf_fs(): - return 77 + return (77, "BPF filesystem not available") # Skip if systemd doesn't support BPFProgram if not systemd_supports_bpf_program(): - return 77 + return (77, "systemd BPFProgram not supported") return 0 @@ -86,7 +86,7 @@ def test_bpf_devices_systemd(): bpf_path = None try: # Run container with systemd cgroup manager. - _, cid = run_and_get_output(conf, command='run', detach=True, cgroup_manager="systemd") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True, cgroup_manager="systemd") # Get systemd scope. state = run_crun_command(['state', cid]) @@ -96,12 +96,12 @@ def test_bpf_devices_systemd(): output = subprocess.check_output(['systemctl', 'show', '-PBPFProgram', scope], close_fds=False).decode().strip() if output == "": - sys.stderr.write("# BPFProgram property not found or empty\n") + logger.info("BPFProgram property not found or empty") return -1 # Should look like "device:/sys/fs/bpf/crun/crun-xxx_scope". if "device:/sys/fs/bpf/crun/" not in output: - sys.stderr.write("# Bad BPFProgram property value: `%s`\n" % output) + logger.info("Bad BPFProgram property value: `%s`", prop_value) return -1 # Test 2: Check that BPF program file was created. @@ -109,7 +109,7 @@ def test_bpf_devices_systemd(): # Extract the path. bpf_path = output.split("device:", 1)[1] if not os.path.exists(bpf_path): - sys.stderr.write("# BPF program file `%s` not found\n" % bpf_path) + logger.info("BPF program file `%s` not found", prog_file) return -1 # Test 3: Check that BPF program is cleaned up. @@ -118,13 +118,13 @@ def test_bpf_devices_systemd(): run_crun_command(["delete", "-f", cid]) cid = None if os.path.exists(bpf_path): - sys.stderr.write("# BPF program `%s` still exist after crun delete\n" % bpf_path) + logger.info("BPF program `%s` still exist after crun delete", prog_file) return -1 return 0 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 349667c7e9..71df85c7b8 100755 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -28,13 +28,13 @@ def test_no_caps(): conf['process']['capabilities'] = {} for i in ['bounding', 'effective', 'inheritable', 'permitted', 'ambient']: conf['process']['capabilities'][i] = [] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) for i in ['CapInh', 'CapPrm', 'CapEff', 'CapBnd', 'CapAmb']: if proc_status.get(i, '') != "0000000000000000": actual = proc_status.get(i, 'MISSING') - sys.stderr.write("# %s capability check failed: expected '0000000000000000', got '%s'\n" % (i, actual)) + logger.info("%s capability check failed: expected '0000000000000000', got '%s'", i, actual) return -1 return 0 @@ -45,13 +45,13 @@ def test_some_caps(): conf['process']['capabilities'] = {} for i in ['bounding', 'effective', 'inheritable', 'permitted', 'ambient']: conf['process']['capabilities'][i] = [] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) for i in ['CapInh', 'CapPrm', 'CapEff', 'CapBnd', 'CapAmb']: if proc_status.get(i, '') != "0000000000000000": actual = proc_status.get(i, 'MISSING') - sys.stderr.write("# %s capability check failed: expected '0000000000000000', got '%s'\n" % (i, actual)) + logger.info("%s capability check failed: expected '0000000000000000', got '%s'", i, actual) return -1 return 0 @@ -63,13 +63,13 @@ def test_unknown_caps(): # unknown caps must be ignored for i in ['bounding', 'effective', 'inheritable', 'permitted', 'ambient']: conf['process']['capabilities'][i] = ['CAP_UNKNOWN', 'UNKNOWN_CAP'] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) for i in ['CapInh', 'CapPrm', 'CapEff', 'CapBnd', 'CapAmb']: if proc_status.get(i, '') != "0000000000000000": actual = proc_status.get(i, 'MISSING') - sys.stderr.write("# %s capability check failed (unknown caps should be ignored): expected '0000000000000000', got '%s'\n" % (i, actual)) + logger.info("%s capability check failed (unknown caps should be ignored): expected '0000000000000000', got '%s'", i, actual) return -1 return 0 @@ -79,28 +79,27 @@ def test_new_privs(): add_all_namespaces(conf) conf['process']['noNewPrivileges'] = True - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) no_new_privs = proc_status.get('NoNewPrivs', 'MISSING') if no_new_privs != "1": - sys.stderr.write("# noNewPrivileges=true test failed: expected '1', got '%s'\n" % no_new_privs) + logger.info("noNewPrivileges=true test failed: expected '1', got '%s'", no_new_privs) return -1 with open("/proc/self/status") as f: - host_proc_status = parse_proc_status("\n".join(f.readlines())) + host_proc_status = parse_proc_status(f.read()) host_no_new_privs = host_proc_status.get('NoNewPrivs', '0') # if nonewprivs is already set, it cannot be unset, so skip the # next test if host_no_new_privs == "1": - sys.stderr.write("# skipping noNewPrivileges=false test: host already has NoNewPrivs=1\n") - return 0 + return (77, "host already has NoNewPrivs=1") conf['process']['noNewPrivileges'] = False - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) no_new_privs = proc_status.get('NoNewPrivs', 'MISSING') if no_new_privs != "0": - sys.stderr.write("# noNewPrivileges=false test failed: expected '0', got '%s'\n" % no_new_privs) + logger.info("noNewPrivileges=false test failed: expected '0', got '%s'", no_new_privs) return -1 return 0 @@ -113,14 +112,14 @@ def helper_test_some_caps(uid, captypes, proc_name): conf['process']['capabilities'] = {} for i in captypes + ['bounding']: conf['process']['capabilities'][i] = ["CAP_SYS_ADMIN"] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) expected = "0000000000200000" actual = proc_status.get(proc_name, 'MISSING') if actual != expected: - sys.stderr.write("# %s capability check failed for uid %d with caps %s: expected '%s', got '%s'\n" % - (proc_name, uid, captypes, expected, actual)) + logger.info("%s capability check failed for uid %d with caps %s: expected '%s', got '%s'", + proc_name, uid, captypes, expected, actual) return -1 return 0 @@ -138,27 +137,27 @@ def test_some_caps_permitted(): def test_some_caps_effective_non_root(): if is_rootless(): - return 77 + return (77, "requires root privileges") return helper_test_some_caps(1000, ["effective", "permitted", "inheritable", "ambient"], 'CapEff') def test_some_caps_bounding_non_root(): if is_rootless(): - return 77 + return (77, "requires root privileges") return helper_test_some_caps(1000, ["bounding"], 'CapBnd') def test_some_caps_inheritable_non_root(): if is_rootless(): - return 77 + return (77, "requires root privileges") return helper_test_some_caps(1000, ["inheritable"], 'CapInh') def test_some_caps_ambient_non_root(): if is_rootless(): - return 77 + return (77, "requires root privileges") return helper_test_some_caps(1000, ["ambient", "permitted", "inheritable"], 'CapAmb') def test_some_caps_permitted_non_root(): if is_rootless(): - return 77 + return (77, "requires root privileges") return helper_test_some_caps(1000, ["ambient", "permitted", "inheritable"], 'CapPrm') diff --git a/tests/test_cwd.py b/tests/test_cwd.py index 7c4dc87e7e..fe80c23e35 100755 --- a/tests/test_cwd.py +++ b/tests/test_cwd.py @@ -22,7 +22,7 @@ def test_cwd(): conf['process']['args'] = ['/init', 'cwd'] conf['process']['cwd'] = "/var" add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "/var" not in out: return -1 return 0 @@ -33,7 +33,7 @@ def test_cwd_relative(): conf['process']['cwd'] = "/sbin" add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "hello" not in str(out): return -1 except Exception as e: @@ -46,7 +46,7 @@ def test_cwd_relative_subdir(): conf['process']['cwd'] = "/" add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "hello" not in str(out): return -1 except: @@ -59,7 +59,7 @@ def test_cwd_not_exist(): conf['process']['cwd'] = "/doesnotexist" add_all_namespaces(conf) try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except: return -1 return 0 @@ -70,7 +70,7 @@ def test_cwd_absolute(): conf['process']['cwd'] = "/sbin" add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "hello" not in str(out): return -1 except: diff --git a/tests/test_delete.py b/tests/test_delete.py index 76a396654b..d6509a1275 100755 --- a/tests/test_delete.py +++ b/tests/test_delete.py @@ -28,6 +28,11 @@ def test_simple_delete(): out, container_id = run_and_get_output(conf, detach=True, hide_stderr=True) if out != "": return -1 + + state = None + freezerCreated = False + cleanup_result = 0 + try: state = json.loads(run_crun_command(["state", container_id])) if state['status'] != "running": @@ -35,35 +40,36 @@ def test_simple_delete(): if state['id'] != container_id: return -1 finally: - freezerCreated=False - if not os.path.exists("/sys/fs/cgroup/cgroup.controllers") and os.access('/sys/fs/cgroup/freezer/', os.W_OK): - # cgroupv1 freezer can easily simulate stuck or breaking `crun delete -f ` - # this should be only done on cgroupv1 systems - if not os.path.exists("/sys/fs/cgroup/freezer/frozen/"): - freezerCreated=True - os.makedirs("/sys/fs/cgroup/freezer/frozen/") - with open('/sys/fs/cgroup/freezer/frozen/tasks', 'w') as f: - f.write(str(state['pid'])) - with open('/sys/fs/cgroup/freezer/frozen/freezer.state', 'w') as f: - f.write('FROZEN') - try: - output = run_crun_command_raw(["delete", "-f", container_id]) - except subprocess.CalledProcessError as exc: - print("Status : FAIL", exc.returncode, exc.output) - return -1 - else: - # this is expected for cgroup v1 so ignore - if not output or b'Device or resource busy' in output: - # if output is empty or expected error pass - pass + if state is not None: + if not os.path.exists("/sys/fs/cgroup/cgroup.controllers") and os.access('/sys/fs/cgroup/freezer/', os.W_OK): + # cgroupv1 freezer can easily simulate stuck or breaking `crun delete -f ` + # this should be only done on cgroupv1 systems + if not os.path.exists("/sys/fs/cgroup/freezer/frozen/"): + freezerCreated = True + os.makedirs("/sys/fs/cgroup/freezer/frozen/") + with open('/sys/fs/cgroup/freezer/frozen/tasks', 'w') as f: + f.write(str(state['pid'])) + with open('/sys/fs/cgroup/freezer/frozen/freezer.state', 'w') as f: + f.write('FROZEN') + try: + output = run_crun_command_raw(["delete", "-f", container_id]) + except subprocess.CalledProcessError as exc: + logger.error("Status : FAIL %s %s", exc.returncode, exc.output) + cleanup_result = -1 else: - # anything else is error - print(output) - return -1 + # this is expected for cgroup v1 so ignore + if not output or b'Device or resource busy' in output: + # if output is empty or expected error pass + pass + else: + # anything else is error + logger.error(output) + cleanup_result = -1 - if freezerCreated: - os.rmdir("/sys/fs/cgroup/freezer/frozen/") - return 0 + if freezerCreated: + os.rmdir("/sys/fs/cgroup/freezer/frozen/") + + return cleanup_result def test_multiple_containers_delete(): """Delete multiple containers with a regular expression""" @@ -77,6 +83,9 @@ def test_multiple_containers_delete(): out_test2, container_id_test2 = run_and_get_output(conf, detach=True, hide_stderr=True) if out_test2 != "": return -1 + + cleanup_result = 0 + try: state_test1 = json.loads(run_crun_command(["state", container_id_test1])) if state_test1['status'] != "running": @@ -92,9 +101,10 @@ def test_multiple_containers_delete(): try: output = run_crun_command_raw(["delete", "-f", "--regex", "test-*"]) except subprocess.CalledProcessError as exc: - print("Status : FAIL", exc.returncode, exc.output) - return -1 - return 0 + logger.error("Status : FAIL %s %s", exc.returncode, exc.output) + cleanup_result = -1 + + return cleanup_result def test_help_delete(): out = run_crun_command(["delete", "--help"]) diff --git a/tests/test_devices.py b/tests/test_devices.py index 3fdfac1b02..1a97302544 100755 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -22,7 +22,7 @@ def test_mode_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") # verify the umask doesn't affect the result os.umask(0o22) @@ -45,20 +45,20 @@ def test_mode_device(): conf['linux']['devices'] = [{"path": "/dev/foo", "type": "b", "major": 1, "minor": 5, "uid": 10, "gid": 11, "fileMode": 0o157},] try: expected = "157" - out = run_and_get_output(conf) + out = run_and_get_output(conf, hide_stderr=True) if expected not in out[0]: - sys.stderr.write("# device mode test failed with userns=%s: expected '%s' in output\n" % (have_userns, expected)) - sys.stderr.write("# actual output: %s\n" % out[0]) - sys.stderr.write("# device config: %s\n" % conf['linux']['devices'][0]) + logger.info("device mode test failed with userns=%s: expected '%s' in output", have_userns, expected) + logger.info("actual output: %s", out[0]) + logger.info("device config: %s", conf['linux']['devices']) return -1 except Exception as e: - sys.stderr.write("# device mode test failed with userns=%s: %s\n" % (have_userns, str(e))) + logger.info("device mode test failed with userns=%s: %s", have_userns, e) return -1 return 0 def test_owner_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") for have_userns in [True, False]: conf = base_config() @@ -78,25 +78,25 @@ def test_owner_device(): conf['linux']['devices'] = [{"path": "/dev/foo", "type": "b", "major": 1, "minor": 5, "uid": 10, "gid": 11},] try: expected = "10:11" - out = run_and_get_output(conf) + out = run_and_get_output(conf, hide_stderr=True) if expected not in out[0]: - sys.stderr.write("# device owner test failed with userns=%s: expected '%s' in output\n" % (have_userns, expected)) - sys.stderr.write("# actual output: %s\n" % out[0]) - sys.stderr.write("# device config: %s\n" % conf['linux']['devices'][0]) + logger.info("device owner test failed with userns=%s: expected '%s' in output", have_userns, expected) + logger.info("actual output: %s", out) + logger.info("device config: %s", conf["linux"]["devices"]) return -1 except Exception as e: - sys.stderr.write("# device owner test failed with userns=%s: %s\n" % (have_userns, str(e))) + logger.info("device owner test failed with userns=%s: %s", have_userns, e) return -1 return 0 def test_deny_devices(): if is_rootless(): - return 77 + return (77, "requires root privileges") try: os.stat("/dev/fuse") except: - return 77 + return (77, "/dev/fuse device not available") conf = base_config() add_all_namespaces(conf) @@ -123,7 +123,7 @@ def test_create_or_bind_mount_device(): try: os.stat("/dev/fuse") except: - return 77 + return (77, "/dev/fuse device not available") conf = base_config() add_all_namespaces(conf) @@ -137,21 +137,21 @@ def test_create_or_bind_mount_device(): "gid": 0 }] try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: - sys.stderr.write("# " + str(e) + "\n") + logger.info("# %s", str(e)) return -1 return 0 def test_allow_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") try: os.stat("/dev/fuse") except: - return 77 + return (77, "/dev/fuse device not available") conf = base_config() add_all_namespaces(conf) @@ -169,19 +169,19 @@ def test_allow_device(): } conf['mounts'].append(dev) try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: return -1 return 0 def test_allow_access(): if is_rootless(): - return 77 + return (77, "requires root privileges") try: os.stat("/dev/fuse") except: - return 77 + return (77, "/dev/fuse device not available") conf = base_config() add_all_namespaces(conf) @@ -199,14 +199,14 @@ def test_allow_access(): } conf['mounts'].append(dev) try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: return -1 return 0 def test_mknod_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf) @@ -214,33 +214,33 @@ def test_mknod_device(): conf['linux']['devices'] = [{"path": "/foo-dev", "type": "b", "major": 10, "minor": 229}, {"path": "/subdir/foo-dev", "type": "b", "major": 10, "minor": 229},] try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: return -1 return 0 def test_trailing_slash_mknod_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf) conf['process']['args'] = ['/init', 'true'] conf['linux']['devices'] = [{"path": "/mnt/", "type": "b", "major": 10, "minor": 229}] try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: return -1 return 0 def test_net_devices(): if is_rootless(): - return 77 + return (77, "requires root privileges") ip_path = shutil.which("ip") if ip_path is None: - sys.stderr.write("# ip command not found\n") - return 77 + logger.info("ip command not found") + return (77, "ip command not found") current_netns = os.open("/proc/self/ns/net", os.O_RDONLY) try: @@ -248,20 +248,20 @@ def test_net_devices(): for specify_broadcast in [True, False]: for specify_name in [True, False]: - sys.stderr.write("# test_net_devices: creating testdevice with specify_broadcast=%s, specify_name=%s\n" % (specify_broadcast, specify_name)) + logger.info("test_net_devices: creating testdevice with specify_broadcast=%s, specify_name=%s", specify_broadcast, specify_name) result = subprocess.run(["ip", "link", "add", "testdevice", "type", "dummy"], capture_output=True, text=True) if result.returncode != 0: - sys.stderr.write("# ip link add failed: %s\n" % result.stderr) + logger.info("ip link add failed: %s", result.stderr) return -1 if specify_broadcast: result = subprocess.run(["ip", "addr", "add", "10.1.2.3/24", "brd", "10.1.2.254", "dev", "testdevice"], capture_output=True, text=True) if result.returncode != 0: - sys.stderr.write("# ip addr add with broadcast failed: %s\n" % result.stderr) + logger.info("ip addr add with broadcast failed: %s", result.stderr) return -1 else: result = subprocess.run(["ip", "addr", "add", "10.1.2.3/24", "dev", "testdevice"], capture_output=True, text=True) if result.returncode != 0: - sys.stderr.write("# ip addr add without broadcast failed: %s\n" % result.stderr) + logger.info("ip addr add without broadcast failed: %s", result.stderr) return -1 conf = base_config() @@ -305,25 +305,25 @@ def test_net_devices(): } try: - out = run_and_get_output(conf) - sys.stderr.write("# test_net_devices: specify_broadcast=%s, specify_name=%s\n" % (specify_broadcast, specify_name)) - sys.stderr.write("# test_net_devices: output: %s\n" % repr(out[0])) + out = run_and_get_output(conf, hide_stderr=True) + logger.info("test_net_devices: specify_broadcast=%s, specify_name=%s", specify_broadcast, specify_name) + logger.info("test_net_devices: output: %s", out[0]) if "address: 10.1.2.3" not in out[0]: - sys.stderr.write("# address not found in output\n") - sys.stderr.write("# full output: %s\n" % repr(out[0])) + logger.info("address not found in output") + logger.info("full output: %s", out[0]) return 1 if specify_broadcast: if "broadcast: 10.1.2.254" not in out[0]: - sys.stderr.write("# broadcast address not found in output\n") - sys.stderr.write("# full output: %s\n" % repr(out[0])) + logger.info("broadcast address not found in output") + logger.info("full output: %s", out[0]) return 1 else: if "broadcast" in out[0]: - sys.stderr.write("# broadcast address found in output when it shouldn't be\n") - sys.stderr.write("# full output: %s\n" % repr(out[0])) + logger.info("broadcast address found in output when it shouldn't be") + logger.info("full output: %s", out[0]) return 1 except Exception as e: - sys.stderr.write("# test_net_devices exception: %s\n" % str(e)) + logger.info("test_net_devices exception: %s", e) return -1 finally: # Clean up the test device @@ -336,7 +336,7 @@ def test_net_devices(): def test_mknod_fifo_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf) @@ -345,15 +345,15 @@ def test_mknod_fifo_device(): {"path": "/dev/testfifo", "type": "p", "fileMode": 0o0660, "uid": 1, "gid": 2} ] try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: - sys.stderr.write("# test_mknod_fifo_device failed: %s\n" % e) + logger.info("test_mknod_fifo_device failed: %s", e) return -1 return 0 def test_mknod_char_device(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf) @@ -362,15 +362,15 @@ def test_mknod_char_device(): {"path": "/dev/testchar", "type": "c", "major": 251, "minor": 1, "fileMode": 0o0640, "uid": 3, "gid": 4} ] try: - run_and_get_output(conf) + run_and_get_output(conf, hide_stderr=True) except Exception as e: - sys.stderr.write("# test_mknod_char_device failed: {e}\n") + logger.info("test_mknod_char_device failed: {e}") return -1 return 0 def test_allow_device_read_only(): if is_rootless(): - return 77 + return (77, "requires root privileges") try: # Best effort load @@ -381,7 +381,7 @@ def test_allow_device_read_only(): st = os.stat("/dev/nullb0") major, minor = os.major(st.st_rdev), os.minor(st.st_rdev) except: - return 77 + return (77, "/dev/nullb0 device not available") conf = base_config() add_all_namespaces(conf) @@ -404,20 +404,20 @@ def test_allow_device_read_only(): try: run_and_get_output(conf) except Exception as e: - sys.stderr.write("# test_allow_device_read_only failed: %s\n" % e) + logger.info("test_allow_device_read_only failed: %s", e) return -1 conf['process']['args'] = ['/init', 'openwronly', '/dev/controlledchar'] try: run_and_get_output(conf) - sys.stderr.write("# test_allow_device_read_only: write access was unexpectedly allowed.\n") + logger.info("test_allow_device_read_only: write access was unexpectedly allowed.") return 1 except Exception as e: output_str = getattr(e, 'output', b'').decode(errors='ignore') if "Operation not permitted" in output_str or "Permission denied" in output_str: return 0 else: - sys.stderr.write("# test_allow_device_read_only (write attempt) failed with: %s, output: %s\n" % (e, output_str)) + logger.info("test_allow_device_read_only (write attempt) failed with: %s, output: %s", e, output_str) return 1 return 1 diff --git a/tests/test_domainname.py b/tests/test_domainname.py index 03ebafae43..328ca7c927 100755 --- a/tests/test_domainname.py +++ b/tests/test_domainname.py @@ -22,7 +22,7 @@ def test_domainname(): conf['process']['args'] = ['/init', 'getdomainname'] conf['domainname'] = "foomachine" add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "foomachine" not in out: return -1 conf = base_config() @@ -32,10 +32,10 @@ def test_domainname(): # in both of the above situation the test should pass. Anything other than this # must be considered as failure. try: - out, cid = run_and_get_output(conf) + out, cid = run_and_get_output(conf, hide_stderr=True) if out == "(none)\n": return 0 - sys.stderr.write("# unexpected success\n") + logger.info("unexpected success") return -1 except: return 0 @@ -53,10 +53,10 @@ def test_domainname_conflict_sysctl(): conf['linux']['sysctl'] = {'kernel.domainname' : 'foo'} cid = None try: - out, cid = run_and_get_output(conf) + out, cid = run_and_get_output(conf, hide_stderr=True) if out == "(none)\n": return 0 - sys.stderr.write("# unexpected success\n") + logger.info("unexpected success") return -1 except: return 0 @@ -75,7 +75,7 @@ def test_domainname_with_sysctl(): conf['linux']['sysctl'] = {'kernel.domainname' : 'foo'} cid = None try: - out, cid = run_and_get_output(conf) + out, cid = run_and_get_output(conf, hide_stderr=True) if out == "(none)\n": return 0 return 0 diff --git a/tests/test_exec.py b/tests/test_exec.py index 75429f3aa8..a95b360dc6 100755 --- a/tests/test_exec.py +++ b/tests/test_exec.py @@ -30,24 +30,24 @@ def test_exec(): add_all_namespaces(conf) cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) out = run_crun_command(["exec", cid, "/init", "echo", "foo"]) if "foo" not in out: - sys.stderr.write("# exec test failed: expected 'foo' in output\n") - sys.stderr.write("# container ID: %s\n" % cid) - sys.stderr.write("# actual output: %s\n" % out) + logger.info("exec test failed: expected 'foo' in output") + logger.info("container ID: %s", cid) + logger.info("actual output: %s", out) return -1 except Exception as e: - sys.stderr.write("# exec test failed with exception: %s\n" % str(e)) + logger.info("exec test failed with exception: %s", e) if cid is not None: - sys.stderr.write("# container ID: %s\n" % cid) + logger.info("container ID: %s", cid) raise finally: if cid is not None: try: run_crun_command(["delete", "-f", cid]) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup container %s: %s\n" % (cid, str(cleanup_e))) + logger.info("warning: failed to cleanup container %s: %s", cid, cleanup_e) return 0 def test_uid_tty(): @@ -67,7 +67,7 @@ def test_uid_tty(): last_error = None try: cid = "container-%s" % os.getpid() - proc = run_and_get_output(conf, command='run', id_container=cid, use_popen=True) + proc = run_and_get_output(conf, hide_stderr=True, command='run', id_container=cid, use_popen=True) for i in range(0, 500): try: out = run_crun_command(["exec", "-t", "--user", "1", cid, "/init", "owner", "/proc/self/fd/0"]) @@ -79,17 +79,17 @@ def test_uid_tty(): pass time.sleep(0.01) if ret != 0: - sys.stderr.write("# uid_tty test failed after 500 attempts\n") - sys.stderr.write("# container ID: %s\n" % cid) + logger.info("uid_tty test failed after 500 attempts") + logger.info("container ID: %s", cid) if last_error: - sys.stderr.write("# last error: %s\n" % str(last_error)) + logger.info("last error: %s", e) return ret finally: if cid is not None: try: run_crun_command(["delete", "-f", cid]) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup container %s: %s\n" % (cid, str(cleanup_e))) + logger.info("warning: failed to cleanup container %s: %s", cid, cleanup_e) return 0 def test_exec_root_netns_with_userns(): @@ -102,7 +102,7 @@ def test_exec_root_netns_with_userns(): conf['linux']['namespaces'].append({"type" : "network", "path" : "/proc/1/ns/net"}) cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) with open("/proc/net/route") as f: payload = f.read() @@ -113,9 +113,9 @@ def test_exec_root_netns_with_userns(): container_routes = [i.split('\t')[0] for i in out.split('\n')[1:] if i.strip()] if len(container_routes) != len(host_routes): - sys.stderr.write("# network namespace test failed: different route count\n") - sys.stderr.write("# host routes (%d): %s\n" % (len(host_routes), host_routes)) - sys.stderr.write("# container routes (%d): %s\n" % (len(container_routes), container_routes)) + logger.info("network namespace test failed: different route count") + logger.info("host routes (%d): %s", len(host_routes), host_routes) + logger.info("container routes (%d): %s", len(container_routes), container_routes) return -1 host_routes.sort() @@ -123,23 +123,23 @@ def test_exec_root_netns_with_userns(): for i, (container_route, host_route) in enumerate(zip(container_routes, host_routes)): if container_route != host_route: - sys.stderr.write("# network namespace test failed: route mismatch at index %d\n" % i) - sys.stderr.write("# expected (host): %s\n" % host_route) - sys.stderr.write("# actual (container): %s\n" % container_route) - sys.stderr.write("# full host routes: %s\n" % host_routes) - sys.stderr.write("# full container routes: %s\n" % container_routes) + logger.info("network namespace test failed: route mismatch at index %d", i) + logger.info("expected (host): %s", host_route) + logger.info("actual (container): %s", container_route) + logger.info("full host routes: %s", host_routes) + logger.info("full container routes: %s", container_routes) return -1 except Exception as e: - sys.stderr.write("# network namespace test failed with exception: %s\n" % str(e)) + logger.info("network namespace test failed with exception: %s", e) if cid is not None: - sys.stderr.write("# container ID: %s\n" % cid) + logger.info("container ID: %s", cid) raise finally: if cid is not None: try: run_crun_command(["delete", "-f", cid]) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup container %s: %s\n" % (cid, str(cleanup_e))) + logger.info("warning: failed to cleanup container %s: %s", cid, cleanup_e) return 0 def test_exec_not_exists_helper(detach): @@ -148,7 +148,7 @@ def test_exec_not_exists_helper(detach): add_all_namespaces(conf) cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) try: if detach: out = run_crun_command(["exec", "-d", cid, "/not.here"]) @@ -176,7 +176,7 @@ def test_exec_additional_gids(): cid = None tempdir = tempfile.mkdtemp() try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) process_file = os.path.join(tempdir, "process.json") with open(process_file, "w") as f: @@ -216,7 +216,7 @@ def test_exec_populate_home_env_from_process_uid(): cid = None tempdir = tempfile.mkdtemp() try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) process_file = os.path.join(tempdir, "process.json") with open(process_file, "w") as f: @@ -274,7 +274,7 @@ def test_exec_add_capability(): "CAP_KILL": cap_kill_dict, \ "CAP_SYS_ADMIN": cap_sys_admin_dict} try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) for cap, value in cap_dict.items(): out = run_crun_command(["exec", "--cap", cap, cid, "/init", "cat", "/proc/self/status"]) for i in ['bounding', 'effective', 'inheritable', 'permitted', 'ambient']: @@ -300,7 +300,7 @@ def test_exec_add_env(): env_dict_orig = {"HOME":"/", "PATH":"/bin"} env_dict_new = {"HOME":"/tmp", "PATH":"/usr/bin","FOO":"BAR"} try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) # check original environment variable for env, value in env_dict_orig.items(): out = run_crun_command(["exec", cid, "/init", "printenv", env]) @@ -338,7 +338,7 @@ def test_exec_set_user(): uid_gid_list = ["1000:1000", "0:0", "65535:65535"] try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) # check current user id out = run_crun_command(["exec", cid, "/init", "id"]) if uid_gid_list[1] not in out: @@ -361,7 +361,7 @@ def test_exec_no_new_privs(): conf['process']['capabilities'] = {} cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) # check original value of NoNewPrivs out = run_crun_command(["exec", cid, "/init", "cat", "/proc/self/status"]) proc_status = parse_proc_status(out) @@ -386,7 +386,7 @@ def test_exec_write_pid_file(): cid = None tempdir = tempfile.mkdtemp() try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) pid_file = os.path.join(tempdir, cid) out = run_crun_command(["exec", "--pid-file", pid_file, cid, "/init", "echo", "hello"]) if "hello" not in out: @@ -449,26 +449,26 @@ def exec_and_get_affinity_mask(cid, exec_cpu_affinity=None): try: with open("/proc/self/status") as f: current_cpu_mask = cpu_mask_from_proc_status(f.read()) - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) mask = exec_and_get_affinity_mask(cid) if mask != current_cpu_mask: - sys.stderr.write("# current cpu mask %s != %s\n" % (current_cpu_mask, mask)) + logger.info("current cpu mask %s != %s", mask, current_cpu_mask) return -1 mask = exec_and_get_affinity_mask(cid, {"initial" : "0-1"}) if mask != "0-1": - sys.stderr.write("# cpu mask %s != 0-1\n" % mask) + logger.info("cpu mask %s != 0-1", mask) return -1 mask = exec_and_get_affinity_mask(cid, {"final" : "0-2"}) if mask != "0-2": - sys.stderr.write("# cpu mask %s != 0-2\n" % mask) + logger.info("cpu mask %s != 0-2", mask) return -1 mask = exec_and_get_affinity_mask(cid, {"initial" : "1", "final" : "0-3"}) if mask != "0-3": - sys.stderr.write("# cpu mask %s != 0-2\n" % mask) + logger.info("cpu mask %s != 0-3", mask) return -1 return 0 finally: @@ -483,7 +483,7 @@ def test_exec_getpgrp(): conf['process']['args'] = ['/init', 'pause'] cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) for terminal in [True, False]: if terminal and os.isatty(1) == False: continue @@ -491,7 +491,7 @@ def test_exec_getpgrp(): out = run_crun_command([x for x in cmdline if x is not None]) pgrp = int(out.split("\n")[0]) if pgrp <= 0: - sys.stderr.write("# invalid pgrp, got %d\n" % pgrp) + logger.info("invalid pgrp, got %d", pgrp) return -1 finally: if cid is not None: @@ -512,7 +512,7 @@ def test_exec_error_propagation(): add_all_namespaces(conf) cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) try: out = run_crun_command_raw(["exec", "--cwd", "/invalid/nonexistent/path", cid, "/init", "echo", "test"]) return -1 @@ -522,12 +522,12 @@ def test_exec_error_propagation(): has_read_pipe_error = "read pipe failed" in error_msg if has_chdir_error and has_read_pipe_error: - sys.stderr.write("# exec error propagation test failed: both chdir and read pipe errors detected\n") - sys.stderr.write("# error message: %s\n" % error_msg) + logger.info("exec error propagation test failed: both chdir and read pipe errors detected") + logger.info("error message: %s", error_output) return -1 if not has_chdir_error: - sys.stderr.write("# exec error propagation test failed: expected chdir error but got: %s\n" % error_msg) + logger.info("exec error propagation test failed: expected chdir error but got: %s", error_msg) return -1 return 0 @@ -536,7 +536,7 @@ def test_exec_error_propagation(): try: run_crun_command(["delete", "-f", cid]) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup container %s: %s\n" % (cid, str(cleanup_e))) + logger.info("warning: failed to cleanup container %s: %s", cid, cleanup_e) return 0 all_tests = { diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 1ce18f1ac7..7bbf6c5106 100755 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -23,7 +23,7 @@ def test_fail_prestart(): conf['hooks'] = {"prestart" : [{"path" : "/bin/false"}]} add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) except: return 0 return -1 @@ -33,7 +33,7 @@ def test_success_prestart(): conf['hooks'] = {"prestart" : [{"path" : "/bin/true"}]} add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) except: return -1 return 0 @@ -46,9 +46,8 @@ def test_hook_env_inherit(): conf['hooks'] = {"prestart" : [hook]} add_all_namespaces(conf) - print(conf['hooks']) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) except: return -1 return 0 @@ -60,9 +59,8 @@ def test_hook_env_no_inherit(): conf['hooks'] = {"prestart" : [hook]} add_all_namespaces(conf) - print(conf['hooks']) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) except: return -1 return 0 diff --git a/tests/test_hostname.py b/tests/test_hostname.py index 56c3e44fff..cd221d8e97 100755 --- a/tests/test_hostname.py +++ b/tests/test_hostname.py @@ -24,15 +24,15 @@ def test_hostname(): conf['hostname'] = expected_hostname add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if expected_hostname not in out: - sys.stderr.write("# hostname test failed: expected '%s' in output\n" % expected_hostname) - sys.stderr.write("# actual output: %s\n" % out.strip()) + logger.info("hostname test failed: expected '%s' in output", expected_hostname) + logger.info("actual output: %s", out) return -1 return 0 except Exception as e: - sys.stderr.write("# hostname test failed with exception: %s\n" % str(e)) - sys.stderr.write("# expected hostname: %s\n" % expected_hostname) + logger.info("hostname test failed with exception: %s", e) + logger.info("expected hostname: %s", expected_hostname) return -1 all_tests = { diff --git a/tests/test_limits.py b/tests/test_limits.py index 1ac81ec63f..ffb6579c04 100755 --- a/tests/test_limits.py +++ b/tests/test_limits.py @@ -21,52 +21,52 @@ def test_limit_pid_minus_1(): conf = base_config() add_all_namespaces(conf) if is_rootless(): - return 77 + return (77, "requires root privileges") conf['process']['args'] = ['/init', 'cat', '/dev/null'] conf['linux']['resources'] = {"pids" : {"limit" : -1}} try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if len(out) == 0: return 0 - sys.stderr.write("# PID limit -1 test failed: expected empty output\n") - sys.stderr.write("# actual output length: %d\n" % len(out)) - sys.stderr.write("# output: %s\n" % out[:100]) + logger.info("PID limit -1 test failed: expected empty output") + logger.info("actual output length: %d", len(out)) + logger.info("output: %s", out) return -1 except Exception as e: - sys.stderr.write("# PID limit -1 test failed with exception: %s\n" % str(e)) + logger.info("PID limit -1 test failed with exception: %s", e) return -1 def test_limit_pid_0(): conf = base_config() add_all_namespaces(conf) if is_rootless(): - return 77 + return (77, "requires root privileges") conf['process']['args'] = ['/init', 'cat', '/dev/null'] conf['linux']['resources'] = {"pids" : {"limit" : 0}} try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if len(out) == 0: return 0 - sys.stderr.write("# PID limit 0 test failed: expected empty output\n") - sys.stderr.write("# actual output length: %d\n" % len(out)) - sys.stderr.write("# output: %s\n" % out[:100]) + logger.info("PID limit 0 test failed: expected empty output") + logger.info("actual output length: %d", len(out)) + logger.info("output: %s", out) return -1 except Exception as e: - sys.stderr.write("# PID limit 0 test failed with exception: %s\n" % str(e)) + logger.info("PID limit 0 test failed with exception: %s", e) return -1 def test_limit_pid_n(): conf = base_config() if is_rootless(): - return 77 + return (77, "requires root privileges") add_all_namespaces(conf) conf['process']['args'] = ['/init', 'forkbomb', '20'] pid_limit = 10 conf['linux']['resources'] = {"pids" : {"limit" : pid_limit}} try: out, _ = run_and_get_output(conf) - sys.stderr.write("# PID limit %d test failed: expected fork bomb to be limited but command succeeded\n" % pid_limit) - sys.stderr.write("# output: %s\n" % out[:200]) + logger.info("PID limit %d test failed: expected fork bomb to be limited but command succeeded", pid_limit) + logger.info("output: %s", out) return -1 except Exception as e: error_output = "" @@ -74,10 +74,10 @@ def test_limit_pid_n(): error_output = e.output.decode('utf-8', errors='ignore') if "fork: Resource temporarily unavailable" in error_output: return 0 - sys.stderr.write("# PID limit %d test failed: expected 'fork: Resource temporarily unavailable' error\n" % pid_limit) - sys.stderr.write("# actual error: %s\n" % str(e)) + logger.info("PID limit %d test failed: expected 'fork: Resource temporarily unavailable' error", pid_limit) + logger.info("actual error: %s", e.output) if error_output: - sys.stderr.write("# error output: %s\n" % error_output[:200]) + logger.info("error output: %s", e.output) return -1 all_tests = { diff --git a/tests/test_mempolicy.py b/tests/test_mempolicy.py index 367fe8675a..4210932d9c 100755 --- a/tests/test_mempolicy.py +++ b/tests/test_mempolicy.py @@ -30,16 +30,18 @@ def check_numa_hw(): def check_mempolicy_prerequisites(need_interleave=False): """Check all prerequisites for numa mempolicy tests. Returns 77 (skip) if not met, 0 if OK""" if not check_numa_hw(): - sys.stderr.write("# numa missing\n") - return 77 + logger.info("numa missing") + return (77, "NUMA hardware not available") if need_interleave and not check_numa_interleave(): - sys.stderr.write("# interleave missing\n") - return 77 + logger.info("interleave missing") + return (77, "NUMA interleave not supported") + return 0 def test_mempolicy_no_conf(): """Test numa mempolicy without configuration""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -48,8 +50,8 @@ def test_mempolicy_no_conf(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -61,8 +63,9 @@ def test_mempolicy_no_conf(): def test_mempolicy_bad_mode(): """Test numa mempolicy with bad mode""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -71,8 +74,8 @@ def test_mempolicy_bad_mode(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -84,8 +87,9 @@ def test_mempolicy_bad_mode(): def test_mempolicy_bad_flag(): """Test numa mempolicy with bad flag""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -94,8 +98,8 @@ def test_mempolicy_bad_flag(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -107,8 +111,9 @@ def test_mempolicy_bad_flag(): def test_mempolicy_numa_balancing_flag(): """Test numa mempolicy preferred with numa_balancing flag""" - if check_mempolicy_prerequisites(need_interleave=True): - return 77 + prereq_result = check_mempolicy_prerequisites(need_interleave=True) + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -117,8 +122,8 @@ def test_mempolicy_numa_balancing_flag(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -130,8 +135,9 @@ def test_mempolicy_numa_balancing_flag(): def test_mempolicy_static_relative_nodes_flags(): """Test numa mempolicy preferred with numa_balancing flag""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -140,8 +146,8 @@ def test_mempolicy_static_relative_nodes_flags(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -153,8 +159,9 @@ def test_mempolicy_static_relative_nodes_flags(): def test_mempolicy_no_nodes(): """Test numa mempolicy without nodes configuration""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -163,8 +170,8 @@ def test_mempolicy_no_nodes(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -176,8 +183,9 @@ def test_mempolicy_no_nodes(): def test_mempolicy_bad_nodes_string(): """Test numa mempolicy without nodes configuration""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -186,8 +194,8 @@ def test_mempolicy_bad_nodes_string(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -199,8 +207,9 @@ def test_mempolicy_bad_nodes_string(): def test_mempolicy_bad_nodes_number(): """Test numa mempolicy without nodes configuration""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -209,8 +218,8 @@ def test_mempolicy_bad_nodes_number(): cid = None try: - _, cid = run_and_get_output(conf, command='run') - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run') + logger.info("unexpected success") return -1 except: pass @@ -222,8 +231,9 @@ def test_mempolicy_bad_nodes_number(): def test_mempolicy_default_mode(): """Test numa mempolicy default mode""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -232,13 +242,13 @@ def test_mempolicy_default_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " default " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' default ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' default ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -248,8 +258,9 @@ def test_mempolicy_default_mode(): def test_mempolicy_local_mode(): """Test numa mempolicy local mode""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -258,13 +269,13 @@ def test_mempolicy_local_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " local " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' local ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' local ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -274,8 +285,9 @@ def test_mempolicy_local_mode(): def test_mempolicy_bind_mode(): """Test numa mempolicy bind mode""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -284,13 +296,13 @@ def test_mempolicy_bind_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " bind:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' bind:0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' bind:0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -300,8 +312,9 @@ def test_mempolicy_bind_mode(): def test_mempolicy_bind_mode_balancing(): """Test numa mempolicy bind mode balancing""" - if check_mempolicy_prerequisites(need_interleave=True): - return 77 + prereq_result = check_mempolicy_prerequisites(need_interleave=True) + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -310,13 +323,13 @@ def test_mempolicy_bind_mode_balancing(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " bind=balancing:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' bind=balancing:0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' bind=balancing:0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -326,8 +339,9 @@ def test_mempolicy_bind_mode_balancing(): def test_mempolicy_bind_mode_balancing_relative(): """Test numa mempolicy bind mode balancing with relative nodes""" - if check_mempolicy_prerequisites(need_interleave=True): - return 77 + prereq_result = check_mempolicy_prerequisites(need_interleave=True) + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -336,13 +350,13 @@ def test_mempolicy_bind_mode_balancing_relative(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " bind=relative|balancing:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' bind=relative|balancing:0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' bind=relative|balancing:0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -352,8 +366,9 @@ def test_mempolicy_bind_mode_balancing_relative(): def test_mempolicy_preferred_mode_static(): """Test numa mempolicy preferred mode with static nodes""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -362,13 +377,13 @@ def test_mempolicy_preferred_mode_static(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " prefer=static:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' prefer=static:0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' prefer=static:0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -378,8 +393,9 @@ def test_mempolicy_preferred_mode_static(): def test_mempolicy_preferred_many_mode(): """Test numa mempolicy preferred many mode with all nodes""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -388,13 +404,13 @@ def test_mempolicy_preferred_many_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " prefer (many):0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' prefer (many):0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' prefer (many):0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -404,8 +420,9 @@ def test_mempolicy_preferred_many_mode(): def test_mempolicy_interleave_mode(): """Test numa mempolicy interleave mode""" - if check_mempolicy_prerequisites(): - return 77 + prereq_result = check_mempolicy_prerequisites() + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -414,13 +431,13 @@ def test_mempolicy_interleave_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " interleave:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' interleave:0 ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' interleave:0 ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: @@ -430,8 +447,9 @@ def test_mempolicy_interleave_mode(): def test_mempolicy_weighted_interleave_mode(): """Test numa mempolicy weighted interleave mode""" - if check_mempolicy_prerequisites(need_interleave=True): - return 77 + prereq_result = check_mempolicy_prerequisites(need_interleave=True) + if prereq_result != 0: + return prereq_result conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/numa_maps'] @@ -440,13 +458,13 @@ def test_mempolicy_weighted_interleave_mode(): cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if " weighted interleave:0 " not in out.splitlines()[1]: - sys.stderr.write("# Unable to find ' weighted interleave ' in /proc/self/numa_maps\n") - sys.stderr.write(out) + logger.info("Unable to find ' weighted interleave ' in /proc/self/numa_maps") + logger.info(out) return -1 except Exception as e: - sys.stderr.write("# Test failed with exception: %s\n" % str(e)) + logger.info("Test failed with exception: %s", e) return -1 finally: if cid is not None: diff --git a/tests/test_mounts.py b/tests/test_mounts.py index 2acc49cb5d..01702d461a 100755 --- a/tests/test_mounts.py +++ b/tests/test_mounts.py @@ -18,6 +18,10 @@ import sys import copy import socket +import os +import shutil +import subprocess +import json from tests_utils import * import tempfile import re @@ -52,14 +56,14 @@ def helper_mount(options: str, tmpfs: bool = True, userns: bool = False, is_file target = '/var/file' if is_file else '/var/dir' m = t.find_target(target) if m is None: - sys.stderr.write("# helper_mount failed: mount target '%s' not found in mountinfo\n" % target) - sys.stderr.write("# mount options: %s, tmpfs=%s, userns=%s, is_file=%s\n" % (options, tmpfs, userns, is_file)) - sys.stderr.write("# mountinfo output: %s\n" % out[:300]) + logger.info("helper_mount failed: mount target '%s' not found in mountinfo", target) + logger.info("mount options: %s, tmpfs=%s, userns=%s, is_file=%s", options, tmpfs, userns, is_file) + logger.info("mountinfo output: %s", out) return [None, None] return [m.vfs_options, m.fs_options] except Exception as e: - sys.stderr.write("# helper_mount failed with exception: %s\n" % str(e)) - sys.stderr.write("# mount options: %s, tmpfs=%s, userns=%s, is_file=%s\n" % (options, tmpfs, userns, is_file)) + logger.info("helper_mount failed with exception: %s", e) + logger.info("mount options: %s, tmpfs=%s, userns=%s, is_file=%s", options, tmpfs, userns, is_file) return [None, None] def test_mount_symlink(): @@ -71,8 +75,8 @@ def test_mount_symlink(): out, _ = run_and_get_output(conf, hide_stderr=True) if "Rome" in out: return 0 - sys.stderr.write("# symlink mount test failed: expected 'Rome' in mountinfo output\n") - sys.stderr.write("# actual output: %s\n" % out[:200]) + logger.info("symlink mount test failed: expected 'Rome' in mountinfo output") + logger.info("actual output: %s", out) return -1 def test_mount_fifo(): @@ -89,8 +93,8 @@ def test_mount_fifo(): conf['mounts'].append(mount_opt) out, _ = run_and_get_output(conf, hide_stderr=True) if "FIFO" not in out: - sys.stderr.write("# FIFO mount test failed with options %s: expected 'FIFO' in output\n" % options) - sys.stderr.write("# actual output: %s\n" % out) + logger.info("FIFO mount test failed with options %s: expected 'FIFO' in output", options) + logger.info("actual output: %s", out) return 1 return 0 @@ -109,26 +113,26 @@ def test_mount_unix_socket(): conf['mounts'].append(mount_opt) out, _ = run_and_get_output(conf, hide_stderr=True) if "socket" not in out: - sys.stderr.write("# unix socket mount test failed with options %s: expected 'socket' in output\n" % options) - sys.stderr.write("# actual output: %s\n" % out) + logger.info("unix socket mount test failed with options %s: expected 'socket' in output", options) + logger.info("actual output: %s", out) return 1 return 0 def test_mount_tmpfs_permissions(): def prepare_rootfs(rootfs): - path = os.path.join(rootfs, "tmp") + path = os.path.join(rootfs, "test-tmpfs") os.mkdir(path) os.chmod(path, 0o712) conf = base_config() - conf['process']['args'] = ['/init', 'mode', '/tmp'] + conf['process']['args'] = ['/init', 'mode', '/test-tmpfs'] add_all_namespaces(conf) - conf['mounts'].append({"destination": "/tmp", "type": "tmpfs", "source": "tmpfs", "options": ["ro"]}) + conf['mounts'].append({"destination": "/test-tmpfs", "type": "tmpfs", "source": "tmpfs", "options": ["ro"]}) out, _ = run_and_get_output(conf, hide_stderr=True, callback_prepare_rootfs=prepare_rootfs) if "712" in out: return 0 - sys.stderr.write("# tmpfs permissions test failed: expected '712' in mode output\n") - sys.stderr.write("# actual output: %s\n" % out) + logger.info("tmpfs permissions test failed: expected '712' in mode output") + logger.info("actual output: %s", out) return -1 def test_mount_bind_to_rootfs(): @@ -202,7 +206,7 @@ def test_ro_cgroup(): for i in reversed(out.split("\n")): if i.find("/sys/fs/cgroup") >= 0: if i.find("ro,") < 0: - print("fail with cgroupns=%s, netns=%s and cgroup_mount=%s, got %s" % (cgroupns, netns, has_cgroup_mount, i), file=sys.stderr) + logger.error("fail with cgroupns=%s, netns=%s and cgroup_mount=%s, got %s", cgroupns, netns, has_cgroup_mount, i) return -1 break return 0 @@ -442,7 +446,7 @@ def test_mount_dev(): def test_userns_bind_mount(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf, userns=True) @@ -473,7 +477,7 @@ def test_userns_bind_mount(): def test_userns_bind_mount_symlink(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf, userns=True) @@ -486,7 +490,7 @@ def test_userns_bind_mount_symlink(): ] conf['linux']['uidMappings'] = fullMapping conf['linux']['gidMappings'] = fullMapping - sys.stderr.write("# start\n") + logger.info("start") bind_dir_parent = os.path.join(get_tests_root(), "bind-mount-userns-symlink") bind_dir = os.path.join(bind_dir_parent, "m") @@ -502,17 +506,22 @@ def test_userns_bind_mount_symlink(): os.chmod(bind_dir_parent, 0o000) conf['process']['args'] = ['/init', 'cat', "/foo/content"] - out, _ = run_and_get_output(conf, chown_rootfs_to=1) + out, _ = run_and_get_output(conf, chown_rootfs_to=1, hide_stderr=True) if out != "hello": - sys.stderr.write("# wrong file owner, found %s instead of %s\n" % (out, "hello")) + logger.info("wrong file content, found '%s' instead of 'hello'", out) return -1 finally: - shutil.rmtree(bind_dir) + try: + # Restore permissions so we can clean up properly + os.chmod(bind_dir_parent, 0o755) + shutil.rmtree(bind_dir_parent) + except Exception as e: + logger.info("Failed to cleanup test directory: %s", e) return 0 def test_idmapped_mounts(): if is_rootless(): - return 77 + return (77, "requires root privileges") source_dir = os.path.join(get_tests_root(), "test-idmapped-mounts") try: os.makedirs(source_dir) @@ -524,7 +533,7 @@ def test_idmapped_mounts(): idmapped_mounts_status = subprocess.call([get_init_path(), "check-feature", "idmapped-mounts", source_dir]) if idmapped_mounts_status != 0: - return 77 + return (77, "idmapped mounts not supported") template = base_config() add_all_namespaces(template, userns=True) @@ -557,7 +566,7 @@ def check(uidMappings, gidMappings, recursive, expected): conf['mounts'].append(mount_opt) out = run_and_get_output(conf, chown_rootfs_to=1) if expected not in out[0]: - sys.stderr.write("# wrong file owner, found %s instead of %s\n" % (out[0], expected)) + logger.info("wrong file owner, found %s instead of %s", out[0], expected) return True return False @@ -641,13 +650,13 @@ def test_cgroup_mount_without_netns(): if i.find("/sys/fs/cgroup") >= 0: count = count + 1 if count < 2: - sys.stderr.write("# fail with cgroupns=%s, got %s\n" % (cgroupns, out)) + logger.info("fail with cgroupns=%s, got %s", cgroupns, i) return -1 return 0 def test_add_remove_mounts(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['mounts'].append({"destination": "/foo", "type": "tmpfs", "source": "tmpfs", "options": ["rw"]}) @@ -672,9 +681,9 @@ def check_test_file(expected): if exists == expected: return True if expected: - sys.stderr.write("# test file not found\n") + logger.info("test file not found") else: - sys.stderr.write("# test file found\n") + logger.info("test file found") return False new_mounts = [{"destination": parent_dir_in_container, "type": "bind", "source": bind_dir, "options": ["bind", "ro"]}, @@ -695,7 +704,7 @@ def check_test_file(expected): return -1 out = run_crun_command(["exec", cid, "/init", "cat", "/proc/self/mountinfo"]) if not re.search(r".*/ /foo/tmpfs .*tmpfs.*", out): - sys.stderr.write("# /foo/tmpfs not found as a tmpfs\n") + logger.info("/foo/tmpfs not found as a tmpfs") return -1 run_crun_command(["mounts", "remove", cid, mounts_path]) @@ -704,7 +713,7 @@ def check_test_file(expected): out = run_crun_command(["exec", cid, "/init", "cat", "/proc/self/mountinfo"]) if re.search(r".*/ /foo/tmpfs .*tmpfs.*", out): - sys.stderr.write("# /foo/tmpfs still mounted\n") + logger.info("/foo/tmpfs still mounted") return -1 finally: if cid is not None: @@ -764,12 +773,12 @@ def prepare_rootfs(rootfs): conf['mounts'].append(mount_opt) try: - out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs) - sys.stderr.write("# got output %s with configuration userns=%s, src-nofollow=%s\n" % (out, userns, src_nofollow)) + out, _ = run_and_get_output(conf, hide_stderr=True, callback_prepare_rootfs=prepare_rootfs) + logger.info("got output %s with configuration userns=%s, src-nofollow=%s", out, userns, src_nofollow) if expected not in out: return -1 except Exception as e: - sys.stderr.write("# error %s\n" % e) + logger.info("error %s", e) return -1 return 0 @@ -789,10 +798,10 @@ def test_bind_mount_symlink_nofollow_procfs(): conf['mounts'].append(mount_opt) try: - out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs) + out, _ = run_and_get_output(conf, hide_stderr=True, callback_prepare_rootfs=prepare_rootfs) return -1 except Exception as e: - sys.stderr.write("# error %s\n" % e) + logger.info("error %s", e) return 0 return 0 @@ -834,17 +843,17 @@ def prepare_rootfs(rootfs): conf['mounts'].append(mount_opt) try: - out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs) - sys.stderr.write("# got output %s with configuration userns=%s, src-nofollow=%s\n" % (out, userns, src_nofollow)) + out, _ = run_and_get_output(conf, hide_stderr=True, callback_prepare_rootfs=prepare_rootfs) + logger.info("got output %s with configuration userns=%s, src-nofollow=%s", out, userns, src_nofollow) if target_content not in out: return 1 except Exception as e: - sys.stderr.write("# error %s\n" % e) + logger.info("error %s", e) return 0 def test_idmapped_mounts_without_userns(): if is_rootless(): - return 77 + return (77, "requires root privileges") source_dir = os.path.join(get_tests_root(), "test-idmapped-mounts-no-userns") try: os.makedirs(source_dir) @@ -875,8 +884,8 @@ def test_idmapped_mounts_without_userns(): out, _ = run_and_get_output(conf, hide_stderr=True) if "1000:1000" not in out: - sys.stderr.write("# idmap without userns test failed: expected '1000:1000' in output\n") - sys.stderr.write("# actual output: %s\n" % out) + logger.info("idmap without userns test failed: expected '1000:1000' in output") + logger.info("actual output: %s", out) return 1 finally: shutil.rmtree(source_dir) diff --git a/tests/test_oci_features.py b/tests/test_oci_features.py index bf92294161..4c09db6b20 100755 --- a/tests/test_oci_features.py +++ b/tests/test_oci_features.py @@ -36,7 +36,7 @@ def get_crun_commit(): raise ValueError("Commit information not found") except (subprocess.CalledProcessError, ValueError) as e: - print(f"Error retrieving crun commit: {str(e)}") + logger.error("Error retrieving crun commit: %s", str(e)) return None def test_crun_features(): @@ -198,36 +198,36 @@ def test_crun_features(): if key == "annotations": if "annotations" not in features: - sys.stderr.write("# annotations section is missing\n") + logger.info("annotations section is missing") return -1 annotations = features["annotations"] if annotations.get("run.oci.crun.commit") != get_crun_commit(): - sys.stderr.write("# wrong value for run.oci.crun.commit\n") + logger.info("wrong value for run.oci.crun.commit") return -1 if ('WASM' in get_crun_feature_string() and annotations.get("run.oci.crun.wasm") != "true"): - sys.stderr.write("# wrong value for run.oci.crun.wasm\n") + logger.info("wrong value for run.oci.crun.wasm") return -1 if 'CRIU' in get_crun_feature_string(): if annotations.get("org.opencontainers.runc.checkpoint.enabled") != "true": - sys.stderr.write("# wrong value for org.opencontainers.runc.checkpoint.enabled\n") + logger.info("wrong value for org.opencontainers.runc.checkpoint.enabled") return -1 if annotations.get("run.oci.crun.checkpoint.enabled") != "true": - sys.stderr.write("# wrong value for run.oci.crun.checkpoint.enabled\n") + logger.info("wrong value for run.oci.crun.checkpoint.enabled") return -1 else: if key not in features or sorted(features[key]) != sorted(value): - sys.stderr.write(f"# Mismatch in feature: {key}\n") - sys.stderr.write(f"# Expected: {value}\n") - sys.stderr.write(f"# Actual: {features.get(key)}\n") + logger.info("# Mismatch in feature: %s", key) + logger.info("# Expected: %s", value) + logger.info("# Actual: %s", features.get(key)) return -1 return 0 except Exception as e: - print("Error running crun features:", str(e)) + logger.error("Error running crun features: %s", str(e)) return -1 all_tests = { diff --git a/tests/test_pid.py b/tests/test_pid.py index 3f214ad9b6..7c1708792f 100755 --- a/tests/test_pid.py +++ b/tests/test_pid.py @@ -19,11 +19,11 @@ def test_pid(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] conf['linux']['namespaces'].append({"type" : "pid"}) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) pid = parse_proc_status(out)['Pid'] if pid == "1": return 0 @@ -33,7 +33,7 @@ def test_pid_user(): conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) pid = parse_proc_status(out)['Pid'] if pid == "1": return 0 @@ -41,11 +41,11 @@ def test_pid_user(): def test_pid_host_namespace(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] # No PID namespace is added. Expect PID to not be 1. - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) pid = parse_proc_status(out)['Pid'] if pid != "1": return 0 @@ -55,7 +55,7 @@ def test_pid_ppid_is_zero(): conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) status = parse_proc_status(out) pid = status.get('Pid') ppid = status.get('PPid') diff --git a/tests/test_pid_file.py b/tests/test_pid_file.py index 3364551183..52b6ce086d 100755 --- a/tests/test_pid_file.py +++ b/tests/test_pid_file.py @@ -24,7 +24,7 @@ def test_pid_file(): conf['process']['args'] = ['/init', 'cwd', ''] pid_file = os.path.abspath('test-pid-%s' % os.getpid()) try: - run_and_get_output(conf, pid_file=pid_file) + run_and_get_output(conf, hide_stderr=True, pid_file=pid_file) with open(pid_file) as p: content = p.read() if len(content) > 0: diff --git a/tests/test_resources.py b/tests/test_resources.py index e019f6fdd2..a676171d9a 100755 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -18,12 +18,14 @@ import subprocess import sys import time +import json from tests_utils import * +import json def test_resources_fail_with_enoent(): if is_rootless(): - return 77 + return (77, "requires root privileges") if not is_cgroup_v2_unified(): return 77 @@ -42,7 +44,7 @@ def test_resources_fail_with_enoent(): def test_resources_pid_limit(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['linux']['resources'] = {"pids" : {"limit" : 1024}} add_all_namespaces(conf) @@ -54,15 +56,15 @@ def test_resources_pid_limit(): conf['process']['args'] = ['/init', 'cat', fn] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "1024" not in out: - sys.stderr.write("# found %s instead of 1024\n" % out) + logger.info("found %s instead of 1024", out) return -1 return 0 def test_resources_pid_limit_userns(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['linux']['resources'] = {"pids" : {"limit" : 1024}} @@ -97,15 +99,15 @@ def test_resources_pid_limit_userns(): conf['process']['args'] = ['/init', 'cat', fn] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "1024" not in out: - sys.stderr.write("# found %s instead of 1024\n" % out) + logger.info("found %s instead of 1024", out) return -1 return 0 def test_resources_unified_invalid_controller(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -134,7 +136,7 @@ def test_resources_unified_invalid_controller(): def test_resources_unified_invalid_key(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -160,7 +162,7 @@ def test_resources_unified_invalid_key(): def test_resources_unified(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -172,7 +174,7 @@ def test_resources_unified(): } cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) out = run_crun_command(["exec", cid, "/init", "cat", "/sys/fs/cgroup/memory.high"]) if "1073741824" not in out: return -1 @@ -183,7 +185,7 @@ def test_resources_unified(): def test_resources_cpu_weight(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -195,7 +197,7 @@ def test_resources_cpu_weight(): } cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) out = run_crun_command(["exec", cid, "/init", "cat", "/sys/fs/cgroup/cpu.weight"]) if "1234" not in out: return -1 @@ -206,7 +208,7 @@ def test_resources_cpu_weight(): def test_resources_cgroupv2_swap_0(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -218,7 +220,7 @@ def test_resources_cgroupv2_swap_0(): } cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) out = run_crun_command(["exec", cid, "/init", "cat", "/sys/fs/cgroup/memory.swap.max"]) if "0" not in out: return -1 @@ -229,7 +231,7 @@ def test_resources_cgroupv2_swap_0(): def test_resources_cpu_quota_minus_one(): if is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v1 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -241,7 +243,7 @@ def test_resources_cpu_quota_minus_one(): } cid = None try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') if "-1" not in out: return -1 finally: @@ -253,11 +255,11 @@ def test_resources_cpu_quota_minus_one(): def test_resources_cpu_weight_systemd(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") if not running_on_systemd(): - return 77 + return (77, "not running on systemd") conf = base_config() add_all_namespaces(conf, cgroupns=True) @@ -270,10 +272,10 @@ def test_resources_cpu_weight_systemd(): } cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True, cgroup_manager="systemd") + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True, cgroup_manager="systemd") out = run_crun_command(["exec", cid, "/init", "cat", "/sys/fs/cgroup/cpu.weight"]) if "1234" not in out: - sys.stderr.write("# found wrong CPUWeight for the container cgroup\n") + logger.info("found wrong CPUWeight for the container cgroup") return -1 state = run_crun_command(['state', cid]) @@ -285,7 +287,7 @@ def test_resources_cpu_weight_systemd(): out = subprocess.check_output(['systemctl', '--user', 'show','-PCPUWeight', scope ], close_fds=False).decode().strip() if out != "1234": - sys.stderr.write("# found wrong CPUWeight for the systemd scope\n") + logger.info("found wrong CPUWeight for the systemd scope") return 1 for values in [(2, 1), (3, 2), (1024, 100), (260000, 9929), (262144, 10000)]: @@ -297,7 +299,7 @@ def test_resources_cpu_weight_systemd(): out = run_crun_command(["exec", cid, "/init", "cat", "/sys/fs/cgroup/cpu.weight"]) if expected_weight not in out: - sys.stderr.write("found wrong CPUWeight %s instead of %s for the container cgroup\n" % (out, expected_weight)) + logger.info("found wrong CPUWeight %s instead of %s for the container cgroup", out, expected_weight) return -1 out = subprocess.check_output(['systemctl', 'show','-PCPUWeight', scope ], close_fds=False).decode().strip() @@ -306,7 +308,7 @@ def test_resources_cpu_weight_systemd(): out = subprocess.check_output(['systemctl', '--user', 'show','-PCPUWeight', scope ], close_fds=False).decode().strip() if out != expected_weight: - sys.stderr.write("found wrong CPUWeight for the systemd scope\n") + logger.info("found wrong CPUWeight for the systemd scope: expected %s, got %s", expected_weight, out) return 1 finally: if cid is not None: @@ -316,14 +318,14 @@ def test_resources_cpu_weight_systemd(): def test_resources_exec_cgroup(): if not is_cgroup_v2_unified() or is_rootless(): - return 77 + return (77, "requires cgroup v2 and root privileges") conf = base_config() add_all_namespaces(conf, cgroupns=True) conf['process']['args'] = ['/init', 'create-sub-cgroup-and-wait', 'foo'] cid = None try: - out, cid = run_and_get_output(conf, command='run', detach=True) + out, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) # Give some time to pid 1 to move to the new cgroup time.sleep(2) out = run_crun_command(["exec", "--cgroup=/foo", cid, "/init", "cat", "/proc/self/cgroup"]) @@ -331,7 +333,7 @@ def test_resources_exec_cgroup(): if i == "": continue if "/foo" not in i: - sys.stderr.write("# /foo not found in the output\n") + logger.info("/foo not found in the output") return -1 return 0 except Exception as e: diff --git a/tests/test_rlimits.py b/tests/test_rlimits.py index d7826ecbd9..b744c12e56 100755 --- a/tests/test_rlimits.py +++ b/tests/test_rlimits.py @@ -32,7 +32,7 @@ def parse_proc_limits(content): def test_rlimits(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/limits'] rlimits = [ @@ -51,13 +51,13 @@ def test_rlimits(): ] conf['process']['rlimits'] = rlimits add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) limits = parse_proc_limits(out) for v in rlimits: limit = limits.get(v['type']) if str(limit[1]) != str(v['soft']) or str(limit[2]) != str(v['hard']): - sys.stderr.write("# %s: %s %s\n" % (limit[0], limit[1], limit[2])) + logger.info("%s: %s %s", key, soft_limit, hard_limit) return -1 return 0 diff --git a/tests/test_seccomp.py b/tests/test_seccomp.py index bbd5010b24..a618701380 100755 --- a/tests/test_seccomp.py +++ b/tests/test_seccomp.py @@ -39,7 +39,7 @@ def recv_fds(sock, msglen, maxfds): def test_seccomp_listener(): if not is_seccomp_listener_supported(): - return 77 + return (77, "seccomp listener not supported") listener_path = "%s/seccomp-listener" % get_tests_root() listener_metadata = "SOME-RANDOM-METADATA" @@ -58,57 +58,57 @@ def test_seccomp_listener(): conf['process']['args'] = ['/init', 'true'] cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) conn = sock.accept() msg, fds = recv_fds(conn[0], 4096, 1) if len(fds) != 1: - sys.stderr.write("# seccomp listener test failed: expected 1 FD, got %d\n" % len(fds)) + logger.info("seccomp listener test failed: expected 1 FD, got %d", len(fds)) return -1 try: m = json.loads(msg) except json.JSONDecodeError as e: - sys.stderr.write("# seccomp listener test failed: invalid JSON message: %s\n" % str(e)) - sys.stderr.write("# raw message: %s\n" % msg) + logger.info("seccomp listener test failed: invalid JSON message: %s", str(e)) + logger.info("raw message: %s", msg) return -1 if m.get('ociVersion') != '0.2.0': - sys.stderr.write("# seccomp listener test failed: expected ociVersion '0.2.0', got '%s'\n" % m.get('ociVersion')) + logger.info("seccomp listener test failed: expected ociVersion '0.2.0', got '%s'", m.get('ociVersion')) return -1 if len(m.get('fds', [])) != 1: - sys.stderr.write("# seccomp listener test failed: expected 1 fd in message, got %d\n" % len(m.get('fds', []))) + logger.info("seccomp listener test failed: expected 1 fd in message, got %d", len(m.get("fds", []))) return -1 if 'pid' not in m: - sys.stderr.write("# seccomp listener test failed: missing 'pid' field in message\n") - sys.stderr.write("# message fields: %s\n" % list(m.keys())) + logger.info("seccomp listener test failed: missing 'pid' field in message") + logger.info("message fields: %s", list(m.keys())) return -1 if m.get('metadata') != listener_metadata: - sys.stderr.write("# seccomp listener test failed: expected metadata '%s', got '%s'\n" % (listener_metadata, m.get('metadata'))) + logger.info("seccomp listener test failed: expected metadata '%s', got '%s'", listener_metadata, m.get('metadata')) return -1 state = m.get('state', {}) if state.get('status') != 'creating': - sys.stderr.write("# seccomp listener test failed: expected status 'creating', got '%s'\n" % state.get('status')) + logger.info("seccomp listener test failed: expected status 'creating', got '%s'", state.get('status')) return -1 if state.get('id') != cid: - sys.stderr.write("# seccomp listener test failed: expected container id '%s', got '%s'\n" % (cid, state.get('id'))) + logger.info("seccomp listener test failed: expected container id '%s', got '%s'", cid, state.get('id')) return -1 return 0 except Exception as e: - sys.stderr.write("# seccomp listener test failed with exception: %s\n" % str(e)) + logger.info("seccomp listener test failed with exception: %s", e) if cid is not None: - sys.stderr.write("# container ID: %s\n" % cid) - sys.stderr.write("# listener path: %s\n" % listener_path) + logger.info("container ID: %s", cid) + logger.info("listener path: %s", listener_path) return -1 finally: if cid is not None: try: run_crun_command(["delete", "-f", cid]) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup container %s: %s\n" % (cid, str(cleanup_e))) + logger.info("warning: failed to cleanup container %s: %s", cid, cleanup_e) try: os.unlink(listener_path) except Exception as cleanup_e: - sys.stderr.write("# warning: failed to cleanup listener socket %s: %s\n" % (listener_path, str(cleanup_e))) + logger.info("warning: failed to cleanup listener socket %s: %s", listener_path, cleanup_e) all_tests = { "seccomp-listener" : test_seccomp_listener, diff --git a/tests/test_start.py b/tests/test_start.py index dcf04bbce6..5a9761d790 100755 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -23,11 +23,12 @@ import threading import socket import json +import tempfile from tests_utils import * def test_not_allowed_ipc_sysctl(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'true'] @@ -35,8 +36,8 @@ def test_not_allowed_ipc_sysctl(): conf['linux']['sysctl'] = {'fs.mqueue.queues_max' : '100'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: pass @@ -50,8 +51,8 @@ def test_not_allowed_ipc_sysctl(): conf['linux']['sysctl'] = {'kernel.msgmax' : '8192'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: pass @@ -65,9 +66,9 @@ def test_not_allowed_ipc_sysctl(): conf['linux']['sysctl'] = {'kernel.msgmax' : '8192'} cid = None try: - _, cid = run_and_get_output(conf) + _, cid = run_and_get_output(conf, hide_stderr=True) except Exception as e: - sys.stderr.write("# setting msgmax with new ipc namespace failed\n") + logger.info("setting msgmax with new ipc namespace failed") return -1 finally: if cid is not None: @@ -76,15 +77,15 @@ def test_not_allowed_ipc_sysctl(): def test_not_allowed_net_sysctl(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'true'] add_all_namespaces(conf, netns=False) conf['linux']['sysctl'] = {'net.ipv4.ping_group_range' : '0 0'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: pass @@ -98,9 +99,9 @@ def test_not_allowed_net_sysctl(): conf['linux']['sysctl'] = {'net.ipv4.ping_group_range' : '0 0'} cid = None try: - _, cid = run_and_get_output(conf) + _, cid = run_and_get_output(conf, hide_stderr=True) except Exception as e: - sys.stderr.write("# setting net.ipv4.ping_group_range with new net namespace failed\n") + logger.info("setting net.ipv4.ping_group_range with new net namespace failed") return -1 finally: if cid is not None: @@ -110,7 +111,7 @@ def test_not_allowed_net_sysctl(): def test_unknown_sysctl(): if is_rootless(): - return 77 + return (77, "requires root privileges") for sysctl in ['kernel.foo', 'bar.baz', 'fs.baz']: conf = base_config() @@ -119,8 +120,8 @@ def test_unknown_sysctl(): conf['linux']['sysctl'] = {sysctl : 'value'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: return 0 @@ -131,7 +132,7 @@ def test_unknown_sysctl(): def test_uts_sysctl(): if is_rootless(): - return 77 + return (77, "requires root privileges") # setting kernel.hostname must always fail. for utsns in [True, False]: @@ -141,8 +142,8 @@ def test_uts_sysctl(): conf['linux']['sysctl'] = {'kernel.hostname' : 'foo'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: return 0 @@ -156,8 +157,8 @@ def test_uts_sysctl(): conf['linux']['sysctl'] = {'kernel.domainname' : 'foo'} cid = None try: - _, cid = run_and_get_output(conf) - sys.stderr.write("# unexpected success\n") + _, cid = run_and_get_output(conf, hide_stderr=True) + logger.info("unexpected success") return -1 except: return 0 @@ -171,7 +172,7 @@ def test_uts_sysctl(): conf['linux']['sysctl'] = {'kernel.domainname' : 'foo'} cid = None try: - _, cid = run_and_get_output(conf) + _, cid = run_and_get_output(conf, hide_stderr=True) return 0 except: return -1 @@ -186,7 +187,7 @@ def test_start(): add_all_namespaces(conf) cid = None try: - proc, cid = run_and_get_output(conf, command='create', use_popen=True) + proc, cid = run_and_get_output(conf, hide_stderr=True, command='create', use_popen=True) for i in range(50): try: s = run_crun_command(["state", cid]) @@ -205,7 +206,7 @@ def test_start(): status = json.load(f) descriptors = status["external_descriptors"] if not isinstance(descriptors, str): - print("external_descriptors is not a string") + logger.error("external_descriptors is not a string") return -1 finally: if cid is not None: @@ -218,7 +219,7 @@ def test_start_override_config(): add_all_namespaces(conf) cid = None try: - proc, cid = run_and_get_output(conf, command='create', use_popen=True, relative_config_path="config/config.json") + proc, cid = run_and_get_output(conf, hide_stderr=True, command='create', use_popen=True, relative_config_path="config/config.json") for i in range(50): try: s = run_crun_command(["state", cid]) @@ -243,7 +244,7 @@ def test_run_twice(): try: id_container = "container-%s" % os.getpid() for i in range(2): - out, cid = run_and_get_output(conf, command='run', id_container=id_container) + out, cid = run_and_get_output(conf, hide_stderr=True, command='run', id_container=id_container) if "hi" not in str(out): return -1 except: @@ -252,14 +253,14 @@ def test_run_twice(): def test_sd_notify(): if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/mountinfo'] add_all_namespaces(conf) env = dict(os.environ) env["NOTIFY_SOCKET"] = "/run/notify/the-socket" try: - out, cid = run_and_get_output(conf, env=env, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, env=env, command='run') if "/run/notify/the-socket" not in str(out): return -1 except: @@ -268,14 +269,14 @@ def test_sd_notify(): def test_sd_notify_file(): if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") conf = base_config() conf['process']['args'] = ['/init', 'ls', '/tmp/parent-dir/the-socket/'] add_all_namespaces(conf) env = dict(os.environ) env["NOTIFY_SOCKET"] = "/tmp/parent-dir/the-socket" try: - out, cid = run_and_get_output(conf, env=env, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, env=env, command='run') if "notify" not in str(out): return -1 except: @@ -284,14 +285,14 @@ def test_sd_notify_file(): def test_sd_notify_env(): if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") conf = base_config() conf['process']['args'] = ['/init', 'printenv', 'NOTIFY_SOCKET'] add_all_namespaces(conf) env = dict(os.environ) env["NOTIFY_SOCKET"] = "/tmp/parent-dir/the-socket" try: - out, cid = run_and_get_output(conf, env=env, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, env=env, command='run') if "/tmp/parent-dir/the-socket/notify" not in str(out): return -1 except: @@ -304,7 +305,7 @@ def test_delete_in_created_state(): add_all_namespaces(conf) cid = None try: - proc, cid = run_and_get_output(conf, command='create', use_popen=True) + proc, cid = run_and_get_output(conf, hide_stderr=True, command='create', use_popen=True) proc.wait() run_crun_command(["delete", cid]) except: @@ -316,14 +317,14 @@ def test_delete_in_created_state(): def test_sd_notify_proxy(): if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") if is_rootless(): - return 77 + return (77, "requires root privileges") has_open_tree_status = subprocess.call([get_init_path(), "check-feature", "open_tree"]) has_move_mount_status = subprocess.call([get_init_path(), "check-feature", "move_mount"]) if has_open_tree_status != 0 or has_move_mount_status != 0: - return 77 + return (77, "requires open_tree and move_mount syscalls") conf = base_config() conf['process']['args'] = ['/init', 'systemd-notify', '--ready'] @@ -352,7 +353,7 @@ def notify_server(): notify_thread = threading.Thread(target=notify_server) notify_thread.start() try: - run_and_get_output(conf, env=env, command='run', chown_rootfs_to=8000) + run_and_get_output(conf, hide_stderr=True, env=env, command='run', chown_rootfs_to=8000) notify_thread.join() if ready_datagram != b"READY=1": return -1 @@ -370,7 +371,7 @@ def test_empty_home(): conf['process']['args'] = ['/sbin/init', 'printenv', 'HOME'] add_all_namespaces(conf) try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "/" not in str(out): return -1 except Exception as e: @@ -379,7 +380,7 @@ def test_empty_home(): def test_run_rootless_netns_with_userns(): if not is_rootless(): - return 77 + return (77, "requires rootless mode") conf = base_config() conf['process']['args'] = ['/init', 'pause'] @@ -388,7 +389,7 @@ def test_run_rootless_netns_with_userns(): conf['linux']['namespaces'].append({"type" : "network", "path" : "/proc/1/ns/net"}) cid = None try: - _, cid = run_and_get_output(conf, command='run', detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True) except: # expect a failure return 0 @@ -406,7 +407,7 @@ def test_listen_pid_env(): env = dict(os.environ) env["LISTEN_FDS"] = "1" try: - out, cid = run_and_get_output(conf, env=env, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, env=env, command='run') if "1" not in str(out): return -1 except: @@ -425,7 +426,7 @@ def test_ioprio(): supported = subprocess.call([get_init_path(), "check-feature", "ioprio"]) if supported != 0: - return 77 + return (77, "ioprio support not available") conf = base_config() add_all_namespaces(conf, netns=False) @@ -438,13 +439,13 @@ def test_ioprio(): cid = None try: - output, cid = run_and_get_output(conf, command='run') + output, cid = run_and_get_output(conf, hide_stderr=True, command='run') value = int(output) if ((value >> IOPRIO_CLASS_SHIFT) & IOPRIO_CLASS_MASK) != IOPRIO_CLASS_IDLE: - print("invalid ioprio class returned") + logger.error("invalid ioprio class returned") return 1 if value & IOPRIO_PRIO_MASK != 0: - print("invalid ioprio priority returned") + logger.error("invalid ioprio priority returned") return 1 return 0 except Exception as e: @@ -459,23 +460,23 @@ def test_run_keep(): conf['process']['args'] = ['/init', 'cat', '/dev/null'] add_all_namespaces(conf) try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') except: - sys.stderr.write("# failed to create container\n") + logger.info("failed to create container") return -1 # without --keep, we must be able to recreate the container with the same id try: - out, cid = run_and_get_output(conf, command='run', keep=True, id_container=cid) + out, cid = run_and_get_output(conf, hide_stderr=True, command='run', keep=True, id_container=cid) except: - sys.stderr.write("# failed to create container\n") + logger.info("failed to create container") return -1 # now it must fail try: try: - out, cid = run_and_get_output(conf, command='run', keep=True, id_container=cid) - sys.stderr.write("# run --keep succeeded twice\n") + out, cid = run_and_get_output(conf, hide_stderr=True, command='run', keep=True, id_container=cid) + logger.info("run --keep succeeded twice") return -1 except: # expected @@ -484,7 +485,7 @@ def test_run_keep(): try: s = run_crun_command(["state", cid]) except: - sys.stderr.write("# crun state failed on --keep container\n") + logger.info("crun state failed on --keep container") return -1 finally: run_crun_command(["delete", "-f", cid]) @@ -500,25 +501,28 @@ def test_invalid_id(): out, _ = run_and_get_output(conf, id_container="this/is/invalid") return -1 except Exception as e: - err = e.output.decode() - if "invalid character `/` in the ID" in err: - return 0 - sys.stderr.write("# got error: %s\n" % err) + if hasattr(e, 'output') and e.output: + err = e.output.decode() + if "invalid character `/` in the ID" in err: + return 0 + logger.info("got error: %s", err) + else: + logger.info("got exception without output: %s", str(e)) return -1 return 0 def test_home_unknown_id(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'printenv', "HOME"] conf['process']['user']['uid'] = 101010 conf['process']['user']['gid'] = 101010 add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if out != "/": - sys.stderr.write("# expected: `/`, got output: `%s`\n" % out) + logger.info("expected: `/`, got output: `%s`", out) return -1 return 0 @@ -532,9 +536,9 @@ def test_start_help(): # https://github.com/containers/crun/issues/1811. def test_systemd_cgroups_path_def_slice(): if 'SYSTEMD' not in get_crun_feature_string(): - return 77 + return (77, "systemd support not compiled in") if not running_on_systemd(): - return 77 + return (77, "not running on systemd") conf = base_config() add_all_namespaces(conf) @@ -543,7 +547,7 @@ def test_systemd_cgroups_path_def_slice(): cid = None try: - _, cid = run_and_get_output(conf, cgroup_manager="systemd", detach=True) + _, cid = run_and_get_output(conf, hide_stderr=True, cgroup_manager="systemd", detach=True) state = run_crun_command(['state', cid]) scope = json.loads(state)['systemd-scope'] @@ -557,7 +561,7 @@ def test_systemd_cgroups_path_def_slice(): got = subprocess.check_output(['systemctl', '--user', 'show','-PSlice', scope], close_fds=False).decode().strip() if got != want: - sys.stderr.write("# Got Slice %s, want %s\n" % got, want) + logger.info("Got Slice %s, want %s", got, want) return 1 except: return 1 diff --git a/tests/test_time.py b/tests/test_time.py index 5f7598b6c9..4e35f61b79 100755 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -28,9 +28,9 @@ def test_time_namespace(): timens_offsets = "/proc/self/timens_offsets" if not os.path.exists(timens_offsets): - return 77 + return (77, "time namespaces not supported") if is_rootless(): - return 77 + return (77, "requires root privileges") time_offsets = { "monotonic": { @@ -48,7 +48,7 @@ def test_time_namespace(): conf['linux']['timeOffsets'] = time_offsets add_all_namespaces(conf,time=True) try: - out, cid = run_and_get_output(conf, command='run') + out, cid = run_and_get_output(conf, hide_stderr=True, command='run') for line in out.split("\n"): parts = line.split() diff --git a/tests/test_tty.py b/tests/test_tty.py index bce378d922..063aef447b 100755 --- a/tests/test_tty.py +++ b/tests/test_tty.py @@ -20,12 +20,12 @@ def tty_helper(fd): if os.isatty(1) == False: - return 77 + return (77, "requires TTY") conf = base_config() conf['process']['args'] = ['/init', 'isatty', fd] conf['process']['terminal'] = True add_all_namespaces(conf) - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if "true" not in out: return -1 return 0 @@ -41,7 +41,7 @@ def test_stderr_tty(): def test_tty_and_detach(): if os.isatty(1) == False: - return 77 + return (77, "requires TTY") conf = base_config() conf['process']['args'] = ['/init', 'isatty', 0] conf['process']['terminal'] = True diff --git a/tests/test_uid_gid.py b/tests/test_uid_gid.py index 2c42a00d74..59b8d55de9 100755 --- a/tests/test_uid_gid.py +++ b/tests/test_uid_gid.py @@ -21,7 +21,7 @@ def test_userns_full_mapping(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() add_all_namespaces(conf, userns=True) @@ -38,7 +38,7 @@ def test_userns_full_mapping(): for filename in ['uid_map', 'gid_map']: conf['process']['args'] = ['/init', 'cat', '/proc/self/%s' % filename] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) if "4294967295" not in out: @@ -48,12 +48,12 @@ def test_userns_full_mapping(): def test_uid(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) conf['process']['user']['uid'] = 1000 - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) ids = proc_status['Uid'].split() @@ -64,12 +64,12 @@ def test_uid(): def test_gid(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) conf['process']['user']['gid'] = 1000 - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) ids = proc_status['Gid'].split() @@ -80,12 +80,12 @@ def test_gid(): def test_no_groups(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) conf['process']['user']['gid'] = 1000 - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) ids = proc_status['Groups'].split() @@ -95,7 +95,7 @@ def test_no_groups(): def test_keep_groups(): if is_rootless(): - return 77 + return (77, "requires root privileges") oldgroups = os.getgroups() out = "" try: @@ -105,7 +105,7 @@ def test_keep_groups(): add_all_namespaces(conf) conf['annotations'] = {} conf['annotations']['run.oci.keep_original_groups'] = "1" - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) finally: os.setgroups(oldgroups) @@ -117,14 +117,14 @@ def test_keep_groups(): def test_additional_gids(): if is_rootless(): - return 77 + return (77, "requires root privileges") conf = base_config() conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] add_all_namespaces(conf) conf['process']['user']['uid'] = 1000 conf['process']['user']['gid'] = 1000 conf['process']['user']['additionalGids'] = [2000, 3000] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) gids_status = proc_status['Gid'].split() @@ -156,7 +156,7 @@ def test_umask(): conf['process']['user']['umask'] = test_umask_int conf['process']['args'] = ['/init', 'cat', '/proc/self/status'] - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) proc_status = parse_proc_status(out) if 'Umask' not in proc_status: @@ -172,7 +172,7 @@ def test_umask(): def test_dev_null_no_chown(): """Test that /dev/null file descriptors are not chowned to container user.""" if is_rootless(): - return 77 + return (77, "requires root privileges") # Get current owner of /dev/null and use owner + 1 as container user dev_null_stat = os.stat('/dev/null') @@ -187,32 +187,32 @@ def test_dev_null_no_chown(): conf['process']['args'] = ['/init', 'owner', '/proc/self/fd/0'] try: - out, container_id = run_and_get_output(conf, stdin_dev_null=True) - sys.stderr.write("# Container ran successfully, output: %s\n" % repr(out)) + out, container_id = run_and_get_output(conf, hide_stderr=True, stdin_dev_null=True) + logger.info("Container ran successfully, output: %s", out) if ':' in out: uid_str, gid_str = out.strip().split(':') uid, gid = int(uid_str), int(gid_str) # Should NOT be owned by container user if uid == container_uid or gid == container_gid: - sys.stderr.write("# dev-null-no-chown test failed: /dev/null fd owned by container user %d:%d\n" % (uid, gid)) - sys.stderr.write("# stdout: %s\n" % repr(out)) + logger.info("dev-null-no-chown test failed: /dev/null fd owned by container user %d:%d", container_uid, container_gid) + logger.info("stdout: %s", out) return -1 - sys.stderr.write("# dev-null-no-chown test passed: /dev/null fd owned by %d:%d (not container user %d:%d)\n" % (uid, gid, container_uid, container_gid)) + logger.info("dev-null-no-chown test passed: /dev/null fd owned by %d:%d (not container user %d:%d)", uid, gid, container_uid, container_gid) else: - sys.stderr.write("# dev-null-no-chown test failed: unexpected owner output format\n") - sys.stderr.write("# stdout: %s\n" % repr(out)) + logger.info("dev-null-no-chown test failed: unexpected owner output format") + logger.info("stdout: %s", out) return -1 return 0 except Exception as e: - sys.stderr.write("# dev-null-no-chown test failed with exception: %s\n" % str(e)) + logger.info("dev-null-no-chown test failed with exception: %s", e) if hasattr(e, 'output'): - sys.stderr.write("# command output: %s\n" % repr(e.output)) + logger.info("command output: %s", e.output) return -1 def test_regular_files_chowned(): """Test that regular file descriptors are chowned to container user.""" if is_rootless(): - return 77 + return (77, "requires root privileges") # Get current owner of /dev/null and use owner + 1 as container user dev_null_stat = os.stat('/dev/null') @@ -227,21 +227,21 @@ def test_regular_files_chowned(): conf['process']['args'] = ['/init', 'owner', '/proc/self/fd/1'] try: - out, _ = run_and_get_output(conf) + out, _ = run_and_get_output(conf, hide_stderr=True) if ':' in out: uid_str, gid_str = out.strip().split(':') uid, gid = int(uid_str), int(gid_str) # Should be owned by container user if uid != container_uid or gid != container_gid: - sys.stderr.write("# regular-files-chowned test failed: regular fd owned by %d:%d (expected %d:%d)\n" % (uid, gid, container_uid, container_gid)) + logger.info("regular-files-chowned test failed: regular fd owned by %d:%d (expected %d:%d)", uid, gid, container_uid, container_gid) return -1 - sys.stderr.write("# regular-files-chowned test passed: regular fd owned by %d:%d (container user)\n" % (uid, gid)) + logger.info("regular-files-chowned test passed: regular fd owned by %d:%d (container user)", uid, gid) else: - sys.stderr.write("# regular-files-chowned test failed: unexpected output format: %s\n" % repr(out)) + logger.info("regular-files-chowned test failed: unexpected output format: %s", out) return -1 return 0 except Exception as e: - sys.stderr.write("# regular-files-chowned test failed with exception: %s\n" % str(e)) + logger.info("regular-files-chowned test failed with exception: %s", e) return -1 all_tests = { diff --git a/tests/test_update.py b/tests/test_update.py index 705f01b340..8b2ab62288 100755 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -17,6 +17,7 @@ import os import shutil +import tempfile from tests_utils import * def test_update(): @@ -25,11 +26,11 @@ def test_update(): add_all_namespaces(conf) temp_dir = tempfile.mkdtemp(dir=get_tests_root()) - out, container_id = run_and_get_output(conf, detach=True) + out, container_id = run_and_get_output(conf, hide_stderr=True, detach=True) try: p = "/sys/fs/cgroup/memory/system.slice/libcrun-%s.scope/memory.limit_in_bytes" % container_id if not os.path.exists(p): - return 77 + return (77, "cgroup v1 memory controller not available") with open(p) as f: oldval = f.read() diff --git a/tests/tests_utils.py b/tests/tests_utils.py index 26f6a90562..aca127eb4b 100755 --- a/tests/tests_utils.py +++ b/tests/tests_utils.py @@ -16,6 +16,7 @@ # along with crun. If not, see . import json +import logging import shutil import sys import os @@ -26,6 +27,20 @@ default_umask = 0o22 +# Simple logging setup for TAP diagnostics +logging.basicConfig( + level=logging.INFO, + format='# %(message)s', + stream=sys.stderr +) +logger = logging.getLogger('crun.tests') + +# Export logger for use in test files +__all__ = ['logger', 'base_config', 'run_and_get_output', 'run_crun_command', 'run_crun_command_raw', + 'parse_proc_status', 'add_all_namespaces', 'tests_main', 'is_rootless', + 'is_cgroup_v2_unified', 'get_crun_feature_string', 'running_on_systemd', + 'get_tests_root', 'get_tests_root_status', 'get_init_path', 'get_crun_path'] + base_conf = """ { "ociVersion": "1.0.0", @@ -122,6 +137,17 @@ "noexec", "nodev" ] + }, + { + "destination": "/tmp", + "type": "tmpfs", + "source": "tmpfs", + "options": [ + "nosuid", + "nodev", + "mode=1777", + "size=65536k" + ] } ], "linux": { @@ -176,6 +202,10 @@ def run_all_tests(all_tests, allowed_tests): allowed_tests = allowed_tests.split() tests = {k: v for k, v in tests.items() if k in allowed_tests} + # Test timing thresholds + SLOW_TEST_THRESHOLD = 30.0 # seconds + VERY_SLOW_TEST_THRESHOLD = 60.0 # seconds + print("1..%d" % len(tests)) cur = 0 for k, v in tests.items(): @@ -189,22 +219,52 @@ def run_all_tests(all_tests, allowed_tests): ret = v() test_duration = time.time() - test_start_time + + # Check for slow tests and emit warnings + if test_duration > VERY_SLOW_TEST_THRESHOLD: + logger.warning("Test '%s' took %.3fs (>%.1fs very slow threshold)", + k, test_duration, VERY_SLOW_TEST_THRESHOLD) + elif test_duration > SLOW_TEST_THRESHOLD: + logger.warning("Test '%s' took %.3fs (>%.1fs slow threshold)", + k, test_duration, SLOW_TEST_THRESHOLD) + if ret == 0: print("ok %d - %s # %.3fs" % (cur, k, test_duration)) - elif ret == 77: - print("ok %d - %s #SKIP # %.3fs" % (cur, k, test_duration)) + elif ret == 77 or (isinstance(ret, tuple) and ret[0] == 77): + skip_reason = "" + if isinstance(ret, tuple) and len(ret) > 1 and ret[1]: + skip_reason = " " + str(ret[1]) + print("ok %d - %s #SKIP%s # %.3fs" % (cur, k, skip_reason, test_duration)) else: + actual_ret = ret[0] if isinstance(ret, tuple) else ret print("not ok %d - %s # %.3fs" % (cur, k, test_duration)) - sys.stderr.write("# Test '%s' failed with return code %d\n" % (k, ret)) + logger.error("Test '%s' failed with return code %d", k, actual_ret) except Exception as e: test_duration = time.time() - test_start_time - sys.stderr.write("# Test '%s' failed with exception after %.3fs:\n" % (k, test_duration)) + logger.error("Test '%s' failed with exception after %.3fs:", k, test_duration) + logger.error("Exception type: %s", type(e).__name__) + logger.error("Exception message: %s", str(e)) + + # Enhanced error details for subprocess errors + if hasattr(e, 'returncode'): + logger.error("Process return code: %d", e.returncode) + if hasattr(e, 'cmd'): + cmd_str = ' '.join(e.cmd) if isinstance(e.cmd, list) else str(e.cmd) + logger.error("Failed command: %s", cmd_str) if hasattr(e, 'output'): - sys.stderr.write("# Container output: %s\n" % str(e.output)) - sys.stderr.write("# Exception: %s\n" % str(e)) - sys.stderr.write("# Traceback:\n") + logger.error("Process output: %s", str(e.output)) + if hasattr(e, 'stderr') and e.stderr: + logger.error("Process stderr: %s", str(e.stderr)) + + # Environment information + logger.error("Working directory: %s", os.getcwd()) + logger.error("Test root: %s", get_tests_root()) + if 'TMPDIR' in os.environ: + logger.error("TMPDIR: %s", os.environ['TMPDIR']) + + logger.error("Traceback:") for line in traceback.format_exc().splitlines(): - sys.stderr.write("# %s\n" % line) + logger.error("%s", line) ret = -1 print("not ok %d - %s # %.3fs" % (cur, k, test_duration)) @@ -316,15 +376,15 @@ def run_and_get_output(config, detach=False, preserve_fds=None, pid_file=None, try: return subprocess.check_output(args, cwd=temp_dir, stdin=stdin, stderr=stderr, env=env, close_fds=False, umask=default_umask).decode(), id_container except subprocess.CalledProcessError as e: - sys.stderr.write("# Command failed: %s\n" % ' '.join(args)) - sys.stderr.write("# Working directory: %s\n" % temp_dir) - sys.stderr.write("# Container ID: %s\n" % id_container) - sys.stderr.write("# Return code: %d\n" % e.returncode) + logger.error("Command failed: %s", ' '.join(args)) + logger.error("Working directory: %s", temp_dir) + logger.error("Container ID: %s", id_container) + logger.error("Return code: %d", e.returncode) if e.output: - sys.stderr.write("# Output: %s\n" % e.output.decode('utf-8', errors='ignore')) - sys.stderr.write("# Config file saved at: %s\n" % config_path) + logger.error("Output: %s", e.output.decode('utf-8', errors='ignore')) + logger.error("Config file saved at: %s", config_path) if not keep: - sys.stderr.write("# Note: temporary directory will be cleaned up\n") + logger.error("Note: temporary directory will be cleaned up") raise def run_crun_command(args): @@ -334,10 +394,10 @@ def run_crun_command(args): try: return subprocess.check_output(cmd_args, close_fds=False).decode() except subprocess.CalledProcessError as e: - sys.stderr.write("# crun command failed: %s\n" % ' '.join(cmd_args)) - sys.stderr.write("# Return code: %d\n" % e.returncode) + logger.error("crun command failed: %s", ' '.join(cmd_args)) + logger.error("Return code: %d", e.returncode) if e.output: - sys.stderr.write("# Output: %s\n" % e.output.decode('utf-8', errors='ignore')) + logger.error("Output: %s", e.output.decode('utf-8', errors='ignore')) raise # Similar as run_crun_command but does not performs decode of output and relays error message for further matching @@ -348,8 +408,8 @@ def run_crun_command_raw(args): try: return subprocess.check_output(cmd_args, close_fds=False, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - sys.stderr.write("# crun command failed: %s\n" % ' '.join(cmd_args)) - sys.stderr.write("# Return code: %d\n" % e.returncode) + logger.error("crun command failed: %s", ' '.join(cmd_args)) + logger.error("Return code: %d", e.returncode) raise def running_on_systemd():