Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"unrecognized arguments" or "option names already added" error when running pytest inside a Docker container using Jenkins #3097

Open
ceridwen opened this issue Jan 8, 2018 · 18 comments
Labels
topic: config related to config handling, argument parsing and config file type: bug problem that needs to be addressed

Comments

@ceridwen
Copy link
Contributor

ceridwen commented Jan 8, 2018

This is a complicated set up, so bear with me.

I am trying to create a Jenkins pipeline job to run Pytest on Jenkins inside a Docker container. As far as I can tell, when Jenkins starts a Docker container, it does so with the working directory of that container set to a directory on the Jenkins filesystem, under /var/jenkins_home/<job name>/ by default. Thus, running the tests requires:

  1. Pull the Docker image, if necessary.
  2. Create the container using the image. (This gives me access to all the test dependencies, which are already installed inside the image.)
  3. Checkout the current source code for the tests from GIthub.
  4. Run pytest.
  5. Have Jenkins process the resulting jUnit XML.

Because Jenkins passes job parameters as environment variables and some other probably-not-relevant reasons, I need to use a front-end script, run_tests.py, to turn those variables into command-line arguments that pytest can understand. run_tests.py then calls pytest.main().

    pytest.main(pytest_args + test_args)

Here, pytest_args are arguments for pytest itself like -s, -k, or --tb=<type>. test_args are command-line arguments that I have configured in conftest.py using pytest_addoption() : these are the arguments created from environment variables. The layout of the project looks like:

repo_root/
repo_root/test_automation/
repo_root/test_automation/conftest.py
repo_root/test_automation/run_tests.py
repo_root/test_automation/test_files.py

This setup works outside Jenkins: I cd to test_automation/, run run_tests.py, it finds conftest.py, and everything is happy. When I run the Jenkins job that's supposedly executing this same steps with PYTEST_DEBUG set:

12:30:51     pytest_load_initial_conftests [hook]
12:30:51         early_config: <_pytest.config.Config object at 0x7f8e2f160c88>
12:30:51         args: [...]
12:30:51         parser: <_pytest.config.Parser object at 0x7f8e2f1717f0>
12:30:51       pytest_plugin_registered [hook]
12:30:51           plugin: <_pytest.capture.CaptureManager object at 0x7f8e2d6b2438>
12:30:51           manager: <_pytest.config.PytestPluginManager object at 0x7f8e2f5a5d30>
12:30:51       finish pytest_plugin_registered --> [] [hook]
12:30:51     finish pytest_load_initial_conftests --> [] [hook]
12:30:51     pytest_cmdline_preparse [hook]
12:30:51         config: <_pytest.config.Config object at 0x7f8e2f160c88>
12:30:51         args: [...]
12:30:51     finish pytest_cmdline_preparse --> [] [hook]
12:30:51 usage: run_tests.py [options] [file_or_dir] [file_or_dir] [...]
12:30:51 run_tests.py: error: unrecognized arguments: ...

Here, the "..." for the args stands for all the arguments in the first two cases, including the pytest-specific ones, and only the arguments configured in conftest.py for the unrecognized arguments, the last case. From looking at the logs for this same configuration that works outside Jenkins, I know that the pytest_load_initial_conftests log should look like:

    pytest_load_initial_conftests [hook]
        early_config: <_pytest.config.Config object at 0x10f512208>
        args: [...]
        parser: <_pytest.config.Parser object at 0x10f512d30>
      pytest_plugin_registered [hook]
          plugin: <_pytest.capture.CaptureManager object at 0x1101ec5f8>
          manager: <_pytest.config.PytestPluginManager object at 0x10f2caba8>
      finish pytest_plugin_registered --> [] [hook]
    find_module called for: conftest [assertion]
    rewriting conftest file: '/Users/user/test_automation/test_automation/conftest.py' [assertion]
    found cached rewritten pyc for '/Users/user/test_automation/test_automation/conftest.py' [assertion]
    find_module called for: run_tests [assertion]
    loaded conftestmodule <module 'conftest' (<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x11019c198>)> [pluginmanage]
      pytest_addoption [hook]
          parser: <_pytest.config.Parser object at 0x10f512d30>
          __multicall__: <_MultiCall 0 results, 0 meths, kwargs={'parser': <_pytest.config.Parser object at 0x10f512d30>, '__multicall__': <_MultiCall 0 results, 0 meths, kwargs={...}>}>
      finish pytest_addoption --> [] [hook]
      pytest_plugin_registered [hook]
          plugin: <module 'conftest' (<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x11019c198>)>
          manager: <_pytest.config.PytestPluginManager object at 0x10f2caba8>
      finish pytest_plugin_registered --> [] [hook]
    finish pytest_load_initial_conftests --> [] [hook] 

It looks a lot like pytest is not finding my conftest.py file, even though it's in the root directory (with respect to how pytest is being run). However, if I have Jenkins run python3 -m pytest --help in the same directory, with everything else the same, pytest will correctly process conftest.py and display the options that are configured there under the custom options: heading. I also tried manually forcing pytest to load conftest.py using -p. When I do that, pytest crashes with a traceback that suggests that conftest.py is already loaded:

14:24:15     pytest_load_initial_conftests [hook]
14:24:15         early_config: <_pytest.config.Config object at 0x7f5acc971780>
14:24:15         args: [...]
14:24:15         parser: <_pytest.config.Parser object at 0x7f5acc9802e8>
14:24:15       pytest_plugin_registered [hook]
14:24:15           plugin: <_pytest.capture.CaptureManager object at 0x7f5acab0ac50>
14:24:15           manager: <_pytest.config.PytestPluginManager object at 0x7f5acc5ff4e0>
14:24:15       finish pytest_plugin_registered --> [] [hook]
14:24:15     find_module called for: conftest [assertion]
14:24:15     rewriting conftest file: '/var/jenkins_home/workspace/Test Automation-pipeline/test_automation/conftest.py' [assertion]
14:24:15     found cached rewritten pyc for '/var/jenkins_home/workspace/Test Automation-pipeline/test_automation/conftest.py' [assertion]
14:24:15     loaded conftestmodule <module 'conftest' (<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f5acabb7668>)> [pluginmanage]
14:24:15       pytest_addoption [hook]
14:24:15           parser: <_pytest.config.Parser object at 0x7f5acc9802e8>
14:24:15           __multicall__: <_MultiCall 0 results, 0 meths, kwargs={'parser': <_pytest.config.Parser object at 0x7f5acc9802e8>, '__multicall__': <_MultiCall 0 results, 0 meths, kwargs={...}>}>
14:24:15 Traceback (most recent call last):
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 342, in _getconftestmodules
14:24:15     return self._path2confmods[path]
14:24:15 KeyError: local('/var/jenkins_home/workspace/Test Automation-pipeline/test_automation')
14:24:15 
14:24:15 During handling of the above exception, another exception occurred:
14:24:15 
14:24:15 Traceback (most recent call last):
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 373, in _importconftest
14:24:15     return self._conftestpath2mod[conftestpath]
14:24:15 KeyError: local('/var/jenkins_home/workspace/Test Automation-pipeline/test_automation/conftest.py')
14:24:15 
14:24:15 During handling of the above exception, another exception occurred:
14:24:15 
14:24:15 Traceback (most recent call last):
14:24:15   File "run_tests.py", line 435, in <module>
14:24:15     invoke_pytest(args, pytest_args)
14:24:15   File "run_tests.py", line 201, in invoke_pytest
14:24:15     pytest.main(pytest_args + test_args)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 49, in main
14:24:15     config = _prepareconfig(args, plugins)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 168, in _prepareconfig
14:24:15     pluginmanager=pluginmanager, args=args)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 745, in __call__
14:24:15     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 339, in _hookexec
14:24:15     return self._inner_hookexec(hook, methods, kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 302, in __call__
14:24:15     return outcome.get_result()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 279, in get_result
14:24:15     raise ex[1].with_traceback(ex[2])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
14:24:15     self.result = func()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 300, in <lambda>
14:24:15     outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 334, in <lambda>
14:24:15     _MultiCall(methods, kwargs, hook.spec_opts).execute()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 613, in execute
14:24:15     return _wrapped_call(hook_impl.function(*args), self.execute)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 250, in _wrapped_call
14:24:15     wrap_controller.send(call_outcome)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/helpconfig.py", line 68, in pytest_cmdline_parse
14:24:15     config = outcome.get_result()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 279, in get_result
14:24:15     raise ex[1].with_traceback(ex[2])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
14:24:15     self.result = func()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 614, in execute
14:24:15     res = hook_impl.function(*args)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 945, in pytest_cmdline_parse
14:24:15     self.parse(args)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 1116, in parse
14:24:15     self._preparse(args, addopts=addopts)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 1087, in _preparse
14:24:15     args=args, parser=self._parser)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 745, in __call__
14:24:15     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 339, in _hookexec
14:24:15     return self._inner_hookexec(hook, methods, kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 302, in __call__
14:24:15     return outcome.get_result()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 279, in get_result
14:24:15     raise ex[1].with_traceback(ex[2])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
14:24:15     self.result = func()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 300, in <lambda>
14:24:15     outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 334, in <lambda>
14:24:15     _MultiCall(methods, kwargs, hook.spec_opts).execute()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 613, in execute
14:24:15     return _wrapped_call(hook_impl.function(*args), self.execute)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 254, in _wrapped_call
14:24:15     return call_outcome.get_result()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 279, in get_result
14:24:15     raise ex[1].with_traceback(ex[2])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
14:24:15     self.result = func()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 614, in execute
14:24:15     res = hook_impl.function(*args)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 991, in pytest_load_initial_conftests
14:24:15     self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 328, in _set_initial_conftests
14:24:15     self._try_load_conftest(current)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 331, in _try_load_conftest
14:24:15     self._getconftestmodules(anchor)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 356, in _getconftestmodules
14:24:15     mod = self._importconftest(conftestpath)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 392, in _importconftest
14:24:15     self.consider_conftest(mod)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 415, in consider_conftest
14:24:15     self.register(conftestmodule, name=conftestmodule.__file__)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 264, in register
14:24:15     ret = super(PytestPluginManager, self).register(plugin, name)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 371, in register
14:24:15     hook._maybe_apply_history(hookimpl)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 768, in _maybe_apply_history
14:24:15     res = self._hookexec(self, [method], kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 339, in _hookexec
14:24:15     return self._inner_hookexec(hook, methods, kwargs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 302, in __call__
14:24:15     return outcome.get_result()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 279, in get_result
14:24:15     raise ex[1].with_traceback(ex[2])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
14:24:15     self.result = func()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 300, in <lambda>
14:24:15     outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 334, in <lambda>
14:24:15     _MultiCall(methods, kwargs, hook.spec_opts).execute()
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/vendored_packages/pluggy.py", line 614, in execute
14:24:15     res = hook_impl.function(*args)
14:24:15   File "/var/jenkins_home/workspace/Test Automation-pipeline/test_automation/conftest.py", line 19, in pytest_addoption
14:24:15     parser.addoption(option, *run_tests.TEST_OPTIONS[option][0], **run_tests.TEST_OPTIONS[option][1])
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 537, in addoption
14:24:15     self._anonymous.addoption(*opts, **attrs)
14:24:15   File "/usr/local/lib/python3.6/site-packages/_pytest/config.py", line 762, in addoption
14:24:15     raise ValueError("option names %s already added" % conflict)
14:24:15 ValueError: option names {'--<option'} already added

I tried passing the --debug option to pytest, but it also crashed with this traceback, so it didn't give me any more information.

  • pip list from inside the container:
boltons (17.2.0)
cachetools (2.0.1)
certifi (2017.11.5)
chardet (3.0.4)
ConfigArgParse (0.12.0)
decorator (4.1.2)
dictdiffer (0.7.0)
docker (2.7.0)
docker-pycreds (0.2.1)
dpath (1.4.0)
google-auth (1.2.1)
idna (2.6)
ipaddress (1.0.19)
Jinja2 (2.10)
jsonschema (2.6.0)
kubernetes (3.0.0)
MarkupSafe (1.0)
networkx (2.0)
openshift (0.3.4)
pip (9.0.1)
plumbum (1.6.5)
py (1.5.2)
pyasn1 (0.4.2)
pyasn1-modules (0.2.1)
pytest (3.2.0)
pytest-html (1.16.1)
pytest-metadata (1.5.1)
python-dateutil (2.6.1)
python-string-utils (0.6.0)
PyYAML (3.12)
requests (2.18.4)
rsa (3.4.2)
ruamel.yaml (0.15.35)
setuptools (36.6.0)
six (1.11.0)
swagger-parser (1.0.0)
swagger-spec-validator (2.1.0)
urllib3 (1.22)
websocket-client (0.40.0)
wheel (0.30.0)
  • I'm using Docker for Mac to run Docker on Mac OS X 10.12.6. (Docker for Mac uses Virtualbox to create a VM on Mac OS X.) Docker is version Version 17.09.1-ce-mac42 (21090).

  • Jenkins is also running in a Docker container, and from what I see the Docker socket is mounted inside Jenkins, so there's only one Docker instance running. Jenkins is version 2.89.2.

Is there anything else I can do to figure out what's going on here? I haven't been able to replicate the bug anywhere outside a Jenkins job. Even when I have Jenkins start the container, then use docker exec to log into, I can still run run_tests.py and it doesn't complain about unrecognized arguments.

@pytestbot
Copy link
Contributor

GitMate.io thinks the contributor most likely able to help you is @nicoddemus.

@pytestbot pytestbot added the type: bug problem that needs to be addressed label Jan 8, 2018
@nicoddemus
Copy link
Member

Hi @ceridwen,

Hmm that's a head scratcher... from the top of my head:

  1. Make sure you are running pytest from repo_root/test_automation/; as stated in the docs for pytest_addoption, only hook implementations in the root of the test suite will be found.

  2. Create repo_root/test_automation/pytest.ini to force the root of the test suite to repo_root/test_automation.

@nicoddemus nicoddemus added the topic: config related to config handling, argument parsing and config file label Jan 9, 2018
@ceridwen
Copy link
Contributor Author

ceridwen commented Jan 10, 2018

The Jenkins script cds to repo_root/test_automation/ before running run_tests.py, and I've checked that it's in the right place (or at least, in a place that looks right) with ls. run_tests.py and conftest.py are both there, and pytest --help seems to find conftest.py okay. I tried creating pytest.ini in repo_root/test_automation/ (after cd) with touch and echo '[pytest]' > pytest.ini, and it didn't change anything.

@nicoddemus
Copy link
Member

Hmm that's strange. Can you post the pytest output banner of the run?

@ceridwen
Copy link
Contributor Author

ceridwen commented Jan 10, 2018

I just tried explicitly passing one of the test files on the command line, rather than letting it do discovery, and that made it work. I assume the work around now is to explicitly pass all the tests on the command line. From the docs on rootdir: "Determine the common ancestor directory for the specified args that are recognised as paths that exist in the file system. If no such paths are found, the common ancestor directory is set to the current working directory." I suspect that passing it an explicit path for one of the test files is changing the effective root directory, but I have no idea why, or why it would have arrived at the wrong root directory in the one case but not the others.

@nicoddemus
Copy link
Member

nicoddemus commented Jan 10, 2018

And what if you pass . instead? Does that give the same results?

@ceridwen
Copy link
Contributor Author

Running it with . instead produces the same reults as passing an explicit test file: it discovers the test files and conftest.py correctly. I used os to check that the current working directory according to Python is the what I thought it was, and it is:

10:21:50 /var/jenkins_home/workspace/Test Automation-pipeline@2/test_automation

It's looking to me like it doesn't calculate the root directory correctly under some conditions, though I have no idea what they are. I can pass . as a workaround for now. Is there a way to get pytest to log what it thinks is the root directory?

@ceridwen
Copy link
Contributor Author

Both the "unrecognized arguments" error and "option names already added" ValueError come before the point when pytest would print the banner, so I don't have banners.

@nicoddemus
Copy link
Member

Unfortunately there are no logs, but here's the function which determines the rootdir:

pytest/_pytest/config.py

Lines 1326 to 1349 in 6fb46a0

def determine_setup(inifile, args, warnfunc=None):
dirs = get_dirs_from_args(args)
if inifile:
iniconfig = py.iniconfig.IniConfig(inifile)
try:
inicfg = iniconfig["pytest"]
except KeyError:
inicfg = None
rootdir = get_common_ancestor(dirs)
else:
ancestor = get_common_ancestor(dirs)
rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc)
if rootdir is None:
for rootdir in ancestor.parts(reverse=True):
if rootdir.join("setup.py").exists():
break
else:
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
if rootdir is None:
rootdir = get_common_ancestor([py.path.local(), ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == '/'
if is_fs_root:
rootdir = ancestor
return rootdir, inifile, inicfg or {}

We could add some trace functions in there. Any chance for you to use a development version inside the docker container? That would allow us to create a branch and add a bunch of trace statements to see where we get at.

@ceridwen
Copy link
Contributor Author

I can use a development branch inside the Docker container with pip's ability to pull from Git during dependency installation, if you create the branch. The only issue might be that because of the way pytest is currently resetting the root logger's level, with pytest 3.3 there's a tremendous amount of log-spam from my dependencies---I think the root directory determination occurs before the log-spam, though, so I can work around it.

@nicoddemus
Copy link
Member

@ceridwen you might set PYTEST_DEBUG and use the internal tracing facility:

pytest/_pytest/config.py

Lines 189 to 197 in 6fb46a0

if os.environ.get('PYTEST_DEBUG'):
err = sys.stderr
encoding = getattr(err, 'encoding', 'utf8')
try:
err = py.io.dupfile(err, encoding=encoding)
except Exception:
pass
self.trace.root.setwriter(err.write)
self.enable_tracing()

You will need to pass self.trace to determine_setup though.

@ceridwen
Copy link
Contributor Author

ceridwen commented Jan 13, 2018

I took your suggestion and put some tracing in a branch.

def determine_setup(inifile, args, warnfunc=None, trace=None):
    dirs = get_dirs_from_args(args)
    trace("initial dirs %s" % dirs)
    if inifile:
        iniconfig = py.iniconfig.IniConfig(inifile)
        try:
            inicfg = iniconfig["pytest"]
        except KeyError:
            inicfg = None
        rootdir = get_common_ancestor(dirs)
        trace("inifile found, %s" % rootdir)
    else:
        ancestor = get_common_ancestor(dirs)
        trace("inifile not found, %s" % ancestor)
        rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc)
        trace("%s %s" % (rootdir, inicfg))
        if rootdir is None:
            trace("rootdir is None")
            for rootdir in ancestor.parts(reverse=True):
                trace("Try rootdir, %s" % rootdir)
                if rootdir.join("setup.py").exists():
                    break
            else:
                rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
                trace("Via inicfg rootdir %s" % rootdir)
                if rootdir is None:
                    rootdir = get_common_ancestor([py.path.local(), ancestor])
                    trace("Common ancestor, %s" % rootdir)
                    is_fs_root = os.path.splitdrive(str(rootdir))[1] == '/'
                    if is_fs_root:
                        rootdir = ancestor
        trace("Final rootdir %s" % rootdir)
    return rootdir, inifile, inicfg or {}

With this in place, this is what the relevant log sections looks:

17:33:53     finish pytest_addhooks --> [] [hook]
17:33:53   initial dirs [local('/var/jenkins_home/workspace/Test Automation-pipeline@tmp/secretFiles/844df185-1cc8-4dbb-8abb-bad8087b3b55')] [config]
17:33:53   inifile not found, /var/jenkins_home/workspace/Test Automation-pipeline@tmp/secretFiles/844df185-1cc8-4dbb-8abb-bad8087b3b55 [config]
17:33:53   None None [config]
17:33:53   rootdir is None [config]
17:33:53   Try rootdir, /var/jenkins_home/workspace/Test Automation-pipeline@tmp/secretFiles/844df185-1cc8-4dbb-8abb-bad8087b3b55 [config]
17:33:53   Try rootdir, /var/jenkins_home/workspace/Test Automation-pipeline@tmp/secretFiles [config]
17:33:53   Try rootdir, /var/jenkins_home/workspace/Test Automation-pipeline@tmp [config]
17:33:53   Try rootdir, /var/jenkins_home/workspace [config]
17:33:53   Try rootdir, /var/jenkins_home [config]
17:33:53   Try rootdir, /var [config]
17:33:53   Try rootdir, / [config]
17:33:53   Via inicfg rootdir None [config]
17:33:53   py.path.local(), /var/jenkins_home/workspace/Test Automation-pipeline/test_automation [config]
17:33:53   Common ancestor, /var/jenkins_home/workspace [config]
17:33:53   Final rootdir /var/jenkins_home/workspace [config]
17:33:53   installed rewrite import hook [assertion]

I don't have the first clue has to how that secrets file ends up being passed into determine_setup. I suspect but don't know that it's the file that I'm passing to pytest using '--system-under-test-config', '****' in the list of string passed to pytest.main, to be processed using pytest_addoption. Once it's there, py.path.local() returns the actual working directory, /var/jenkins_home/workspace/Test Automation-pipeline/test_automation, and the common ancestor of that with /var/jenkins_home/workspace/Test Automation-pipeline@tmp/secretFiles/844df185-1cc8-4dbb-8abb-bad8087b3b55 is /var/jenkins_home/workspace, which doesn't contain conftest.py. The actual list passed to pytest.main is the sum of:

['--debug', '--trace-config', '-k', 'not test', '--junitxml', 'report.xml']
['--system-under-test-config', '****', '--system-under-test-context', 'vk8s2', '--external-router-user', '****', '--external-router-password', '****, '--services-scale', 100, '--pods-scale', 33, '--redeploy-flavor', 'kubernetes-1.7']

Pytest believes that its arguments are:

17:33:53       args: ['--debug', '--trace-config', '-k', 'not test', '--junitxml', 'report.xml', '--system-under-test-config', '****', '--system-under-test-context', 'vk8s2', '--external-router-user', '****', '--external-router-password', '****, '--services-scale', 100, '--pods-scale', 33, '--redeploy-flavor', 'kubernetes-1.7']

It looks like the command parsing might be the issue here. Have any idea what the problem is?

@nicoddemus
Copy link
Member

Hi @ceridwen sorry for the delay.

Indeed seems like it might be problem related to argument parsing. Can you try passing options using = instead? For instance, instead of ['--foo', '/some/path'], pass ['--foo=/some/path'].

Ref: #1642

@ceridwen
Copy link
Contributor Author

I tried passing the arguments using =, except for the -k and the boolean options that don't take an argument, and the bug didn't show up.

@nicoddemus
Copy link
Member

nicoddemus commented Jan 16, 2018

I've been digging in the issue tracker because this problem seems familiar: #961, #1435, #906.

I think the cause is that the new options are being configured by a conftest.py file, the problem is that the rootdir is determined before we load conftests so the paths passed to pytest are considered "unknown" at that stage so they are used by determine_setup to find the rootdir (see #906 (comment)).

Not sure if this can be fixed, because pytest can't know if --system-under-test-config takes a parameter or not when it calls determine_setup; determine_setup needs to be called at that point because we need to have a rootdir defined to be able load conftests...

@ceridwen
Copy link
Contributor Author

I think I understand the issue now. I think it might be worth discussing whether there's a more transparent way to handle command-line options for pytest, like maybe splitting out command-line options into their own file that's loaded eagerly? That said, this is also a documentation problem, because the existing documentation (https://docs.pytest.org/en/latest/example/simple.html, https://docs.pytest.org/en/latest/example/parametrize.html, https://docs.pytest.org/en/latest/example/markers.html) uses pytest_addoption in conftest.py without mentioning that passing a path in any configured options can cause discovery to go haywire. Thoughts?

@nicoddemus
Copy link
Member

Definitely agree on the documentation problem.

I agree that we should discuss how we can better handle options from conftest.py files, this is definitely a source of confusion and there are edge cases to handle, for example with determine_setup as your problem demonstrates.

@blockjon
Copy link

I am also running a Docker container on a Jenkins agent and have encountered this issue.

The docker statement looks roughly like this:

docker run <image> ./myscript.sh

inside that script, I do something like this:

python runtests.py \
    --junitxml=artifacts/junit.xml \
    --durations=30 \
    --timeout=40 \
    --use-settings-local \
    --deterministic \
    --simtime 1466034570 \
    --seed 23456260713 \
    --create-db \
    --unreliable-only | ts

I am seeing the problem described in this ticket about 50% of the time. Not sure what I should do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: config related to config handling, argument parsing and config file type: bug problem that needs to be addressed
Projects
None yet
Development

No branches or pull requests

4 participants