Received: by 10.223.176.5 with SMTP id f5csp1291173wra; Wed, 7 Feb 2018 16:37:15 -0800 (PST) X-Google-Smtp-Source: AH8x225Pi/PpBrNaNEnTkazH7RO9awU2lum1fU5ZPA1FSdd0sfaR5St78J0M5nBzsSuLd11cfCmt X-Received: by 2002:a17:902:3a3:: with SMTP id d32-v6mr7634691pld.193.1518050235628; Wed, 07 Feb 2018 16:37:15 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1518050235; cv=none; d=google.com; s=arc-20160816; b=B3EstGVA71uCbvNu7LgLQ/1E5i4BK7JhPH8ROfAD7RI4PGM5F+lnz2FM0faaeEk1iy VOCET6xrWooqZrBiGOoPmrVUbjoqvygsu3LmGTxvB3MeAeIXMWhv5NzPYhOZOQdtBOCv PKdnpMYI0a2lKD4E06rRjNqHm035E/fe1Ntgr6pCIFcn0u4kGfKuX/s9giH0VVtdRUID hNV00SwfyMbLAPZ75Q6s4+hnVVr34nLlpzeLq+JR2bdIarm9baPET3VbMDpaz7L/CS6o VBs0c7RHlpMoA+Ho2AP7MOedblKyFPqzY/lJ+g53BV3x5jsT9KmCbcnlTRw7ZNG9grol 18Yw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=list-id:precedence:sender:cc:to:subject:message-id:date:from :references:in-reply-to:mime-version:dkim-signature :arc-authentication-results; bh=9SlEnUWNkz9+uAMcNjpEj5fVFcLgSnI2ZAmbY1iRNcY=; b=V+ZSOJszrDMODpGjc00IAqWnV9MzHRhzqp1ZwtVB/3lXy0QR2LiRlkvqVN1nurpjBL MJNWiVm+a2H3FlwuktD39fdRVtpuIbFzKgwizyuJ55WZx27n/uGClGOlEyqVzrTY4ynW Khx1JKuH1jdOHa7juSTe0Cbwjhog+aqW6Fx9nvhrwOHGMyafY3jElU7TlQ+SsHtw/7Xf RHlR6U+eRqAbL6RVXRoF/kGBAKAKvsAYG3hhAfLUFPlT8zgQb4kWkKPDR8SufFS//JbC Dv3Ash/3m4i2ewmhHxT2eIyILr/B6cBVBMzw/QsJ4lsPKQR5iemL6J4fMOo+5MxB6rBy Uk9A== ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@gmail.com header.s=20161025 header.b=SVEWGFN1; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Return-Path: Received: from vger.kernel.org (vger.kernel.org. [209.132.180.67]) by mx.google.com with ESMTP id g1si1895522pfj.237.2018.02.07.16.37.01; Wed, 07 Feb 2018 16:37:15 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) client-ip=209.132.180.67; Authentication-Results: mx.google.com; dkim=pass header.i=@gmail.com header.s=20161025 header.b=SVEWGFN1; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1751998AbeBHAgK (ORCPT + 99 others); Wed, 7 Feb 2018 19:36:10 -0500 Received: from mail-vk0-f54.google.com ([209.85.213.54]:34253 "EHLO mail-vk0-f54.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1750987AbeBHAf7 (ORCPT ); Wed, 7 Feb 2018 19:35:59 -0500 Received: by mail-vk0-f54.google.com with SMTP id f186so1748926vkc.1; Wed, 07 Feb 2018 16:35:58 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=mime-version:in-reply-to:references:from:date:message-id:subject:to :cc; bh=9SlEnUWNkz9+uAMcNjpEj5fVFcLgSnI2ZAmbY1iRNcY=; b=SVEWGFN1ta6YR7dtsYDC7Zk197AGDjGUdObZni8u2bxvR0T54apFNr6oC2J1OAt6vS 6TV2zroHYeghPh851rLg38bSHvC29ywLOcLV0f4M8InFKC+c9iSl5dpddckjhsjwmojp d2g70d6/8kbEqOy57yTxxodzL5nGeUcrcZ8l01mEqaNljbzreNKN9UUrFRC9KGOzDsRQ opo/KFy3A0K5QDIAcIbDFLi8ouH+Vqo4AV0xN7hStLsBAKy4yh75Zz9q8/+fgguIiIDn Jn3RzvVdTFtTxVU2UrxTeHVMVk8LAKRsFCzVO1O1fezBz9a/4kheyt4Sc8ZKCPMNdxH0 cxtw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:in-reply-to:references:from:date :message-id:subject:to:cc; bh=9SlEnUWNkz9+uAMcNjpEj5fVFcLgSnI2ZAmbY1iRNcY=; b=FBiU94ac9IiDY0a0Dz0ZA4EiIYN8q8oIHxA0QgNj8xBgd0d6KKDaVT6FRPRgh7LqpJ 3wq4f0SkVq0sSWlqUjuWmRwwG8t5nFd68Blbp3NudDZjUq4FUbv0iM/RfG89d9/TF4jq 4LRDIoeIbs2XT5Ytw4N1TnKqdYSHlo+cZeeuAnJkmFcn4pLyMc5GNZvvv32FU5n+NCNl SHmjKOj8qMMvkj/GXmp7+WwyLntxMsJ5lSmjOujeX3/Vn4avBvWP3QKtaVJxU9tQ1PqP yqLT66h/18mFYy3SkKkrRrbdPldQrz917vKpEH+rI6K4BXcabMNglaKB/zQVYoNBzs4Z xh9Q== X-Gm-Message-State: APf1xPA7UrKJGPSLcyZTDqrJ2+2/a1qFI8HPZVvNdOy3JCYgQeF7Tt46 yWbppbP+FggY+pdaTfqCVR1BEHACCrILCNkOpWk= X-Received: by 10.31.15.149 with SMTP id 143mr6540214vkp.126.1518050158091; Wed, 07 Feb 2018 16:35:58 -0800 (PST) MIME-Version: 1.0 Received: by 10.103.70.21 with HTTP; Wed, 7 Feb 2018 16:35:57 -0800 (PST) In-Reply-To: <1517877294-4826-8-git-send-email-yamada.masahiro@socionext.com> References: <1517877294-4826-1-git-send-email-yamada.masahiro@socionext.com> <1517877294-4826-8-git-send-email-yamada.masahiro@socionext.com> From: Ulf Magnusson Date: Thu, 8 Feb 2018 01:35:57 +0100 Message-ID: Subject: Re: [PATCH 07/14] kconfig: test: add framework for Kconfig unit-tests To: Masahiro Yamada Cc: Linux Kbuild mailing list , Greg Kroah-Hartman , Andrew Morton , Nicolas Pitre , "Luis R . Rodriguez" , Randy Dunlap , Sam Ravnborg , Michal Marek , Linus Torvalds , Borislav Petkov , Linux Kernel Mailing List , Thomas Gleixner , Yaakov Selkowitz , Marc Herbert Content-Type: text/plain; charset="UTF-8" Sender: linux-kernel-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org On Tue, Feb 6, 2018 at 1:34 AM, Masahiro Yamada wrote: > I admit various parts in Kconfig are cryptic and need refactoring, > but at the same time, I fear regressions. > > There are several subtle corner cases where it is difficult to notice > breakage. It is time to add unit-tests. > > Here is a simple framework based on pytest. The conftest.py provides > a fixture useful to run commands such as 'oldaskconfig' etc. and > to compare the resulted .config, stdout, stderr with expectations. > > How to add test cases? > ---------------------- > > For each test case, you should create a subdirectory under > scripts/kconfig/tests/ (so test cases are seperated from each other). > Every test case directory must contain the following files: > > - __init__.py: describes test functions > - Kconfig: the top level Kconfig file for this test > > To do a useful job, test cases generally need additional data like > input .config and information about expected results. > > How to run tests? > ----------------- > > You need python3 and pytest. Then, run "make testconfig". > O= option is supported. If V=1 is given, details logs during tests > are displayed. > > Signed-off-by: Masahiro Yamada > --- > > scripts/kconfig/Makefile | 8 ++ > scripts/kconfig/tests/conftest.py | 255 ++++++++++++++++++++++++++++++++++++++ > scripts/kconfig/tests/pytest.ini | 6 + > 3 files changed, 269 insertions(+) > create mode 100644 scripts/kconfig/tests/conftest.py > create mode 100644 scripts/kconfig/tests/pytest.ini > > diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile > index cb3ec53..c5d1d1a 100644 > --- a/scripts/kconfig/Makefile > +++ b/scripts/kconfig/Makefile > @@ -135,6 +135,14 @@ PHONY += tinyconfig > tinyconfig: > $(Q)$(MAKE) -f $(srctree)/Makefile allnoconfig tiny.config > > +# CHECK: -o cache_dir= working? > +PHONY += testconfig > +testconfig: $(obj)/conf > + $(PYTHON3) -B -m pytest $(srctree)/$(src)/tests \ > + -o cache_dir=$(abspath $(obj)/tests/.cache) \ > + $(if $(findstring 1,$(KBUILD_VERBOSE)),--capture=no) > +clean-dirs += tests/.cache > + > # Help text used by make help > help: > @echo ' config - Update current config utilising a line-oriented program' > diff --git a/scripts/kconfig/tests/conftest.py b/scripts/kconfig/tests/conftest.py > new file mode 100644 > index 0000000..f0f3237 > --- /dev/null > +++ b/scripts/kconfig/tests/conftest.py > @@ -0,0 +1,255 @@ > +# SPDX-License-Identifier: GPL-2.0 > +# > +# Copyright (C) 2018 Masahiro Yamada > +# > + > +import os > +import pytest > +import shutil > +import subprocess > +import tempfile > + > +conf_path = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf')) > + > +class Conf: > + > + def __init__(self, request): > + """Create a new Conf object, which is a scripts/kconfig/conf > + runner and result checker. > + > + Arguments: > + request - object to introspect the requesting test module > + """ > + > + # the directory of the test being run > + self.test_dir = os.path.dirname(str(request.fspath)) > + > + def __run_conf(self, mode, dot_config=None, out_file='.config', > + interactive=False, in_keys=None, extra_env={}): > + """Run scripts/kconfig/conf > + > + mode: input mode option (--oldaskconfig, --defconfig= etc.) > + dot_config: the .config file for input. > + out_file: file name to contain the output config data. > + interactive: flag to specify the interactive mode. > + in_keys: key inputs for interactive modes. > + extra_env: additional environment. > + """ > + > + command = [conf_path, mode, 'Kconfig'] > + > + # Override 'srctree' environment to make the test as the top directory > + extra_env['srctree'] = self.test_dir > + > + # scripts/kconfig/conf is run in a temporary directory. > + # This directory is automatically removed when done. > + with tempfile.TemporaryDirectory() as temp_dir: > + > + # if .config is given, copy it to the working directory > + if dot_config: > + shutil.copyfile(os.path.join(self.test_dir, dot_config), > + os.path.join(temp_dir, '.config')) > + > + ps = subprocess.Popen(command, > + stdin=subprocess.PIPE, > + stdout=subprocess.PIPE, > + stderr=subprocess.PIPE, > + cwd=temp_dir, > + env=dict(os.environ, **extra_env)) > + > + # If user key input is specified, feed it into stdin. > + if in_keys: > + ps.stdin.write(in_keys.encode('utf-8')) > + > + while ps.poll() == None: > + # For interactive modes such as 'make config', 'make oldconfig', > + # send 'Enter' key until the program finishes. > + if interactive: > + ps.stdin.write(b'\n') > + > + self.retcode = ps.returncode > + self.stdout = ps.stdout.read().decode() > + self.stderr = ps.stderr.read().decode() > + > + # Retrieve the resulted config data only when .config is supposed > + # to exist. If the command fails, the .config does not exist. > + # 'make listnewconfig' does not produce .config in the first place. > + if self.retcode == 0 and out_file: > + with open(os.path.join(temp_dir, out_file)) as f: > + self.config = f.read() > + else: > + self.config = None > + > + # Logging: > + # Pytest captures the following information by default. In failure > + # of tests, the captured log will be displayed. This will be useful to > + # figure out what has happened. > + > + print("command: {}\n".format(' '.join(command))) > + print("retcode: {}\n".format(self.retcode)) > + > + if dot_config: > + print("input .config:".format(dot_config)) > + > + print("stdout:") > + print(self.stdout) > + print("stderr:") > + print(self.stderr) > + > + if self.config is not None: > + print("output of {}:".format(out_file)) > + print(self.config) > + > + return self.retcode > + > + def oldaskconfig(self, dot_config=None, in_keys=None): > + """Run oldaskconfig (make config) > + > + dot_config: the .config file for input (optional). > + in_key: key inputs (optional). > + """ > + return self.__run_conf('--oldaskconfig', dot_config=dot_config, > + interactive=True, in_keys=in_keys) > + > + def oldconfig(self, dot_config=None, in_keys=None): > + """Run oldconfig > + > + dot_config: the .config file for input (optional). > + in_key: key inputs (optional). > + """ > + return self.__run_conf('--oldconfig', dot_config=dot_config, > + interactive=True, in_keys=in_keys) > + > + def defconfig(self, defconfig): > + """Run defconfig > + > + defconfig: the defconfig file for input. > + """ > + defconfig_path = os.path.join(self.test_dir, defconfig) > + return self.__run_conf('--defconfig={}'.format(defconfig_path)) > + > + def olddefconfig(self, dot_config=None): > + """Run olddefconfig > + > + dot_config: the .config file for input (optional). > + """ > + return self.__run_conf('--olddefconfig', dot_config=dot_config) > + > + def __allconfig(self, foo, all_config): > + """Run all*config > + > + all_config: fragment config file for KCONFIG_ALLCONFIG (optional). > + """ > + if all_config: > + all_config_path = os.path.join(self.test_dir, all_config) > + extra_env = {'KCONFIG_ALLCONFIG': all_config_path} > + else: > + extra_env = {} > + > + return self.__run_conf('--all{}config'.format(foo), extra_env=extra_env) > + > + def allyesconfig(self, all_config=None): > + """Run allyesconfig > + """ > + return self.__allconfig('yes', all_config) > + > + def allmodconfig(self, all_config=None): > + """Run allmodconfig > + """ > + return self.__allconfig('mod', all_config) > + > + def allnoconfig(self, all_config=None): > + """Run allnoconfig > + """ > + return self.__allconfig('no', all_config) > + > + def alldefconfig(self, all_config=None): > + """Run alldefconfig > + """ > + return self.__allconfig('def', all_config) > + > + def savedefconfig(self, dot_config): > + """Run savedefconfig > + """ > + return self.__run_conf('--savedefconfig', out_file='defconfig') > + > + def listnewconfig(self, dot_config=None): > + """Run listnewconfig > + """ > + return self.__run_conf('--listnewconfig', dot_config=dot_config, > + out_file=None) > + > + # checkers > + def __read_and_compare(self, compare, expected): > + """Compare the result with expectation. > + > + Arguments: > + compare: function to compare the result with expectation > + expected: file that contains the expected data > + """ > + with open(os.path.join(self.test_dir, expected)) as f: > + expected_data = f.read() > + print(expected_data) > + return compare(self, expected_data) > + > + def __contains(self, attr, expected): > + print("{0} is expected to contain '{1}':".format(attr, expected)) > + return self.__read_and_compare(lambda s, e: getattr(s, attr).find(e) >= 0, > + expected) > + > + def __matches(self, attr, expected): > + print("{0} is expected to match '{1}':".format(attr, expected)) > + return self.__read_and_compare(lambda s, e: getattr(s, attr) == e, > + expected) > + > + def config_contains(self, expected): > + """Check if resulted configuration contains expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__contains('config', expected) > + > + def config_matches(self, expected): > + """Check if resulted configuration exactly matches expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__matches('config', expected) > + > + def stdout_contains(self, expected): > + """Check if resulted stdout contains expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__contains('stdout', expected) > + > + def stdout_matches(self, cmp_file): > + """Check if resulted stdout exactly matches expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__matches('stdout', expected) > + > + def stderr_contains(self, expected): > + """Check if resulted stderr contains expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__contains('stderr', expected) > + > + def stderr_matches(self, cmp_file): > + """Check if resulted stderr exactly matches expected data. > + > + Arguments: > + expected: file that contains the expected data. > + """ > + return self.__matches('stderr', expected) > + > +@pytest.fixture(scope="module") > +def conf(request): > + return Conf(request) > diff --git a/scripts/kconfig/tests/pytest.ini b/scripts/kconfig/tests/pytest.ini > new file mode 100644 > index 0000000..07b94e0 > --- /dev/null > +++ b/scripts/kconfig/tests/pytest.ini > @@ -0,0 +1,6 @@ > +[pytest] > +addopts = --verbose > +# Pytest requires that test files have unique names, because pytest imports > +# them as top-level modules. It is silly to prefix or suffix a test file with > +# the directory name that contains it. Use __init__.py for all test files. > +python_files = __init__.py > -- > 2.7.4 > Reviewed-by: Ulf Magnusson