Skip to content

Capture: avoid using SysCapture with FDCapture #1585

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

Closed
hannes-ucsc opened this issue Jun 2, 2016 · 17 comments
Closed

Capture: avoid using SysCapture with FDCapture #1585

hannes-ucsc opened this issue Jun 2, 2016 · 17 comments
Labels
plugin: capture related to the capture builtin plugin

Comments

@hannes-ucsc
Copy link

Sorry for the blunt title, but I think the capture manager is implemented badly. The proper way to redirect one of the standard file handles, say stdout, is to os.close(sys.stdout.fileno()) and then immediately call os.open() or os.dup() to open a different file, but using the same file handle. The key is that on any modern Unix, open() and dup() are guaranteed to return the index of lowest available file descriptor table entry. If you os.close(1) and then immediately call open() or dup() after that, they are guaranteed to return 1. This is how Unix shells have implemented the redirection operators >, < and | since the 1970s. The beauty of this approach is that you won't have to touch sys.stdin at all because it's fileno() will remain the same. It is completely transparent to the higher layers.

The reason I think this should be fixed is that some important software (Fabric, for example) expects sys.stdout to be backed by a real file handle. As PyTest's capture is currently implemented, I can only run tests involving Fabric with --capture=no. But I need to run tests in parallel and with --junitxml, so disabling capture is not really an option.

I looked at capture.py and I am a a bit overwhelmed by its complexity right now, otherwise I would submit a PR. I think most of the complexity stems from the ability to temporarily suspend the capturing, which admittedly would be hard to do with the close()-then-open() approach. Maybe this technique can be implemented as --capture=raw and it would simply not support the temporary suspension of capture.

@RonnyPfannschmidt
Copy link
Member

i have to admit i am tempted to close this as offensively unactionable

also there is no need to degrade into non-suspendable capture, since dup2 has correct non-guessing control and is actually used to keep the real stdout around for real output of the test process in addition to managing closing the target fd should it be open

now if you take a deeper look into the FDCapture, which is the default, on every os that has dup/dup2

it does simply use dup2 to manage suspend/resume and uses a temporary file

i suspect what you really want is not a raw or a FD capture, but a PTY Capture that has controlling fd and a controlled fd, the output/input is bidirectional between those - and that mechanism needs a background worker process/thread to transfer the data

@hannes-ucsc
Copy link
Author

Why does FDCapture also use SysCapture? The documentation presents them as alternatives. It's the messing with sys.stdin and sys.stderr that causes Fabric to balk.

WIth FDCapture, is sys.stdout.fileno() == 1 when capture is enabled?

I need capture for --junixml and I need to use Fabric. The way things currently are, I can not use PyTest.

@hannes-ucsc
Copy link
Author

BTW, I currently use Nose and its capture works fine with Fabric but it is broken in other ways that make me want to switch to PyTest.

@RonnyPfannschmidt
Copy link
Member

i see what you mean now, so what you want is a fd --only capture or a fileno attribute on stdin/out

@hannes-ucsc
Copy link
Author

I guess so. But my question's weren't rhetorical. I really don't know the answer to them.

And here's another one: Why is SysCapture even needed? If you replace file descriptors 0, 1 and 2 everything should just work. It is the commonly accepted way to redirect the standard streams and if you redirect them that way, no other means are needed.

@RonnyPfannschmidt
Copy link
Member

there is a legacy reason spanning back to about pytest 1.x i think
im just about to add a fileno method, which will return the targetfd if the backing file has a fileno

@RonnyPfannschmidt
Copy link
Member

i found a different interaction bug

plain pytest has correct filenos

pytest-xdist uses stdin/out to do process control
thus killing the usual methods

the interaction between stdcapture and fdcapture is a bit unwanted and impossible to fix off-hand without rewriting xdist without execnet

@hannes-ucsc
Copy link
Author

Just FYI, I'm not using xdist right now. And with --capture=fd, I get

ValueError: redirected Stdin is pseudofile, has no fileno()

@RonnyPfannschmidt
Copy link
Member

then there is another bug (i just made some testcases to verify that there is a fileno

please show the full traceback

@hannes-ucsc
Copy link
Author

Error Message

ValueError: redirected Stdin is pseudofile, has no fileno()
Stacktrace

self = <cgcloud.core.test.test_core.CoreTests testMethod=test_generic_fedora_22_box>

>   test_method = (lambda _box_cls: lambda self: cls._test( self, _box_cls ))( box_cls )

src/cgcloud/core/test/test_core.py:44: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/cgcloud/core/test/test_core.py:22: in _test
    self._cgcloud( 'create', role )
src/cgcloud/core/test/__init__.py:108: in _cgcloud
    main( args )
src/cgcloud/core/cli.py:49: in main
    app.run( args )
../lib/src/cgcloud/lib/util.py:308: in run
    command.run( options )
src/cgcloud/core/commands.py:81: in run
    return self.run_in_ctx( options, ctx )
src/cgcloud/core/commands.py:105: in run_in_ctx
    return self.run_on_role( options, ctx, role )
src/cgcloud/core/commands.py:124: in run_on_role
    return self.run_on_box( options, box )
src/cgcloud/core/commands.py:564: in run_on_box
    box.create( spec, **self.creation_kwargs( options, box ) )
src/cgcloud/core/box.py:779: in create
    self.ctx.ec2.terminate_instances( unfinished_ids_list )
/usr/lib/python2.7/contextlib.py:24: in __exit__
    self.gen.next()
src/cgcloud/core/box.py:746: in create
    executor( _wait_ready, (self,) )
src/cgcloud/core/box.py:688: in executor
    f( *args )
src/cgcloud/core/box.py:737: in _wait_ready
    self.ctx.ec2.terminate_instances( [ box.instance_id ] )
/usr/lib/python2.7/contextlib.py:24: in __exit__
    self.gen.next()
src/cgcloud/core/box.py:732: in _wait_ready
    box._wait_ready( { 'pending' }, first_boot=True )
src/cgcloud/core/box.py:1180: in _wait_ready
    self._on_instance_ready( first_boot )
src/cgcloud/core/generic_boxes.py:276: in _on_instance_ready
    self.__fix_stupid_locale_problem( )
src/cgcloud/core/box.py:80: in wrapper
    return box._execute_task( task, user )
src/cgcloud/core/box.py:1139: in _execute_task
    return execute( task, hosts=[ host ] )[ host ]
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/tasks.py:387: in execute
    multiprocessing
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/tasks.py:277: in _execute
    return task.run(*args, **kwargs)
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/tasks.py:174: in run
    return self.wrapped(*args, **kwargs)
src/cgcloud/core/generic_boxes.py:288: in __fix_stupid_locale_problem
    sudo( 'localedef -c -i en_US -f UTF-8 en_US.UTF-8' )
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/network.py:649: in host_prompting_wrapper
    return func(*args, **kwargs)
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/operations.py:1118: in sudo
    stderr=stderr, timeout=timeout, shell_escape=shell_escape,
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/operations.py:928: in _run_command
    stderr=stderr, timeout=timeout)
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/operations.py:812: in _execute
    worker.raise_if_needed()
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/thread_handling.py:12: in wrapper
    callable(*args, **kwargs)
../venv/local/lib/python2.7/site-packages/Fabric-1.10.3-py2.7.egg/fabric/io.py:231: in input_loop
    r, w, x = select([sys.stdin], [], [], 0.0)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <_pytest.capture.DontReadFromInput instance at 0x2b60b8e54ea8>

    def fileno(self):
>       raise ValueError("redirected Stdin is pseudofile, has no fileno()")
E       ValueError: redirected Stdin is pseudofile, has no fileno()

../venv/local/lib/python2.7/site-packages/_pytest/capture.py:439: ValueError
Standard Output

[52.40.170.27] Executing task '__fix_stupid_locale_problem'
[52.40.170.27] sudo: localedef -c -i en_US -f UTF-8 en_US.UTF-8
Standard Error

INFO: Running ('create', 'generic-fedora-22-box')
INFO: Using zone 'us-west-2a' and namespace '/test/JozTIE--5do/'
INFO: Looking up default image for role generic-fedora-22-box and virtualization type hvm, ... 
INFO: ... found ami-9a36d5fa.
INFO: Setting up security group ...
INFO: ... finished setting up sg-61cf0e07.
INFO: Creating t2.micro instance(s) ... 
INFO: ... got InvalidParameterValue, trying again in 5s ...
INFO: ... got InvalidParameterValue, trying again in 5s ...
INFO: ... created i-c301c018.
INFO: Tagging instance ... 
INFO: ... instance tagged {'cluster_name': u'i-c301c018', 'generation': '0', 'cluster_ordinal': '0', 'Name': 'test_JozTIE--5do_generic-fedora-22-box', 'enable_agent': 'True'}.
INFO: ... waiting for instance i-c301c018 ... 
INFO: ... running, waiting for assignment of public IP ... 
INFO: ... assigned, waiting for SSH port ... 
INFO: ... open ... 
INFO: ... testing SSH ... 
INFO: ... SSH working ..., 
INFO: ... instance ready.
WARNING: Terminating instance ...

@RonnyPfannschmidt
Copy link
Member

now that's way more comprehensible

we dont have support for stdin capture using a fd, such tests must use pexpect or own pty control
else you pretty much need to enter text while doing a automated testrun

feel free to do a custom monkeypatch there

also please next time do a humane bugreport and include a traceback instead of claiming everything is broken following up initially with unrelated miss-leading examples

that just cost me the time to finish my marker refactor today for basically no reason and i am pretty unhappy about that

@hannes-ucsc
Copy link
Author

Sorry, I glanced over the fact that its complaining about stdin. How very stupid of me.

I still don't get why PyTest can't just replace 0, 1, 2 and leave sys.stdin, sys.stdout and sys.stderr as is.

How can I prevent PyTest from replacing sys.stdin?

@RonnyPfannschmidt
Copy link
Member

for now i would suggest to open a pty and monkeypatching stdin.fileno() as a hack
im not sure how to elevate this (since it is a developer protection on the common case)

also in case of xdist, fd 0 and 1 are preoccupied and cannot be used due to the xdist protocol exchange

fabric has some implementation details i disagree with (i could rant, but that wont fix any problem)

it might be helpful to create a pytest plugin that monkey-patches the test capture state so that stdin is always a file (i can guide on this tomorrow and until the sprint, but i wont find the timeto tackle this before the sprint)

yourlabsopensource pushed a commit to yourlabs/shyml that referenced this issue Apr 24, 2019
Sorry for the broken release after disabling tests
Fighting with pytest-dev/pytest#1585
Re-enabled tests again
But cli2.autotest inside pytest cannot execute subprocess properly
So we now count on executing the tests through ./sh.yml test
To cover the code that actually executes scripts
yourlabsopensource pushed a commit to yourlabs/shyml that referenced this issue Apr 25, 2019
Sorry for the broken release after disabling tests
Fighting with pytest-dev/pytest#1585
Re-enabled tests again
But cli2.autotest inside pytest cannot execute subprocess properly
So we now count on executing the tests through ./sh.yml test
To cover the code that actually executes scripts
@blueyed blueyed changed the title Capture is all wrong Capture: avoid using SysCapture with FDCapture Oct 28, 2019
@blueyed
Copy link
Contributor

blueyed commented Oct 28, 2019

Some good info in here, re-opening for now.

@blueyed blueyed reopened this Oct 28, 2019
@blueyed blueyed added the plugin: capture related to the capture builtin plugin label Oct 28, 2019
@RonnyPfannschmidt
Copy link
Member

@blueyed for tackling the actual info effectively i propose cross-linking from a new more to the point issue in near future (and closing this one once that distilled issue is created)

@blueyed
Copy link
Contributor

blueyed commented Oct 28, 2019

ok, will try to keep it alive through my inbox.

@blueyed blueyed closed this as completed Oct 28, 2019
@bluetech
Copy link
Member

I also had a hard time understanding why FDCapture uses a SysCapture. I am trying to remove this dependency, if only to see why it is necessary.

Up to now, I found two reasons:

  1. Flushing: if only the FDs are redirected, but the sys objects remain the same, they can buffer up data then won't reach the underlying FD in time. This may be solvable by adding flush calls at appropriate places, though that breaks the abstraction a bit (need to reach the sys files anyway...).
    It might also be a nice feature though -- enables to test flushing, which is currently not possible.

  2. Isolation: Some tests in the pytest testsuite test that if the sys files are changed during a test, they are restored once it's done. This property may either be dropped, or handled some other way.

I still intend to try it -- will edit if I find more reasons 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plugin: capture related to the capture builtin plugin
Projects
None yet
Development

No branches or pull requests

4 participants