Commit cbfbf536 authored by Alexandre Duret-Lutz's avatar Alexandre Duret-Lutz

python: make sure spot.automata() terminates the command

Fixes #341.

* python/spot/__init__.py (automata): Rewrite and simplify using
the subprocess context manager.
* tests/python/341.py: New file.
* tests/Makefile.am: Add it.
* NEWS: Mention the issue.
parent 6afc2d45
Pipeline #1229 passed with stages
in 111 minutes and 15 seconds
...@@ -59,6 +59,9 @@ New in spot 2.5.2.dev (not yet released) ...@@ -59,6 +59,9 @@ New in spot 2.5.2.dev (not yet released)
- "autfilt -B --sat-minimize" was incorrectly producing - "autfilt -B --sat-minimize" was incorrectly producing
transition-based automata. transition-based automata.
- Using spot.automata("cmd...|") to read just a few automata out of
an infinite stream would not properly terminate the command.
New in spot 2.5.2 (2018-03-25) New in spot 2.5.2 (2018-03-25)
Bugs fixed: Bugs fixed:
......
...@@ -34,6 +34,7 @@ import subprocess ...@@ -34,6 +34,7 @@ import subprocess
import os import os
import signal import signal
import tempfile import tempfile
from contextlib import suppress as _supress
# The parrameters used by default when show() is called on an automaton. # The parrameters used by default when show() is called on an automaton.
_show_default = None _show_default = None
...@@ -441,30 +442,33 @@ def automata(*sources, timeout=None, ignore_abort=True, ...@@ -441,30 +442,33 @@ def automata(*sources, timeout=None, ignore_abort=True,
else: else:
p = automaton_stream_parser(filename, o) p = automaton_stream_parser(filename, o)
a = True a = True
while a: # Using proc as a context manager ensures that proc.stdout will be
# This returns None when we reach the end of the file. # closed on exit, and the process will be properly waited for.
a = p.parse(_bdd_dict).aut # This is important when running tools that produce an infinite
if a: # stream of automata and that must be killed once the generator
yield a # returned by spot.automata() is destroyed. Otherwise, _supress()
# is just a dummy context manager that does nothing (Python 3.7
# introduces nullcontext() for this purpose, but at the time of
# writing we support Python 3.4).
mgr = proc if proc else _supress()
with mgr:
while a:
# This returns None when we reach the end of the file.
a = p.parse(_bdd_dict).aut
if a:
yield a
finally: finally:
# Make sure we destroy the parser (p) and the subprocess # Make sure we destroy the parser (p) and the subprocess
# (prop) in the correct order... # (prop) in the correct order.
del p del p
if proc is not None: if proc is not None:
if not a: ret = proc.returncode
# We reached the end of the stream. Wait for the
# process to finish, so that we get its exit code.
ret = proc.wait()
else:
# if a != None, we probably got there through an
# exception, and the subprocess might still be
# running. Check if an exit status is available
# just in case.
ret = proc.poll()
del proc del proc
if ret: # Do not complain about the exit code if we are already raising
# an exception.
if ret and sys.exc_info()[0] is None:
raise subprocess.CalledProcessError(ret, filename[:-1]) raise subprocess.CalledProcessError(ret, filename[:-1])
# deleting o explicitely now prevents Python 3.5 from # deleting o explicitly now prevents Python 3.5 from
# reporting the following error: "<built-in function # reporting the following error: "<built-in function
# delete_automaton_parser_options> returned a result with # delete_automaton_parser_options> returned a result with
# an error set". It's not clear to me if the bug is in Python # an error set". It's not clear to me if the bug is in Python
......
...@@ -351,10 +351,10 @@ TESTS_ipython = \ ...@@ -351,10 +351,10 @@ TESTS_ipython = \
# do not consider part of the documentation: those have to start # do not consider part of the documentation: those have to start
# with a _. # with a _.
TESTS_python = \ TESTS_python = \
python/341.py \
python/_altscc.ipynb \ python/_altscc.ipynb \
python/_autparserr.ipynb \ python/_autparserr.ipynb \
python/_aux.ipynb \ python/_aux.ipynb \
python/_word.ipynb \
python/accparse2.py \ python/accparse2.py \
python/alarm.py \ python/alarm.py \
python/alternating.py \ python/alternating.py \
...@@ -362,6 +362,7 @@ TESTS_python = \ ...@@ -362,6 +362,7 @@ TESTS_python = \
python/bddnqueen.py \ python/bddnqueen.py \
python/bugdet.py \ python/bugdet.py \
python/declenv.py \ python/declenv.py \
python/_word.ipynb \
python/decompose_scc.py \ python/decompose_scc.py \
python/dualize.py \ python/dualize.py \
python/except.py \ python/except.py \
......
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017 Laboratoire de Recherche et Développement de l'Epita
# (LRDE).
#
# This file is part of Spot, a model checking library.
#
# Spot is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Spot is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import spot
from subprocess import _active
def two_intersecting_automata():
"""return two random automata with a non-empty intersection"""
g = spot.automata('randaut -A4 -Q5 -n-1 2 |')
for a, b in zip(g, g):
if a.intersects(b):
return a, b
for i in range(5):
two_intersecting_automata()
n = len(_active)
print(n, "active processes")
assert(n == 0);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment