]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
pytest:conditional_ace_claims: ease export of failing tests to C
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Wed, 23 Aug 2023 03:23:29 +0000 (15:23 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 26 Sep 2023 23:45:35 +0000 (23:45 +0000)
When a test fails, this prints a little stanza like

static void test_something(void **state)
{
       INIT();
       USER_SIDS("WD", "AA");
       DEVICE_SIDS("BA", "BG");
       SD("D:(XA;;0x1f;;;AA;(! Member_of{SID(AA)}))");
       DENY_CHECK(0x10);
}

which is exactly right for copying into
libcli/security/tests/test_run_conditional_ace.c
which is much easier to iterate over with compiling and debugging.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/conditional_ace_claims.py

index c38586473c6f9df07bdde645f2193dbb1d7d4ae8..9b2aaef259fa515ca0e2a833f29dc33712db8c6c 100644 (file)
@@ -32,6 +32,63 @@ DEFAULT_ACCESS2 = (security.SEC_STD_READ_CONTROL |
                    security.SEC_ADS_READ_PROP)
 
 
+def write_c_test_on_failure(f):
+    """This is a function decorator that writes a function for
+    /libcli/security/tests/test_run_conditional_ace.c that runs the
+    equivalent test. Why?! Because iterating over a test to debug the
+    failure is slower in Python tests, but adding new tests is faster
+    in Python. So the flow goes like this:
+
+    1. add python tests, run them
+    2. if nothing fails, goto 1
+    3. copy the test_something() text into test_run_conditional_ace.c,
+       rename it, and add it to main().
+    4. `make bin/test_run_conditional_ace && rr bin/test_run_conditional_ace`
+    5. `rr replay`
+
+    and you're away. You can also just work from the Python, but a few
+    runs of `make -j` after touching something in libcli/security will
+    make you see why this exists.
+
+    You might be thinking that this surely took longer to write than
+    waiting 100 times for a 30 second compile, but that misses the
+    point that debugging needs to be ergonomic and fun.
+    """
+    from json import dumps as q  # JSON quoting is C quoting, more or less
+
+    def wrapper(name, token, sddl, access_desired):
+        try:
+            f(name, token, sddl, access_desired)
+        except Exception:
+            print()
+            print('static void test_something(void **state)')
+            print('{')
+            print('\tINIT();')
+            for s in ('sids', 'device_sids'):
+                if s in token:
+                    macro = ('user_sids' if s == 'sids' else s).upper()
+                    v = ', '.join(q(x) for x in token[s])
+                    print(f'\t{macro}({v});')
+            for s in ('user_claims', 'device_claims'):
+                if s in token:
+                    macro = s.upper()
+                    for name, values in token[s].items():
+                        if not isinstance(values, (list, tuple)):
+                            values = [values]
+                        v = ', '.join(q(x) for x in values)
+                        v = q(f"{v}")
+                        print(f'\t{macro}({q(name)}, {v});')
+            print(f'\tSD({q(sddl)});')
+            if 'allow' in f.__name__:
+                print(f'\tALLOW_CHECK({access_desired:#x});')
+            else:
+                print(f'\tDENY_CHECK({access_desired:#x});')
+            print('}')
+            print()
+            raise
+    return wrapper
+
+
 class ConditionalAceClaimsBase(TestCase):
     maxDiff = 0
 
@@ -77,6 +134,7 @@ class ConditionalAceClaimsBase(TestCase):
             with open(os.path.join(fuzz_seed_dir, name), 'w') as f:
                 f.write(sddl)
 
+    @write_c_test_on_failure
     def _test_allow_with_args(self, _token, sddl, access_desired):
         if isinstance(_token, dict):
             token = Token(**_token)
@@ -94,6 +152,7 @@ class ConditionalAceClaimsBase(TestCase):
 
         self.assertEqual(granted, access_desired)
 
+    @write_c_test_on_failure
     def _test_deny_with_args(self, token, sddl, access_desired):
         if isinstance(token, dict):
             token = Token(**token)