diff mbox series

[RFC,net-next,1/2] selftests: drv-net: add ability to schedule cleanup with defer()

Message ID 20240626013611.2330979-2-kuba@kernel.org (mailing list archive)
State Superseded
Delegated to: Netdev Maintainers
Headers show
Series selftests: drv-net: add ability to schedule cleanup with defer() | expand

Checks

Context Check Description
netdev/tree_selection success Clearly marked for net-next
netdev/apply fail Patch does not apply to net-next-0

Commit Message

Jakub Kicinski June 26, 2024, 1:36 a.m. UTC
This implements what I was describing in [1]. When writing a test
author can schedule cleanup / undo actions right after the creation
completes, eg:

  cmd("touch /tmp/file")
  defer(cmd, "rm /tmp/file")

defer() takes the function name as first argument, and the rest are
arguments for that function. defer()red functions are called in
inverse order after test exits. It's also possible to capture them
and execute earlier (in which case they get automatically de-queued).

  undo = defer(cmd, "rm /tmp/file")
  # ... some unsafe code ...
  undo.exec()

As a nice safety all exceptions from defer()ed calls are captured,
printed, and ignored (they do make the test fail, however).
This addresses the common problem of exceptions in cleanup paths
often being unhandled, leading to potential leaks.

There is a global action queue, flushed by ksft_run(). We could support
function level defers too, I guess, but there's no immediate need..

Link: https://lore.kernel.org/all/877cedb2ki.fsf@nvidia.com/ # [1]
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/testing/selftests/net/lib/py/ksft.py  | 49 +++++++++++++++------
 tools/testing/selftests/net/lib/py/utils.py | 41 +++++++++++++++++
 2 files changed, 76 insertions(+), 14 deletions(-)

Comments

Przemek Kitszel June 26, 2024, 7:43 a.m. UTC | #1
On 6/26/24 03:36, Jakub Kicinski wrote:
> This implements what I was describing in [1]. When writing a test
> author can schedule cleanup / undo actions right after the creation
> completes, eg:
> 
>    cmd("touch /tmp/file")
>    defer(cmd, "rm /tmp/file")
> 
> defer() takes the function name as first argument, and the rest are
> arguments for that function. defer()red functions are called in
> inverse order after test exits. 

I like the defer name more than undo, and that makes immediate
connection to golang's defer.

For reference golang's defer is called at function exit, the choice was
made that way (instead C++'s destructor at the end of the scope) to make
it more performant. It's a language construct, so could not be easily 
translated into python.

I see why you expect discussion to be potentially long here ;), I just
showed it to my python speaking friends and all the alternatives came
up: context manager, yield, pytest "built in" fixtures and so on.
I like your approach more through, it is much nicer at actual-test code
side.


> It's also possible to capture them
> and execute earlier (in which case they get automatically de-queued).

not sure if this is good, either test developer should be able to easily
guard call to defer, or could call it with a lambda capture so actual
execution will be no-op-ish.
OTOH, why not, both .exec() and .cancel() are nice to use

so, I'm fine both ways

> 
>    undo = defer(cmd, "rm /tmp/file")
>    # ... some unsafe code ...
>    undo.exec()
> 
> As a nice safety all exceptions from defer()ed calls are captured,
> printed, and ignored (they do make the test fail, however).
> This addresses the common problem of exceptions in cleanup paths
> often being unhandled, leading to potential leaks.

Nice! Please only make it so that cleanup-failure does not overwrite
happy-test-path-failure (IOW "ret = ret ? ret : cleanup_ret")

> 
> There is a global action queue, flushed by ksft_run(). We could support
> function level defers too, I guess, but there's no immediate need..

That would be a must have for general solution, would it require some
boilerplate code at function level?

> 
> Link: https://lore.kernel.org/all/877cedb2ki.fsf@nvidia.com/ # [1]
> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
> ---
>   tools/testing/selftests/net/lib/py/ksft.py  | 49 +++++++++++++++------
>   tools/testing/selftests/net/lib/py/utils.py | 41 +++++++++++++++++
>   2 files changed, 76 insertions(+), 14 deletions(-)
> 
> diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py
> index 45ffe277d94a..4a72b9cbb27d 100644
> --- a/tools/testing/selftests/net/lib/py/ksft.py
> +++ b/tools/testing/selftests/net/lib/py/ksft.py
> @@ -6,6 +6,7 @@ import sys
>   import time
>   import traceback
>   from .consts import KSFT_MAIN_NAME
> +from .utils import global_defer_queue
>   
>   KSFT_RESULT = None
>   KSFT_RESULT_ALL = True
> @@ -108,6 +109,24 @@ KSFT_RESULT_ALL = True
>       print(res)
>   
>   
> +def ksft_flush_defer():
> +    global KSFT_RESULT
> +
> +    while global_defer_queue:
> +        entry = global_defer_queue[-1]
> +        try:
> +            entry.exec()
> +        except Exception:
> +            if global_defer_queue and global_defer_queue[-1] == entry:
> +                global_defer_queue.pop()
> +
> +            ksft_pr("Exception while handling defer / cleanup!")

please print current queue size, if only for convenience of test
developer to be able tell if they are moving forward in 
fix-rerun-observe cycle

> +            tb = traceback.format_exc()
> +            for line in tb.strip().split('\n'):
> +                ksft_pr("Defer Exception|", line)
> +            KSFT_RESULT = False

I have no idea if this could be other error than just False, if so,
don't overwrite

[...]

>   
> +global_defer_queue = []
> +
> +
> +class defer:
> +    def __init__(self, func, *args, **kwargs):
> +        global global_defer_queue
> +        if global_defer_queue is None:
> +            raise Exception("defer environment has not been set up")
> +
> +        if not callable(func):
> +            raise Exception("defer created with un-callable object, did you call the function instead of passing its name?")
> +
> +        self.func = func
> +        self.args = args
> +        self.kwargs = kwargs
> +
> +        self.queued = True
> +        self.executed = False
> +
> +        self._queue =  global_defer_queue
> +        self._queue.append(self)
> +
> +    def __enter__(self):
> +        return self
> +
> +    def __exit__(self, ex_type, ex_value, ex_tb):
> +        return self.exec()

why do you need __enter__ and __exit__ if this is not a context
manager / to-be-used-via-with?

> +
> +    def _exec(self):
> +        self.func(*self.args, **self.kwargs)
> +
> +    def cancel(self):
> +        self._queue.remove(self)
> +        self.queued = False
> +
> +    def exec(self):
> +        self._exec()
> +        self.cancel()
> +        self.executed = True
> +
> +
>   def tool(name, args, json=None, ns=None, host=None):
>       cmd_str = name + ' '
>       if json:
Petr Machata June 26, 2024, 9:19 a.m. UTC | #2
Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:

> On 6/26/24 03:36, Jakub Kicinski wrote:
>
>> There is a global action queue, flushed by ksft_run(). We could support
>> function level defers too, I guess, but there's no immediate need..
>
> That would be a must have for general solution, would it require some
> boilerplate code at function level?

Presumably you'd need a per-function defer pool.

Which would be naturally modeled as a context manager, but I promised
myself I'd shut up about that.

>> +    def __enter__(self):
>> +        return self
>> +
>> +    def __exit__(self, ex_type, ex_value, ex_tb):
>> +        return self.exec()
>
> why do you need __enter__ and __exit__ if this is not a context
> manager / to-be-used-via-with?

But you could use it as a context manager.

    with defer(blah blah):
        do stuff
    # blah blah runs here

IMHO makes sense.
Przemek Kitszel June 26, 2024, 9:38 a.m. UTC | #3
On 6/26/24 11:19, Petr Machata wrote:
> 
> Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:
> 
>> On 6/26/24 03:36, Jakub Kicinski wrote:
>>
>>> There is a global action queue, flushed by ksft_run(). We could support
>>> function level defers too, I guess, but there's no immediate need..
>>
>> That would be a must have for general solution, would it require some
>> boilerplate code at function level?
> 
> Presumably you'd need a per-function defer pool.
> 
> Which would be naturally modeled as a context manager, but I promised
> myself I'd shut up about that.
> 
>>> +    def __enter__(self):
>>> +        return self
>>> +
>>> +    def __exit__(self, ex_type, ex_value, ex_tb):
>>> +        return self.exec()
>>
>> why do you need __enter__ and __exit__ if this is not a context
>> manager / to-be-used-via-with?
> 
> But you could use it as a context manager.
> 
>      with defer(blah blah):
>          do stuff
>      # blah blah runs here
> 
> IMHO makes sense.

oh, nice! agree!
Then this little part is "the general solution", with the rest
(global queue) added as a sugar layer for kselftests, to only
avoid inflating indentation levels, great
Petr Machata June 26, 2024, 10:18 a.m. UTC | #4
Jakub Kicinski <kuba@kernel.org> writes:

> This implements what I was describing in [1]. When writing a test
> author can schedule cleanup / undo actions right after the creation
> completes, eg:
>
>   cmd("touch /tmp/file")
>   defer(cmd, "rm /tmp/file")
>
> defer() takes the function name as first argument, and the rest are
> arguments for that function. defer()red functions are called in
> inverse order after test exits. It's also possible to capture them
> and execute earlier (in which case they get automatically de-queued).
>
>   undo = defer(cmd, "rm /tmp/file")
>   # ... some unsafe code ...
>   undo.exec()
>
> As a nice safety all exceptions from defer()ed calls are captured,
> printed, and ignored (they do make the test fail, however).
> This addresses the common problem of exceptions in cleanup paths
> often being unhandled, leading to potential leaks.
>
> There is a global action queue, flushed by ksft_run(). We could support
> function level defers too, I guess, but there's no immediate need..
>
> Link: https://lore.kernel.org/all/877cedb2ki.fsf@nvidia.com/ # [1]
> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
> ---
>  tools/testing/selftests/net/lib/py/ksft.py  | 49 +++++++++++++++------
>  tools/testing/selftests/net/lib/py/utils.py | 41 +++++++++++++++++
>  2 files changed, 76 insertions(+), 14 deletions(-)
>
> diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py
> index 45ffe277d94a..4a72b9cbb27d 100644
> --- a/tools/testing/selftests/net/lib/py/ksft.py
> +++ b/tools/testing/selftests/net/lib/py/ksft.py
> @@ -6,6 +6,7 @@ import sys
>  import time
>  import traceback
>  from .consts import KSFT_MAIN_NAME
> +from .utils import global_defer_queue
>  
>  KSFT_RESULT = None
>  KSFT_RESULT_ALL = True
> @@ -108,6 +109,24 @@ KSFT_RESULT_ALL = True
>      print(res)
>  
>  
> +def ksft_flush_defer():
> +    global KSFT_RESULT
> +
> +    while global_defer_queue:
> +        entry = global_defer_queue[-1]
> +        try:
> +            entry.exec()

I wonder if you added _exec() to invoke it here. Because then you could
just do entry = global_defer_queue.pop() and entry._exec(), and in the
except branch you would just have the test-related business, without the
queue management.

> +        except Exception:

I think this should be either an unqualified except: or except
BaseException:.

> +            if global_defer_queue and global_defer_queue[-1] == entry:
> +                global_defer_queue.pop()
> +
> +            ksft_pr("Exception while handling defer / cleanup!")

Hmm, I was thinking about adding defer.__str__ and using it here to
give more clue as to where it went wrong, but the traceback is IMHO
plenty good enough.

> +            tb = traceback.format_exc()
> +            for line in tb.strip().split('\n'):
> +                ksft_pr("Defer Exception|", line)
> +            KSFT_RESULT = False
> +
> +
>  def ksft_run(cases=None, globs=None, case_pfx=None, args=()):
>      cases = cases or []
>  
> @@ -130,29 +149,31 @@ KSFT_RESULT_ALL = True
>      for case in cases:
>          KSFT_RESULT = True
>          cnt += 1
> +        comment = ""
> +        cnt_key = ""
> +
>          try:
>              case(*args)
>          except KsftSkipEx as e:
> -            ktap_result(True, cnt, case, comment="SKIP " + str(e))
> -            totals['skip'] += 1
> -            continue
> +            comment = "SKIP " + str(e)
> +            cnt_key = 'skip'
>          except KsftXfailEx as e:
> -            ktap_result(True, cnt, case, comment="XFAIL " + str(e))
> -            totals['xfail'] += 1
> -            continue
> +            comment = "XFAIL " + str(e)
> +            cnt_key = 'xfail'
>          except Exception as e:
>              tb = traceback.format_exc()
>              for line in tb.strip().split('\n'):
>                  ksft_pr("Exception|", line)
> -            ktap_result(False, cnt, case)
> -            totals['fail'] += 1
> -            continue
> +            KSFT_RESULT = False
> +            cnt_key = 'fail'
>  
> -        ktap_result(KSFT_RESULT, cnt, case)
> -        if KSFT_RESULT:
> -            totals['pass'] += 1
> -        else:
> -            totals['fail'] += 1
> +        ksft_flush_defer()
> +
> +        if not cnt_key:
> +            cnt_key = 'pass' if KSFT_RESULT else 'fail'
> +
> +        ktap_result(KSFT_RESULT, cnt, case, comment=comment)
> +        totals[cnt_key] += 1
>  
>      print(
>          f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"

Majority of this hunk is just preparatory and should be in a patch of
its own. Then in this patch it should just introduce the flush.

> diff --git a/tools/testing/selftests/net/lib/py/utils.py b/tools/testing/selftests/net/lib/py/utils.py
> index 405aa510aaf2..1ef6ebaa369e 100644
> --- a/tools/testing/selftests/net/lib/py/utils.py
> +++ b/tools/testing/selftests/net/lib/py/utils.py
> @@ -66,6 +66,47 @@ import time
>          return self.process(terminate=self.terminate, fail=self.check_fail)
>  
>  
> +global_defer_queue = []
> +
> +
> +class defer:
> +    def __init__(self, func, *args, **kwargs):
> +        global global_defer_queue
> +        if global_defer_queue is None:
> +            raise Exception("defer environment has not been set up")
> +
> +        if not callable(func):
> +            raise Exception("defer created with un-callable object, did you call the function instead of passing its name?")
> +
> +        self.func = func
> +        self.args = args
> +        self.kwargs = kwargs
> +
> +        self.queued = True
> +        self.executed = False
> +
> +        self._queue =  global_defer_queue
> +        self._queue.append(self)
> +
> +    def __enter__(self):
> +        return self
> +
> +    def __exit__(self, ex_type, ex_value, ex_tb):
> +        return self.exec()
> +
> +    def _exec(self):
> +        self.func(*self.args, **self.kwargs)
> +
> +    def cancel(self):

This shouldn't dequeue if not self.queued.

> +        self._queue.remove(self)
> +        self.queued = False
> +
> +    def exec(self):

This shouldn't exec if self.executed.

But I actually wonder if we need two flags at all. Whether the defer
entry is resolved through exec(), cancel() or __exit__(), it's "done".
It could be left in the queue, in which case the "done" flag is going to
disable future exec requests. Or it can just be dropped from the queue
when done, in which case we don't even need the "done" flag as such.

> +        self._exec()
> +        self.cancel()
> +        self.executed = True
> +
> +
>  def tool(name, args, json=None, ns=None, host=None):
>      cmd_str = name + ' '
>      if json:
Jakub Kicinski June 26, 2024, 4:09 p.m. UTC | #5
On Wed, 26 Jun 2024 12:18:58 +0200 Petr Machata wrote:
> Jakub Kicinski <kuba@kernel.org> writes:
> > +def ksft_flush_defer():
> > +    global KSFT_RESULT
> > +
> > +    while global_defer_queue:
> > +        entry = global_defer_queue[-1]
> > +        try:
> > +            entry.exec()  
> 
> I wonder if you added _exec() to invoke it here. Because then you could
> just do entry = global_defer_queue.pop() and entry._exec(), and in the
> except branch you would just have the test-related business, without the
> queue management.

Initially I had both _exec, and _dequeue as separate helpers, but then
_dequeue was identical to cancel, so I removed that one, but _exec
stayed.

As you point out _exec() would do nicely during "flush".. but linter was
angry at me for calling private functions. I couldn't quickly think of
a clean scheme of naming things. Or rather, I should say, I like that
the only non-private functions in class defer right now are
test-author-facing. At some point I considered renaming _exec() to
__call__() or run() but I was worried people will incorrectly
call it, instead of calling exec().

So I decided to stick to a bit of awkward handling in the internals for
the benefit of more obvious test-facing API. But no strong preference,
LMK if calling _exec() here is fine or I should rename it..

> > +        except Exception:  
> 
> I think this should be either an unqualified except: or except
> BaseException:.

SG


> >      print(
> >          f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"  
> 
> Majority of this hunk is just preparatory and should be in a patch of
> its own. Then in this patch it should just introduce the flush.

True, will split.

> > +    def cancel(self):  
> 
> This shouldn't dequeue if not self.queued.

I was wondering if we're better off throwing the exception from
remove() or silently ignoring (what is probably an error in the 
test code). I went with the former intentionally, but happy to
change.

> > +        self._queue.remove(self)
> > +        self.queued = False
> > +
> > +    def exec(self):  
> 
> This shouldn't exec if self.executed.
> 
> But I actually wonder if we need two flags at all. Whether the defer
> entry is resolved through exec(), cancel() or __exit__(), it's "done".
> It could be left in the queue, in which case the "done" flag is going to
> disable future exec requests. Or it can just be dropped from the queue
> when done, in which case we don't even need the "done" flag as such.

If you recall there's a rss_ctx test case which removes contexts out of
order. The flags are basically for that test. We run the .exec() to
remove a context, and then we can check 

	if thing.queued:
		.. code for context that's alive ..
	else:
		.. code for dead context ..
Jakub Kicinski June 26, 2024, 4:44 p.m. UTC | #6
On Wed, 26 Jun 2024 11:19:08 +0200 Petr Machata wrote:
> >> There is a global action queue, flushed by ksft_run(). We could support
> >> function level defers too, I guess, but there's no immediate need..  
> >
> > That would be a must have for general solution, would it require some
> > boilerplate code at function level?  
> 
> Presumably you'd need a per-function defer pool.
> 
> Which would be naturally modeled as a context manager, but I promised
> myself I'd shut up about that.

No preference on the internal mechanism :)

The part I was unsure about was whether in this case test writer will
want to mix the contexts:

	fq = FuncDeferQueue()
	fq.defer(...) # this one is called when function exits
	defer(...)    # this one is global

or not:

	defer_func_context()
	# any defer after this is assumed to be func-level
	defer(...)

so probably best to wait until we have some real examples.
Jakub Kicinski June 26, 2024, 4:49 p.m. UTC | #7
On Wed, 26 Jun 2024 09:43:54 +0200 Przemek Kitszel wrote:
> > As a nice safety all exceptions from defer()ed calls are captured,
> > printed, and ignored (they do make the test fail, however).
> > This addresses the common problem of exceptions in cleanup paths
> > often being unhandled, leading to potential leaks.  
> 
> Nice! Please only make it so that cleanup-failure does not overwrite
> happy-test-path-failure (IOW "ret = ret ? ret : cleanup_ret")

That should be what we end up doing. The ret is a boolean (pass / fail)
so we have:

	pass &= cleanup_pass

effectively.

> > +            ksft_pr("Exception while handling defer / cleanup!")  
> 
> please print current queue size, if only for convenience of test
> developer to be able tell if they are moving forward in 
> fix-rerun-observe cycle

Hm... not a bad point, defer() cycles are possible.
But then again, we don't guard against infinite loops 
in  tests either, and kselftest runner (the general one, 
outside our Python) has a timeout, so it will kill the script.

> > +            tb = traceback.format_exc()
> > +            for line in tb.strip().split('\n'):
> > +                ksft_pr("Defer Exception|", line)
> > +            KSFT_RESULT = False  
> 
> I have no idea if this could be other error than just False, if so,
> don't overwrite

Yup, just True / False. The other types (skip, xfail) are a pass
(True) plus a comment, per KTAP spec.
Petr Machata June 27, 2024, 7:37 a.m. UTC | #8
Jakub Kicinski <kuba@kernel.org> writes:

> On Wed, 26 Jun 2024 12:18:58 +0200 Petr Machata wrote:
>> Jakub Kicinski <kuba@kernel.org> writes:
>> > +def ksft_flush_defer():
>> > +    global KSFT_RESULT
>> > +
>> > +    while global_defer_queue:
>> > +        entry = global_defer_queue[-1]
>> > +        try:
>> > +            entry.exec()  
>> 
>> I wonder if you added _exec() to invoke it here. Because then you could
>> just do entry = global_defer_queue.pop() and entry._exec(), and in the
>> except branch you would just have the test-related business, without the
>> queue management.
>
> Initially I had both _exec, and _dequeue as separate helpers, but then
> _dequeue was identical to cancel, so I removed that one, but _exec
> stayed.
>
> As you point out _exec() would do nicely during "flush".. but linter was
> angry at me for calling private functions. I couldn't quickly think of
> a clean scheme of naming things. Or rather, I should say, I like that
> the only non-private functions in class defer right now are
> test-author-facing. At some point I considered renaming _exec() to
> __call__() or run() but I was worried people will incorrectly
> call it, instead of calling exec().
>
> So I decided to stick to a bit of awkward handling in the internals for
> the benefit of more obvious test-facing API. But no strong preference,
> LMK if calling _exec() here is fine or I should rename it..

Maybe call it something like exec_only()? There's a list that you just
need to go through, it looks a shame not to just .pop() everything out
one by one and instead have this management business in the error path.

>> > +        except Exception:  
>> 
>> I think this should be either an unqualified except: or except
>> BaseException:.
>
> SG
>
>
>> >      print(
>> >          f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"  
>> 
>> Majority of this hunk is just preparatory and should be in a patch of
>> its own. Then in this patch it should just introduce the flush.
>
> True, will split.
>
>> > +    def cancel(self):  
>> 
>> This shouldn't dequeue if not self.queued.
>
> I was wondering if we're better off throwing the exception from
> remove() or silently ignoring (what is probably an error in the 
> test code). I went with the former intentionally, but happy to
> change.

Hmm, right, it would throw. Therefore second exec() would as well. Good.
But that means that exec() should first cancel, then exec, otherwise
second exec invocation would actually exec the cleanup a second time
before bailing out.

>
>> > +        self._queue.remove(self)
>> > +        self.queued = False
>> > +
>> > +    def exec(self):  
>> 
>> This shouldn't exec if self.executed.
>> 
>> But I actually wonder if we need two flags at all. Whether the defer
>> entry is resolved through exec(), cancel() or __exit__(), it's "done".
>> It could be left in the queue, in which case the "done" flag is going to
>> disable future exec requests. Or it can just be dropped from the queue
>> when done, in which case we don't even need the "done" flag as such.
>
> If you recall there's a rss_ctx test case which removes contexts out of
> order. The flags are basically for that test. We run the .exec() to
> remove a context, and then we can check 
>
> 	if thing.queued:
> 		.. code for context that's alive ..
> 	else:
> 		.. code for dead context ..

That test already has its own flags to track which was removed, can't it
use those? My preference is always to keep an API as minimal as possible
and the flags, if any, would ideally be private. I don't think defer
objects should keep track of whether the user has already invoked them
or not, that's their user's business to know.
Przemek Kitszel June 27, 2024, 8:40 a.m. UTC | #9
On 6/26/24 18:49, Jakub Kicinski wrote:
> On Wed, 26 Jun 2024 09:43:54 +0200 Przemek Kitszel wrote:
>>> As a nice safety all exceptions from defer()ed calls are captured,
>>> printed, and ignored (they do make the test fail, however).
>>> This addresses the common problem of exceptions in cleanup paths
>>> often being unhandled, leading to potential leaks.
>>
>> Nice! Please only make it so that cleanup-failure does not overwrite
>> happy-test-path-failure (IOW "ret = ret ? ret : cleanup_ret")
> 
> That should be what we end up doing. The ret is a boolean (pass / fail)
> so we have:
> 
> 	pass &= cleanup_pass
> 
> effectively.
> 
>>> +            ksft_pr("Exception while handling defer / cleanup!")
>>
>> please print current queue size, if only for convenience of test
>> developer to be able tell if they are moving forward in
>> fix-rerun-observe cycle
> 
> Hm... not a bad point, defer() cycles are possible.
> But then again, we don't guard against infinite loops
> in  tests either, and kselftest runner (the general one,
> outside our Python) has a timeout, so it will kill the script.

I mean the flow:
$EDITOR mytest.py
./mytest.py
# output: Exception while handling defer / cleanup (at 4 out of 13 cleanups)

then repeat with the hope that fix to cleanup procedure will move us
forward, say:
$EDITOR mytest.py; ./mytest.py
#output: ... (at 7 of 13 cleanups)

just name of failed cleanup method is not enough as those could be
added via loop

> 
>>> +            tb = traceback.format_exc()
>>> +            for line in tb.strip().split('\n'):
>>> +                ksft_pr("Defer Exception|", line)
>>> +            KSFT_RESULT = False
>>
>> I have no idea if this could be other error than just False, if so,
>> don't overwrite
> 
> Yup, just True / False. The other types (skip, xfail) are a pass
> (True) plus a comment, per KTAP spec.
> 


Thanks!
Jakub Kicinski June 27, 2024, 3:35 p.m. UTC | #10
On Thu, 27 Jun 2024 10:40:31 +0200 Przemek Kitszel wrote:
> > Hm... not a bad point, defer() cycles are possible.
> > But then again, we don't guard against infinite loops
> > in  tests either, and kselftest runner (the general one,
> > outside our Python) has a timeout, so it will kill the script.  
> 
> I mean the flow:
> $EDITOR mytest.py
> ./mytest.py
> # output: Exception while handling defer / cleanup (at 4 out of 13 cleanups)
> 
> then repeat with the hope that fix to cleanup procedure will move us
> forward, say:
> $EDITOR mytest.py; ./mytest.py
> #output: ... (at 7 of 13 cleanups)
> 
> just name of failed cleanup method is not enough as those could be
> added via loop

Oh, yes, nice one!
Jakub Kicinski June 27, 2024, 3:41 p.m. UTC | #11
On Thu, 27 Jun 2024 09:37:50 +0200 Petr Machata wrote:
> > I was wondering if we're better off throwing the exception from
> > remove() or silently ignoring (what is probably an error in the 
> > test code). I went with the former intentionally, but happy to
> > change.  
> 
> Hmm, right, it would throw. Therefore second exec() would as well. Good.
> But that means that exec() should first cancel, then exec, otherwise
> second exec invocation would actually exec the cleanup a second time
> before bailing out.

Good point, that sounds safer.

> >> This shouldn't exec if self.executed.
> >> 
> >> But I actually wonder if we need two flags at all. Whether the defer
> >> entry is resolved through exec(), cancel() or __exit__(), it's "done".
> >> It could be left in the queue, in which case the "done" flag is going to
> >> disable future exec requests. Or it can just be dropped from the queue
> >> when done, in which case we don't even need the "done" flag as such.  
> >
> > If you recall there's a rss_ctx test case which removes contexts out of
> > order. The flags are basically for that test. We run the .exec() to
> > remove a context, and then we can check 
> >
> > 	if thing.queued:
> > 		.. code for context that's alive ..
> > 	else:
> > 		.. code for dead context ..  
> 
> That test already has its own flags to track which was removed, can't it
> use those? My preference is always to keep an API as minimal as possible
> and the flags, if any, would ideally be private. I don't think defer
> objects should keep track of whether the user has already invoked them
> or not, that's their user's business to know.

Ack, will delete it then.
diff mbox series

Patch

diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py
index 45ffe277d94a..4a72b9cbb27d 100644
--- a/tools/testing/selftests/net/lib/py/ksft.py
+++ b/tools/testing/selftests/net/lib/py/ksft.py
@@ -6,6 +6,7 @@  import sys
 import time
 import traceback
 from .consts import KSFT_MAIN_NAME
+from .utils import global_defer_queue
 
 KSFT_RESULT = None
 KSFT_RESULT_ALL = True
@@ -108,6 +109,24 @@  KSFT_RESULT_ALL = True
     print(res)
 
 
+def ksft_flush_defer():
+    global KSFT_RESULT
+
+    while global_defer_queue:
+        entry = global_defer_queue[-1]
+        try:
+            entry.exec()
+        except Exception:
+            if global_defer_queue and global_defer_queue[-1] == entry:
+                global_defer_queue.pop()
+
+            ksft_pr("Exception while handling defer / cleanup!")
+            tb = traceback.format_exc()
+            for line in tb.strip().split('\n'):
+                ksft_pr("Defer Exception|", line)
+            KSFT_RESULT = False
+
+
 def ksft_run(cases=None, globs=None, case_pfx=None, args=()):
     cases = cases or []
 
@@ -130,29 +149,31 @@  KSFT_RESULT_ALL = True
     for case in cases:
         KSFT_RESULT = True
         cnt += 1
+        comment = ""
+        cnt_key = ""
+
         try:
             case(*args)
         except KsftSkipEx as e:
-            ktap_result(True, cnt, case, comment="SKIP " + str(e))
-            totals['skip'] += 1
-            continue
+            comment = "SKIP " + str(e)
+            cnt_key = 'skip'
         except KsftXfailEx as e:
-            ktap_result(True, cnt, case, comment="XFAIL " + str(e))
-            totals['xfail'] += 1
-            continue
+            comment = "XFAIL " + str(e)
+            cnt_key = 'xfail'
         except Exception as e:
             tb = traceback.format_exc()
             for line in tb.strip().split('\n'):
                 ksft_pr("Exception|", line)
-            ktap_result(False, cnt, case)
-            totals['fail'] += 1
-            continue
+            KSFT_RESULT = False
+            cnt_key = 'fail'
 
-        ktap_result(KSFT_RESULT, cnt, case)
-        if KSFT_RESULT:
-            totals['pass'] += 1
-        else:
-            totals['fail'] += 1
+        ksft_flush_defer()
+
+        if not cnt_key:
+            cnt_key = 'pass' if KSFT_RESULT else 'fail'
+
+        ktap_result(KSFT_RESULT, cnt, case, comment=comment)
+        totals[cnt_key] += 1
 
     print(
         f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"
diff --git a/tools/testing/selftests/net/lib/py/utils.py b/tools/testing/selftests/net/lib/py/utils.py
index 405aa510aaf2..1ef6ebaa369e 100644
--- a/tools/testing/selftests/net/lib/py/utils.py
+++ b/tools/testing/selftests/net/lib/py/utils.py
@@ -66,6 +66,47 @@  import time
         return self.process(terminate=self.terminate, fail=self.check_fail)
 
 
+global_defer_queue = []
+
+
+class defer:
+    def __init__(self, func, *args, **kwargs):
+        global global_defer_queue
+        if global_defer_queue is None:
+            raise Exception("defer environment has not been set up")
+
+        if not callable(func):
+            raise Exception("defer created with un-callable object, did you call the function instead of passing its name?")
+
+        self.func = func
+        self.args = args
+        self.kwargs = kwargs
+
+        self.queued = True
+        self.executed = False
+
+        self._queue =  global_defer_queue
+        self._queue.append(self)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, ex_type, ex_value, ex_tb):
+        return self.exec()
+
+    def _exec(self):
+        self.func(*self.args, **self.kwargs)
+
+    def cancel(self):
+        self._queue.remove(self)
+        self.queued = False
+
+    def exec(self):
+        self._exec()
+        self.cancel()
+        self.executed = True
+
+
 def tool(name, args, json=None, ns=None, host=None):
     cmd_str = name + ' '
     if json: